<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Amira Abidi</title>
    <description>The latest articles on DEV Community by Amira Abidi (@amira_abidi).</description>
    <link>https://dev.to/amira_abidi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4071897%2F77d0ea5b-1d71-495f-8327-cf783cccdea0.jpg</url>
      <title>DEV Community: Amira Abidi</title>
      <link>https://dev.to/amira_abidi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amira_abidi"/>
    <language>en</language>
    <item>
      <title>AWS EKS Networking: Understanding What Happens Behind the Cluster</title>
      <dc:creator>Amira Abidi</dc:creator>
      <pubDate>Tue, 22 Sep 2026 07:53:40 +0000</pubDate>
      <link>https://dev.to/amira_abidi/aws-eks-networking-understanding-what-happens-behind-the-cluster-3ii9</link>
      <guid>https://dev.to/amira_abidi/aws-eks-networking-understanding-what-happens-behind-the-cluster-3ii9</guid>
      <description>&lt;p&gt;When I started working with Amazon EKS, I initially focused on the Kubernetes layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pods&lt;/li&gt;
&lt;li&gt;Deployments&lt;/li&gt;
&lt;li&gt;Services&lt;/li&gt;
&lt;li&gt;Ingress&lt;/li&gt;
&lt;li&gt;HPA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But running Kubernetes on AWS quickly raises another question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens at the network level?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How does a pod communicate with another pod?&lt;br&gt;
How does Internet traffic reach the application?&lt;br&gt;
How does a pod connect to Amazon RDS?&lt;/p&gt;

&lt;p&gt;While building my &lt;strong&gt;LibraryCorner&lt;/strong&gt; platform, I spent time understanding how Kubernetes networking interacts with AWS networking.&lt;/p&gt;

&lt;p&gt;Here are the main concepts I found important.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. EKS runs inside an AWS VPC
&lt;/h2&gt;

&lt;p&gt;The first thing to understand is that an EKS cluster is connected to an AWS VPC.&lt;/p&gt;

&lt;p&gt;The VPC provides the underlying network for the cluster and its AWS resources.&lt;/p&gt;

&lt;p&gt;A simplified architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Internet
                       │
                       ▼
               AWS Load Balancer
                       │
                       ▼
                Kubernetes Service
                       │
                       ▼
                      Pods
                       │
              ┌────────┴────────┐
              ▼                 ▼
             EKS               RDS
           workloads         PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that understanding EKS also requires understanding basic AWS networking.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Public and private subnets
&lt;/h2&gt;

&lt;p&gt;A common architecture separates public-facing resources from private workloads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
   │
   ▼
Public subnet
   │
   ▼
Load Balancer
   │
   ▼
Private subnet
   │
   ▼
EKS workloads
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application workloads don't need to be directly exposed to the Internet.&lt;/p&gt;

&lt;p&gt;This separation reduces the attack surface and makes the architecture easier to control.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What does the NAT Gateway do?
&lt;/h2&gt;

&lt;p&gt;Private workloads may still need outbound Internet access.&lt;/p&gt;

&lt;p&gt;For example, a pod may need to reach an external API or download something.&lt;/p&gt;

&lt;p&gt;A NAT Gateway provides this outbound path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Private subnet
      │
      ▼
NAT Gateway
      │
      ▼
Internet Gateway
      │
      ▼
Internet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important distinction is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NAT Gateway enables outbound connectivity. It does not make the private workload directly reachable from the Internet.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. How do Pods get network connectivity?
&lt;/h2&gt;

&lt;p&gt;With the Amazon VPC CNI, Kubernetes pods can receive IP addresses from the VPC network.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VPC
 │
 ├── EKS Node
 │     ├── Pod → IP
 │     ├── Pod → IP
 │     └── Pod → IP
 │
 └── RDS → IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a strong relationship between Kubernetes and the AWS network.&lt;/p&gt;

&lt;p&gt;It also means that &lt;strong&gt;IP address availability matters when scaling an EKS cluster&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;More pods can mean more demand for available IP addresses and subnet capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. How does Internet traffic reach a Pod?
&lt;/h2&gt;

&lt;p&gt;Pods are ephemeral, so we don't want clients connecting directly to individual pod IPs.&lt;/p&gt;

&lt;p&gt;Instead, Kubernetes and AWS provide abstractions for routing traffic.&lt;/p&gt;

&lt;p&gt;A simplified request path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 │
 ▼
AWS Load Balancer
 │
 ▼
Kubernetes Service
 │
 ▼
Pod
 │
 ▼
Application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;Load Balancer&lt;/strong&gt; provides an external entry point, while the &lt;strong&gt;Kubernetes Service&lt;/strong&gt; provides a stable way to reach a group of pods.&lt;/p&gt;

&lt;p&gt;If a pod is recreated, the Service can continue routing traffic to the available pods.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. EKS and RDS
&lt;/h2&gt;

&lt;p&gt;LibraryCorner uses Amazon RDS for PostgreSQL.&lt;/p&gt;

&lt;p&gt;The application communicates with the database through the VPC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EKS Pod
   │
   │ PostgreSQL
   ▼
Amazon RDS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database does not need to be publicly accessible.&lt;/p&gt;

&lt;p&gt;Security rules can restrict database access so that only the required application traffic is allowed.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Allowed:
EKS → RDS : 5432

Not allowed:
Internet → RDS : 5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simple but important security principle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only expose what needs to be exposed.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Multi-AZ and networking
&lt;/h2&gt;

&lt;p&gt;For the production-oriented architecture of LibraryCorner, I used multiple Availability Zones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              AWS Region
                  │
        ┌─────────┴─────────┐
        │                   │
       AZ-A                AZ-B
        │                   │
    EKS nodes           EKS nodes
        │                   │
        └─────────┬─────────┘
                  │
                 RDS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multi-AZ architecture requires thinking about subnet placement, routing, IP capacity and resource availability across zones.&lt;/p&gt;

&lt;p&gt;Networking is therefore part of the architecture, not just a configuration detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. The lesson I took from this
&lt;/h2&gt;

&lt;p&gt;Before working on EKS networking, I tended to think about Kubernetes mainly in terms of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pods
Services
Deployments
Ingress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I see a larger picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS VPC
   │
   ├── Subnets
   ├── Routes
   ├── Security Groups
   ├── NAT
   │
   └── EKS
        │
        ├── Nodes
        ├── Pods
        └── Services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Kubernetes networking on AWS cannot really be understood independently from AWS networking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was one of the most useful lessons from building my EKS platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building an EKS platform is not only about deploying containers.&lt;/p&gt;

&lt;p&gt;You also need to understand the network underneath them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPCs&lt;/li&gt;
&lt;li&gt;Subnets&lt;/li&gt;
&lt;li&gt;Route tables&lt;/li&gt;
&lt;li&gt;NAT Gateways&lt;/li&gt;
&lt;li&gt;Security Groups&lt;/li&gt;
&lt;li&gt;Load Balancers&lt;/li&gt;
&lt;li&gt;Kubernetes Services&lt;/li&gt;
&lt;li&gt;Pod networking&lt;/li&gt;
&lt;li&gt;Availability Zones&lt;/li&gt;
&lt;li&gt;RDS connectivity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For me, understanding these layers was another step from &lt;strong&gt;"I can deploy an application"&lt;/strong&gt; toward &lt;strong&gt;"I understand the infrastructure that makes the application work."&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of my LibraryCorner Cloud/DevOps project, where I progressively build and document a containerized application platform on AWS.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Kubernetes Configuration Management with Kustomize: Dev vs Prod</title>
      <dc:creator>Amira Abidi</dc:creator>
      <pubDate>Tue, 15 Sep 2026 14:48:06 +0000</pubDate>
      <link>https://dev.to/amira_abidi/kubernetes-configuration-management-with-kustomize-dev-vs-prod-2bk6</link>
      <guid>https://dev.to/amira_abidi/kubernetes-configuration-management-with-kustomize-dev-vs-prod-2bk6</guid>
      <description>&lt;p&gt;Managing multiple Kubernetes environments looks simple at first.&lt;/p&gt;

&lt;p&gt;You create a few YAML manifests, deploy them to development, then adapt them for production.&lt;/p&gt;

&lt;p&gt;Then reality happens.&lt;/p&gt;

&lt;p&gt;Development and production need different replica counts, resource limits, image tags, hostnames, configuration values, and sometimes completely different infrastructure settings.&lt;/p&gt;

&lt;p&gt;Copying the manifests for each environment may work for a small project, but it quickly becomes difficult to maintain.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Kustomize&lt;/strong&gt; becomes useful.&lt;/p&gt;

&lt;p&gt;In my LibraryCorner project, I used Kustomize to manage Kubernetes configurations across separate development and production environments while keeping a shared configuration base.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Problem: Kubernetes Manifests Across Environments
&lt;/h2&gt;

&lt;p&gt;Imagine that we have a simple application deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api:latest&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This might be perfectly fine for development.&lt;/p&gt;

&lt;p&gt;But production may require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more replicas&lt;/li&gt;
&lt;li&gt;different resource requests and limits&lt;/li&gt;
&lt;li&gt;a production image tag&lt;/li&gt;
&lt;li&gt;different configuration&lt;/li&gt;
&lt;li&gt;different ingress/hostname settings&lt;/li&gt;
&lt;li&gt;stricter security settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One tempting approach is to create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment-dev.yaml
deployment-prod.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and maintain both independently.&lt;/p&gt;

&lt;p&gt;The problem is duplication.&lt;/p&gt;

&lt;p&gt;If the base Deployment changes, we now have to remember to update multiple copies.&lt;/p&gt;

&lt;p&gt;That's where Kustomize provides a cleaner approach.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. What Is Kustomize?
&lt;/h2&gt;

&lt;p&gt;Kustomize is a Kubernetes-native configuration management tool.&lt;/p&gt;

&lt;p&gt;Instead of using templates, Kustomize lets us define a common &lt;strong&gt;base configuration&lt;/strong&gt; and then apply environment-specific customizations through &lt;strong&gt;overlays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The general structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k8s/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   └── kustomization.yaml
│
└── overlays/
    ├── dev/
    │   └── kustomization.yaml
    │
    └── prod/
        └── kustomization.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important concept is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 Base
                  │
          ┌───────┴───────┐
          │               │
         Dev             Prod
       Overlay           Overlay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The base contains what is common.&lt;/p&gt;

&lt;p&gt;The overlays contain what changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Building the Base
&lt;/h2&gt;

&lt;p&gt;Let's start with the common configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;base/deployment.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;base/service.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we define the resources in &lt;code&gt;base/kustomization.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kustomize.config.k8s.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Kustomization&lt;/span&gt;

&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deployment.yaml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;service.yaml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the base describes the application without knowing whether it will run in development or production.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Creating the Development Overlay
&lt;/h2&gt;

&lt;p&gt;Now let's create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;overlays/dev/kustomization.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We reference the base:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kustomize.config.k8s.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Kustomization&lt;/span&gt;

&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;../../base&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then apply development-specific changes.&lt;/p&gt;

&lt;p&gt;For example, we might want only one replica:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also modify the container image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
    &lt;span class="na"&gt;newName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
    &lt;span class="na"&gt;newTag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting development configuration is generated from the base plus these customizations.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Creating the Production Overlay
&lt;/h2&gt;

&lt;p&gt;The production overlay follows the same principle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;overlays/prod/kustomization.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kustomize.config.k8s.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Kustomization&lt;/span&gt;

&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;../../base&lt;/span&gt;

&lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
    &lt;span class="na"&gt;newName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
    &lt;span class="na"&gt;newTag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base
 │
 ├── Dev → 1 replica + dev image
 │
 └── Prod → 3 replicas + prod image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The common Kubernetes configuration remains in one place.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Patches for More Specific Changes
&lt;/h2&gt;

&lt;p&gt;Sometimes changing replicas or images isn't enough.&lt;/p&gt;

&lt;p&gt;For example, production may require different resource limits.&lt;/p&gt;

&lt;p&gt;We can use a patch.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;overlays/prod/resources-patch.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
          &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;250m"&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;256Mi"&lt;/span&gt;
            &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500m"&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;512Mi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference the patch from the production overlay:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;resources-patch.yaml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the production-specific configuration isolated from the shared base.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Environment-Specific Configuration
&lt;/h2&gt;

&lt;p&gt;Another common requirement is changing configuration values between environments.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ENVIRONMENT=dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;versus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ENVIRONMENT=prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kustomize can generate ConfigMaps from literals or files.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;configMapGenerator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-config&lt;/span&gt;
    &lt;span class="na"&gt;literals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ENVIRONMENT=dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The production overlay can define:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;configMapGenerator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;librarycorner-config&lt;/span&gt;
    &lt;span class="na"&gt;literals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ENVIRONMENT=prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the same application manifest to be reused while keeping environment-specific configuration separate.&lt;/p&gt;

&lt;p&gt;For sensitive information, however, I would not put credentials directly into Git-managed manifests. Secrets should be handled through an appropriate secret-management solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Previewing the Result
&lt;/h2&gt;

&lt;p&gt;One of my favorite aspects of Kustomize is that we can render the final Kubernetes manifests before applying them.&lt;/p&gt;

&lt;p&gt;For development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl kustomize overlays/dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl kustomize overlays/prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it possible to inspect exactly what Kubernetes will receive.&lt;/p&gt;

&lt;p&gt;We can also apply the configuration directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-k&lt;/span&gt; overlays/dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-k&lt;/span&gt; overlays/prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-k&lt;/code&gt; option tells &lt;code&gt;kubectl&lt;/code&gt; to use Kustomize.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Kustomize in a CI/CD Pipeline
&lt;/h2&gt;

&lt;p&gt;Kustomize becomes particularly useful when combined with CI/CD.&lt;/p&gt;

&lt;p&gt;For example, a simplified deployment pipeline can look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Git push
   │
   ▼
Build application image
   │
   ▼
Run tests
   │
   ▼
Push image
   │
   ▼
Update Kubernetes configuration
   │
   ▼
Kustomize overlay
   │
   ▼
Deploy to EKS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, a pipeline could deploy development using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-k&lt;/span&gt; k8s/overlays/dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while production uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-k&lt;/span&gt; k8s/overlays/prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the Kubernetes configuration part of the application delivery process instead of maintaining deployment settings manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. How I Used It in LibraryCorner
&lt;/h2&gt;

&lt;p&gt;In LibraryCorner, I used separate Kubernetes configurations for development and production.&lt;/p&gt;

&lt;p&gt;The goal was not to create two independent Kubernetes applications.&lt;/p&gt;

&lt;p&gt;Instead, I wanted:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One common application configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;plus&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment-specific differences.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The structure was conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k8s/
├── base/
│   ├── deployment
│   ├── service
│   ├── config
│   └── kustomization.yaml
│
└── overlays/
    ├── dev/
    │   └── kustomization.yaml
    │
    └── prod/
        ├── kustomization.yaml
        └── patches/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach helped keep the environments consistent while still allowing production to have different scaling and resource requirements.&lt;/p&gt;

&lt;p&gt;It also fits naturally with the infrastructure architecture of the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS
│
├── Development
│   └── EKS
│       └── Kustomize → Dev configuration
│
└── Production
    └── EKS
        └── Kustomize → Prod configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  11. Kustomize vs Helm
&lt;/h2&gt;

&lt;p&gt;Kustomize and Helm are often compared because both can be used to manage Kubernetes configurations.&lt;/p&gt;

&lt;p&gt;The difference is largely in their approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kustomize&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;works directly with Kubernetes YAML&lt;/li&gt;
&lt;li&gt;uses bases and overlays&lt;/li&gt;
&lt;li&gt;doesn't require a templating language&lt;/li&gt;
&lt;li&gt;is integrated into &lt;code&gt;kubectl&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Helm&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uses charts and templates&lt;/li&gt;
&lt;li&gt;provides packaging and versioning&lt;/li&gt;
&lt;li&gt;has a larger ecosystem for distributing applications&lt;/li&gt;
&lt;li&gt;is particularly useful when deploying reusable third-party applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For my LibraryCorner project, Kustomize was a good fit because I wanted to maintain a relatively small number of environment-specific configurations without introducing a templating layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. What I Learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson for me was that environment separation is not only an infrastructure problem.&lt;/p&gt;

&lt;p&gt;It also exists at the Kubernetes configuration level.&lt;/p&gt;

&lt;p&gt;A clean structure such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base + Dev Overlay + Prod Overlay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;helps avoid configuration drift and duplicated manifests.&lt;/p&gt;

&lt;p&gt;It also makes the deployment process easier to reason about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Same application
      +
Different environment configuration
      =
Predictable deployments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kustomize is therefore a relatively small tool, but it solves a very practical problem when Kubernetes configurations start becoming more complex.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Kubernetes manifests are easy to write.&lt;/p&gt;

&lt;p&gt;Keeping them maintainable across multiple environments is the harder part.&lt;/p&gt;

&lt;p&gt;Kustomize provides a simple Kubernetes-native approach:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Base → Overlays → Environment-specific configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For projects running across development and production environments, this can significantly reduce duplication while keeping the differences explicit.&lt;/p&gt;

&lt;p&gt;In my case, it became an important part of the LibraryCorner deployment architecture on AWS EKS.&lt;/p&gt;

&lt;p&gt;And the next question naturally follows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we make sure these Kubernetes workloads are actually healthy and observable once they're running?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's where monitoring and observability come in — and that's a topic I've already explored with Prometheus and Grafana in LibraryCorner.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>cicd</category>
      <category>aws</category>
    </item>
    <item>
      <title>Monitoring AWS EKS with Prometheus and Grafana</title>
      <dc:creator>Amira Abidi</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:35:31 +0000</pubDate>
      <link>https://dev.to/amira_abidi/monitoring-aws-eks-with-prometheus-and-grafana-pb9</link>
      <guid>https://dev.to/amira_abidi/monitoring-aws-eks-with-prometheus-and-grafana-pb9</guid>
      <description>&lt;p&gt;In my previous articles, I shared how I moved my application from Docker to AWS EKS and how I used Terraform to build the infrastructure.&lt;/p&gt;

&lt;p&gt;Once everything was deployed and running, I had another question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do I know if my application and Kubernetes cluster are actually healthy?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Seeing pods in a &lt;code&gt;Running&lt;/code&gt; state is useful, but it doesn't tell the whole story.&lt;/p&gt;

&lt;p&gt;That's where observability came in.&lt;/p&gt;

&lt;p&gt;For my project, I implemented a monitoring stack based on &lt;strong&gt;Prometheus, Grafana, and Alertmanager&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's what I built and, more importantly, what I learned from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Monitoring Stack
&lt;/h2&gt;

&lt;p&gt;I deployed &lt;strong&gt;kube-prometheus-stack&lt;/strong&gt; on my EKS cluster using Helm.&lt;/p&gt;

&lt;p&gt;It provides several components out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt; to collect and store metrics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; to visualize them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alertmanager&lt;/strong&gt; to manage alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;node-exporter&lt;/strong&gt; for node-level metrics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;kube-state-metrics&lt;/strong&gt; for Kubernetes object metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of installing and configuring every component separately, this gave me a solid monitoring foundation for Kubernetes.&lt;/p&gt;

&lt;p&gt;The simplified flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EKS Cluster
    │
    ├── Application Pods
    ├── Kubernetes Metrics
    └── Worker Nodes
           │
           ▼
      Prometheus
           │
      ┌────┴────┐
      ▼         ▼
   Grafana   Alertmanager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prometheus collects the metrics, Grafana turns them into dashboards, and Alertmanager handles situations that require attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Am I Actually Monitoring?
&lt;/h2&gt;

&lt;p&gt;Installing Prometheus is easy.&lt;/p&gt;

&lt;p&gt;Knowing &lt;strong&gt;what is worth monitoring&lt;/strong&gt; is more interesting.&lt;/p&gt;

&lt;p&gt;For my EKS environment, I wanted visibility at several levels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cluster and nodes
&lt;/h3&gt;

&lt;p&gt;I can monitor things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU usage&lt;/li&gt;
&lt;li&gt;Memory consumption&lt;/li&gt;
&lt;li&gt;Node availability&lt;/li&gt;
&lt;li&gt;Kubernetes resource usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps answer questions like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is the cluster running normally, or are resources becoming saturated?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Kubernetes workloads
&lt;/h3&gt;

&lt;p&gt;At the Kubernetes level, I can observe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of running pods&lt;/li&gt;
&lt;li&gt;Pod restarts&lt;/li&gt;
&lt;li&gt;Deployment status&lt;/li&gt;
&lt;li&gt;Resource consumption per pod&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is particularly useful when troubleshooting.&lt;/p&gt;

&lt;p&gt;A service may still be reachable while one of its pods is constantly restarting in the background.&lt;/p&gt;

&lt;p&gt;Without monitoring, that kind of behavior can easily go unnoticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grafana: Turning Metrics into Something Useful
&lt;/h2&gt;

&lt;p&gt;Prometheus collecting thousands of metrics isn't very useful if I can't easily interpret them.&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;Grafana&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;I use Grafana dashboards to get a visual overview of the cluster and quickly identify unusual behavior.&lt;/p&gt;

&lt;p&gt;For example, instead of repeatedly running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl top pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can observe CPU and memory usage over time.&lt;/p&gt;

&lt;p&gt;That &lt;strong&gt;“over time”&lt;/strong&gt; part is important.&lt;/p&gt;

&lt;p&gt;A command tells me what is happening now.&lt;/p&gt;

&lt;p&gt;Monitoring helps me understand &lt;strong&gt;what happened before the problem appeared&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessing Grafana
&lt;/h2&gt;

&lt;p&gt;I also wanted Grafana to be accessible without relying on local port forwarding.&lt;/p&gt;

&lt;p&gt;I exposed it through an &lt;strong&gt;AWS Application Load Balancer&lt;/strong&gt;, using a dedicated subdomain and its own &lt;strong&gt;ACM certificate for HTTPS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
   │
   ▼
Grafana subdomain
   │
   ▼
AWS ALB + HTTPS
   │
   ▼
Grafana on EKS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This made the monitoring interface accessible in a way that was closer to how I would expose an internal platform service in a real environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring vs Alerting
&lt;/h2&gt;

&lt;p&gt;Dashboards are useful when someone is looking at them.&lt;/p&gt;

&lt;p&gt;But nobody should have to watch Grafana all day.&lt;/p&gt;

&lt;p&gt;That's why the stack also includes &lt;strong&gt;Alertmanager&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Prometheus can evaluate alerting rules based on collected metrics. When a condition is triggered, Alertmanager is responsible for processing the alert and routing the notification.&lt;/p&gt;

&lt;p&gt;This introduced an important distinction for me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring tells me what's happening.&lt;br&gt;
Alerting tells me when I need to care.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A good monitoring system shouldn't generate noise for every small variation. The challenge is deciding which conditions actually require action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability Changed How I Look at Kubernetes
&lt;/h2&gt;

&lt;p&gt;One of the biggest lessons from this project was that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Running is not the same as healthy.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kubernetes can tell me that a pod exists.&lt;/p&gt;

&lt;p&gt;Prometheus can tell me that its memory consumption has been increasing for the last two hours.&lt;/p&gt;

&lt;p&gt;Grafana can make that trend immediately visible.&lt;/p&gt;

&lt;p&gt;Alertmanager can tell me when that trend crosses a threshold that requires attention.&lt;/p&gt;

&lt;p&gt;These tools complement Kubernetes rather than replace its built-in health mechanisms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Before implementing this stack, I mostly thought about monitoring as dashboards.&lt;/p&gt;

&lt;p&gt;Now I see observability as part of the architecture itself.&lt;/p&gt;

&lt;p&gt;When deploying an application, I should be able to answer questions like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it available?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it performing normally?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are resources becoming saturated?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did something change before the incident happened?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And most importantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will I know there's a problem before a user tells me?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building the monitoring stack made the EKS environment feel much less like a collection of running resources and much more like a system I could actually operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Monitoring EKS also forced me to understand more about what happens underneath Kubernetes.&lt;/p&gt;

&lt;p&gt;How does traffic reach my application?&lt;/p&gt;

&lt;p&gt;Why are my worker nodes in private subnets?&lt;/p&gt;

&lt;p&gt;Why do they need NAT Gateways?&lt;/p&gt;

&lt;p&gt;And where exactly does the Application Load Balancer fit into all of this?&lt;/p&gt;

&lt;p&gt;That's what I'll cover next:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding EKS Networking: ALB, Private Subnets &amp;amp; NAT Gateway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're running Kubernetes, what are the first metrics or alerts you usually configure?&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>aws</category>
      <category>observability</category>
      <category>devops</category>
    </item>
    <item>
      <title>How I Built My AWS EKS Infrastructure with Terraform</title>
      <dc:creator>Amira Abidi</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:55:00 +0000</pubDate>
      <link>https://dev.to/amira_abidi/how-i-built-my-aws-eks-infrastructure-with-terraform-23ai</link>
      <guid>https://dev.to/amira_abidi/how-i-built-my-aws-eks-infrastructure-with-terraform-23ai</guid>
      <description>&lt;p&gt;In my previous article, I shared how I moved my application from a local Docker environment to AWS EKS.&lt;/p&gt;

&lt;p&gt;This time, I want to focus on one of the most important parts of the project: &lt;strong&gt;Infrastructure as Code with Terraform&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My goal was simple: I didn't want my AWS infrastructure to depend on resources created manually from the AWS Console.&lt;/p&gt;

&lt;p&gt;I wanted it to be &lt;strong&gt;reproducible, structured, and easier to maintain&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Terraform?
&lt;/h2&gt;

&lt;p&gt;My architecture includes several AWS components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC and subnets&lt;/li&gt;
&lt;li&gt;Internet and NAT Gateways&lt;/li&gt;
&lt;li&gt;EKS&lt;/li&gt;
&lt;li&gt;RDS PostgreSQL&lt;/li&gt;
&lt;li&gt;Application Load Balancer&lt;/li&gt;
&lt;li&gt;Route 53&lt;/li&gt;
&lt;li&gt;ACM certificates&lt;/li&gt;
&lt;li&gt;IAM roles and policies&lt;/li&gt;
&lt;li&gt;ECR repositories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Creating everything manually would work, but reproducing the same architecture consistently would quickly become difficult.&lt;/p&gt;

&lt;p&gt;That's where Terraform came in.&lt;/p&gt;

&lt;p&gt;Instead of describing the infrastructure in documentation, I could describe it directly as code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structuring the Infrastructure
&lt;/h2&gt;

&lt;p&gt;One thing I quickly learned is that putting every resource into a single &lt;code&gt;main.tf&lt;/code&gt; file doesn't scale very well.&lt;/p&gt;

&lt;p&gt;So I organized the infrastructure into reusable &lt;strong&gt;Terraform modules&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The structure looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform/
├── modules/
│   ├── vpc/
│   ├── eks/
│   ├── rds/
│   ├── ecr/
│   └── security/
│
└── environments/
    ├── dev/
    └── prod/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each module has a specific responsibility.&lt;/p&gt;

&lt;p&gt;The VPC module manages networking, the EKS module manages the Kubernetes infrastructure, and the RDS module manages the PostgreSQL database.&lt;/p&gt;

&lt;p&gt;This made the code much easier to understand and maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking First
&lt;/h2&gt;

&lt;p&gt;Before deploying EKS, I needed the network.&lt;/p&gt;

&lt;p&gt;I created a VPC distributed across &lt;strong&gt;three Availability Zones&lt;/strong&gt;, with public and private subnets.&lt;/p&gt;

&lt;p&gt;The public layer hosts internet-facing components such as the Application Load Balancer and NAT Gateways.&lt;/p&gt;

&lt;p&gt;The EKS worker nodes run in &lt;strong&gt;private subnets&lt;/strong&gt;, which means they aren't directly exposed to the internet.&lt;/p&gt;

&lt;p&gt;The simplified architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
   │
   ▼
Application Load Balancer
   │
   ▼
Private Subnets
   │
   ├── EKS Worker Nodes
   │
   └── RDS PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was an important lesson for me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using Kubernetes doesn't remove the need to understand networking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You still need to think about routing, security groups, public/private subnets, DNS, and internet access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the EKS Cluster
&lt;/h2&gt;

&lt;p&gt;Once the network was ready, Terraform could create the EKS cluster and its node groups.&lt;/p&gt;

&lt;p&gt;The worker nodes are distributed across the three Availability Zones.&lt;/p&gt;

&lt;p&gt;From there, Kubernetes manages the application workloads.&lt;/p&gt;

&lt;p&gt;My application itself isn't defined directly in Terraform. Kubernetes resources are managed separately, allowing Terraform to focus primarily on the AWS infrastructure.&lt;/p&gt;

&lt;p&gt;I found this separation useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform manages the infrastructure. Kubernetes manages the workloads.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Multiple Environments
&lt;/h2&gt;

&lt;p&gt;Another requirement was separating &lt;strong&gt;development and production&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of maintaining two completely different Terraform codebases, both environments reuse the same modules with different configurations.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              ┌── dev
Terraform ────┤
              └── prod
                  │
                  ▼
             Shared Modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means I can change the infrastructure logic once while keeping environment-specific values separate.&lt;/p&gt;

&lt;p&gt;Each environment has its own EKS cluster and RDS database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote Terraform State
&lt;/h2&gt;

&lt;p&gt;Terraform state is critical because it maps the configuration to the resources that actually exist in AWS.&lt;/p&gt;

&lt;p&gt;Keeping it only on my laptop wasn't something I wanted to rely on.&lt;/p&gt;

&lt;p&gt;So I configured a &lt;strong&gt;remote backend using Amazon S3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This makes the infrastructure state persistent and independent from my local machine.&lt;/p&gt;

&lt;p&gt;It also reinforced an important point:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform state is part of the infrastructure and needs to be protected accordingly.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Before this project, Terraform looked mostly like a way to automate AWS resource creation.&lt;/p&gt;

&lt;p&gt;After building a complete environment with it, I see it differently.&lt;/p&gt;

&lt;p&gt;The real value isn't just:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I can create an EKS cluster with Terraform.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's being able to describe an architecture in a way that is &lt;strong&gt;repeatable, reviewable, and maintainable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I also learned that the order of thinking matters.&lt;/p&gt;

&lt;p&gt;Before writing Terraform code, I needed to understand the architecture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;networking → security → compute → database → application → observability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Terraform doesn't design the architecture for you.&lt;/p&gt;

&lt;p&gt;It forces you to express the architecture you've designed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Infrastructure is now reproducible and the application is running on Kubernetes.&lt;/p&gt;

&lt;p&gt;But deploying a service isn't enough.&lt;/p&gt;

&lt;p&gt;The next question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do I know if my Kubernetes cluster and application are actually healthy?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the next article, I'll look at how I added &lt;strong&gt;Prometheus and Grafana monitoring to AWS EKS&lt;/strong&gt; and what I learned about observability along the way.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>kubernetes</category>
      <category>devops</category>
    </item>
    <item>
      <title>From a Local Docker App to AWS EKS: My DevOps Project</title>
      <dc:creator>Amira Abidi</dc:creator>
      <pubDate>Sat, 22 Aug 2026 10:36:43 +0000</pubDate>
      <link>https://dev.to/amira_abidi/from-a-local-docker-app-to-aws-eks-my-devops-project-3om5</link>
      <guid>https://dev.to/amira_abidi/from-a-local-docker-app-to-aws-eks-my-devops-project-3om5</guid>
      <description>&lt;p&gt;I recently worked on a DevOps project with a simple starting point: an application that worked perfectly fine on my local machine.&lt;/p&gt;

&lt;p&gt;The application, &lt;strong&gt;LibraryCorner&lt;/strong&gt;, consists of a frontend, a FastAPI backend, and a PostgreSQL database. It was already containerized with Docker, so technically, everything worked.&lt;/p&gt;

&lt;p&gt;But “it works on my machine” is very different from running a service in a production-like environment.&lt;/p&gt;

&lt;p&gt;So I gave myself a challenge:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How can I transform a local application into a service that is deployable, highly available, observable, and recoverable?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These four properties became the foundation of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;I deployed the application on AWS using &lt;strong&gt;EKS&lt;/strong&gt; for container orchestration and &lt;strong&gt;RDS PostgreSQL&lt;/strong&gt; for the database.&lt;/p&gt;

&lt;p&gt;The request flow looks roughly like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User → Route 53 → Application Load Balancer → EKS → RDS PostgreSQL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The EKS nodes and the database run inside private subnets, while the Application Load Balancer acts as the public entry point.&lt;/p&gt;

&lt;p&gt;Around this core architecture, I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; for Infrastructure as Code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab CI/CD&lt;/strong&gt; for automation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECR&lt;/strong&gt; for Docker images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Secrets Manager&lt;/strong&gt; for secrets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus &amp;amp; Grafana&lt;/strong&gt; for monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch&lt;/strong&gt; for AWS monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt; for Terraform state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also separated the &lt;strong&gt;development and production environments&lt;/strong&gt;, each with its own infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Deployable
&lt;/h2&gt;

&lt;p&gt;My first goal was to avoid manual infrastructure changes as much as possible.&lt;/p&gt;

&lt;p&gt;The AWS infrastructure is managed with &lt;strong&gt;Terraform&lt;/strong&gt;, while the application deployment is handled through &lt;strong&gt;GitLab CI/CD&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The pipeline builds the Docker images, pushes them to ECR, and deploys the application to Kubernetes.&lt;/p&gt;

&lt;p&gt;For AWS authentication, I used &lt;strong&gt;OIDC instead of storing long-lived AWS access keys in GitLab&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I actually wrote a separate article about this because I found the authentication flow particularly interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Highly Available
&lt;/h2&gt;

&lt;p&gt;Deploying something to Kubernetes doesn't automatically make it highly available.&lt;/p&gt;

&lt;p&gt;I configured the EKS infrastructure across multiple Availability Zones and deployed multiple application replicas.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;Application Load Balancer&lt;/strong&gt; distributes incoming traffic, while Kubernetes can replace unhealthy pods.&lt;/p&gt;

&lt;p&gt;I also configured a &lt;strong&gt;Horizontal Pod Autoscaler (HPA)&lt;/strong&gt; to scale the application between &lt;strong&gt;2 and 6 pods&lt;/strong&gt; depending on resource usage.&lt;/p&gt;

&lt;p&gt;For the production database, RDS uses a &lt;strong&gt;Multi-AZ configuration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple: the application shouldn't depend on a single instance, pod, or Availability Zone.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Observable
&lt;/h2&gt;

&lt;p&gt;A service can be running and still be unhealthy.&lt;/p&gt;

&lt;p&gt;That's why monitoring was another important part of the project.&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;Prometheus&lt;/strong&gt; to collect Kubernetes and application metrics and &lt;strong&gt;Grafana&lt;/strong&gt; to visualize them.&lt;/p&gt;

&lt;p&gt;This gave me visibility into things like resource usage, pod health, and application behavior.&lt;/p&gt;

&lt;p&gt;I also configured alerts so that important problems don't require someone to constantly watch a dashboard.&lt;/p&gt;

&lt;p&gt;This part of the project changed the way I think about infrastructure: deploying an application is only the beginning. You also need to understand what happens &lt;strong&gt;after deployment&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Recoverable
&lt;/h2&gt;

&lt;p&gt;High availability doesn't replace backups.&lt;/p&gt;

&lt;p&gt;I wanted to know what would happen if the database actually had to be restored.&lt;/p&gt;

&lt;p&gt;So instead of simply enabling RDS snapshots and assuming recovery would work, I performed a real restoration test.&lt;/p&gt;

&lt;p&gt;The measured recovery time was approximately &lt;strong&gt;14 minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For me, this was one of the most useful exercises in the project because it turned disaster recovery from a configuration checkbox into something measurable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson wasn't about a specific AWS service.&lt;/p&gt;

&lt;p&gt;It was understanding how all these pieces work together.&lt;/p&gt;

&lt;p&gt;Terraform creates the infrastructure. GitLab automates delivery. Kubernetes manages the workloads. AWS provides the underlying services. Prometheus and Grafana tell me what's happening. Backups give me a recovery path when things go wrong.&lt;/p&gt;

&lt;p&gt;And each layer introduces its own decisions and trade-offs.&lt;/p&gt;

&lt;p&gt;This project also reminded me of something from my QA background:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A system isn't reliable because we expect it to work. It's reliable because we can verify what happens when it doesn't.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This article was an overview of the architecture, but several parts deserve their own deep dive.&lt;/p&gt;

&lt;p&gt;Next, I'll share how I built the &lt;strong&gt;AWS EKS infrastructure with Terraform&lt;/strong&gt;, including some of the problems I encountered and what I learned while fixing them.&lt;/p&gt;

&lt;p&gt;If you're also learning AWS, Kubernetes, or DevOps by building projects, I'd love to hear about what you're working on.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>kubernetes</category>
      <category>terraform</category>
    </item>
    <item>
      <title>How I Removed AWS Access Keys from GitLab CI/CD with OIDC</title>
      <dc:creator>Amira Abidi</dc:creator>
      <pubDate>Wed, 12 Aug 2026 09:05:09 +0000</pubDate>
      <link>https://dev.to/amira_abidi/how-i-removed-aws-access-keys-from-gitlab-cicd-with-oidc-4fn6</link>
      <guid>https://dev.to/amira_abidi/how-i-removed-aws-access-keys-from-gitlab-cicd-with-oidc-4fn6</guid>
      <description>&lt;p&gt;When I first connected my GitLab CI/CD pipelines to AWS, I used the simplest solution: an IAM user with an Access Key and Secret Access Key stored as GitLab CI/CD variables.&lt;/p&gt;

&lt;p&gt;It worked.&lt;/p&gt;

&lt;p&gt;But there was one problem: &lt;strong&gt;those credentials were permanent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They had to be stored, protected and eventually rotated. If they were accidentally exposed in logs or compromised, they could remain valid until manually revoked.&lt;/p&gt;

&lt;p&gt;I wanted a cleaner solution.&lt;/p&gt;

&lt;p&gt;So I replaced permanent AWS credentials with &lt;strong&gt;OIDC federation between GitLab and AWS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The result is simple: GitLab pipelines can access AWS without storing any permanent AWS credentials.&lt;/p&gt;

&lt;p&gt;In this post, I'll explain how I implemented it, how the authentication flow works, and one important issue I faced when using it with EKS and Terraform.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The authentication flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────┐
│  GitLab CI   │
└──────┬───────┘
       │
       │ OIDC token
       ▼
┌──────────────┐
│   AWS STS    │
└──────┬───────┘
       │
       │ Temporary credentials
       ▼
┌──────────────┐
│   IAM Role   │
└──────┬───────┘
       │
       ├──────────► Terraform
       │
       ├──────────► ECR
       │
       └──────────► EKS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of GitLab storing an AWS Access Key, it proves its identity to AWS using a short-lived OIDC token.&lt;/p&gt;

&lt;p&gt;AWS verifies the token and returns temporary credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  How does OIDC authentication work?
&lt;/h2&gt;

&lt;p&gt;The process can be summarized in five steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GitLab creates an OIDC token for the CI/CD job.&lt;/li&gt;
&lt;li&gt;The pipeline sends this token to AWS.&lt;/li&gt;
&lt;li&gt;AWS verifies that the token really comes from GitLab.&lt;/li&gt;
&lt;li&gt;AWS checks that the project is allowed to assume the requested IAM role.&lt;/li&gt;
&lt;li&gt;AWS STS returns temporary credentials.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These credentials expire automatically.&lt;/p&gt;

&lt;p&gt;So there is nothing permanent to store or rotate inside GitLab.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Register GitLab as an OIDC provider
&lt;/h2&gt;

&lt;p&gt;AWS first needs to trust GitLab as an identity provider.&lt;/p&gt;

&lt;p&gt;I configured the OIDC provider using Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"tls_certificate"&lt;/span&gt; &lt;span class="s2"&gt;"gitlab"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.gitlab_url}/.well-known/openid-configuration"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_openid_connect_provider"&lt;/span&gt; &lt;span class="s2"&gt;"gitlab"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gitlab_url&lt;/span&gt;
  &lt;span class="nx"&gt;client_id_list&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gitlab_url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;thumbprint_list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tls_certificate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gitlab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;certificates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;sha1_fingerprint&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells AWS that tokens issued by GitLab can be used for authentication.&lt;/p&gt;

&lt;p&gt;But trusting GitLab itself is not enough.&lt;/p&gt;

&lt;p&gt;AWS also needs to know &lt;strong&gt;which GitLab projects are allowed to access which IAM roles.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Restrict access with the IAM Trust Policy
&lt;/h2&gt;

&lt;p&gt;This is one of the most important parts of the configuration.&lt;/p&gt;

&lt;p&gt;We don't want any GitLab project to be able to assume our AWS roles.&lt;/p&gt;

&lt;p&gt;The IAM Trust Policy checks information contained in the GitLab token.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
  &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gitlab.com:aud"&lt;/span&gt;
  &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gitlab_url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringLike"&lt;/span&gt;
  &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gitlab.com:sub"&lt;/span&gt;
  &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"project_path:${each.value}:ref_type:branch:ref:*"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
  &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gitlab.com:namespace_id"&lt;/span&gt;
  &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gitlab_namespace_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important values here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aud&lt;/code&gt;: checks the expected audience.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sub&lt;/code&gt;: identifies the GitLab project and branch.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;namespace_id&lt;/code&gt;: identifies the GitLab namespace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I especially like using &lt;code&gt;namespace_id&lt;/code&gt; because it is a stable numeric identifier.&lt;/p&gt;

&lt;p&gt;GitLab project and group names can change, but the namespace ID provides an additional way to make sure the token comes from the expected namespace.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — One IAM role per repository
&lt;/h2&gt;

&lt;p&gt;My project uses four GitLab repositories, so I created a separate IAM role for each one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;Main AWS permissions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Push Docker images to ECR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Push Docker images to ECR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;Manage AWS infrastructure with Terraform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;Check ECR images and deploy to EKS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why separate them?&lt;/p&gt;

&lt;p&gt;Because the backend pipeline doesn't need permission to modify the EKS cluster.&lt;/p&gt;

&lt;p&gt;And the deployment pipeline doesn't need permission to push Docker images.&lt;/p&gt;

&lt;p&gt;Each pipeline gets only the permissions required for its job.&lt;/p&gt;

&lt;p&gt;For example, the deployment role only needs to check whether an image exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s2"&gt;"ecr:DescribeImages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"ecr:DescribeRepositories"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It cannot push a new image.&lt;/p&gt;

&lt;p&gt;This gives me a much cleaner separation of responsibilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — Request the OIDC token from GitLab
&lt;/h2&gt;

&lt;p&gt;On the GitLab side, the configuration is surprisingly small.&lt;/p&gt;

&lt;p&gt;GitLab can generate the OIDC token directly inside the CI/CD job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;id_tokens&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GITLAB_OIDC_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;aud&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://gitlab.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I define the AWS role and the location of the token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AWS_REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1"&lt;/span&gt;
  &lt;span class="na"&gt;AWS_ROLE_ARN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::123456789012:role/gitlab-ci-book-infra"&lt;/span&gt;
  &lt;span class="na"&gt;AWS_WEB_IDENTITY_TOKEN_FILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${CI_PROJECT_DIR}/.gitlab-oidc-token"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And write the token to the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;before_script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "$GITLAB_OIDC_TOKEN" &amp;gt; "$AWS_WEB_IDENTITY_TOKEN_FILE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's basically it.&lt;/p&gt;

&lt;p&gt;Terraform and AWS SDKs understand the web identity authentication flow automatically.&lt;/p&gt;

&lt;p&gt;There is no need to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And more importantly, I don't need to store permanent values such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in GitLab CI/CD variables anymore.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5 — EKS needs an extra configuration
&lt;/h2&gt;

&lt;p&gt;This was an important thing I learned while implementing the solution.&lt;/p&gt;

&lt;p&gt;Successfully authenticating with AWS does &lt;strong&gt;not&lt;/strong&gt; automatically mean that the pipeline can access Kubernetes.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws eks update-kubeconfig
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may work correctly while:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because there are two different authorization levels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS IAM
   │
   │ Can this role access the AWS/EKS API?
   ▼
EKS Cluster
   │
   │ Is this IAM role allowed inside Kubernetes?
   ▼
Kubernetes resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IAM role therefore also needs access to the EKS cluster.&lt;/p&gt;

&lt;p&gt;I configured this with an EKS access entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_eks_access_entry"&lt;/span&gt; &lt;span class="s2"&gt;"ci_deploy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_name&lt;/span&gt;
  &lt;span class="nx"&gt;principal_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ci_deploy_role_arn&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"STANDARD"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And associated the required EKS access policy with it.&lt;/p&gt;

&lt;p&gt;Once this was configured, the deployment pipeline could authenticate correctly to the cluster.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Terraform + EKS issue I ran into
&lt;/h2&gt;

&lt;p&gt;This was probably the most interesting problem I encountered.&lt;/p&gt;

&lt;p&gt;EKS authentication tokens are short-lived.&lt;/p&gt;

&lt;p&gt;Initially, I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_eks_cluster_auth"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works when &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt; happen immediately.&lt;/p&gt;

&lt;p&gt;But my pipeline separates them.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform plan
      │
      ▼
Manual approval
      │
      ▼
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is that the EKS token can be generated during &lt;code&gt;terraform plan&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If I wait before approving the &lt;code&gt;apply&lt;/code&gt;, the saved token may already be expired.&lt;/p&gt;

&lt;p&gt;Terraform then fails when trying to communicate with Kubernetes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The solution: generate the token when it is needed
&lt;/h2&gt;

&lt;p&gt;Instead of storing the token during the plan, I use an &lt;code&gt;exec&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"kubernetes"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_endpoint&lt;/span&gt;

  &lt;span class="nx"&gt;cluster_ca_certificate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_certificate_authority_data&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;exec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;api_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"client.authentication.k8s.io/v1beta1"&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws-iam-authenticator"&lt;/span&gt;
    &lt;span class="nx"&gt;args&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_name&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the token is generated when Terraform actually needs to connect to Kubernetes.&lt;/p&gt;

&lt;p&gt;So even if I wait before approving &lt;code&gt;terraform apply&lt;/code&gt;, Terraform gets a fresh token.&lt;/p&gt;




&lt;h2&gt;
  
  
  Another small GitLab CI lesson
&lt;/h2&gt;

&lt;p&gt;Manual GitLab jobs can allow failures by default depending on how they are configured.&lt;/p&gt;

&lt;p&gt;For an infrastructure deployment, that's something I definitely don't want.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;terraform apply&lt;/code&gt; fails, the pipeline should fail too.&lt;/p&gt;

&lt;p&gt;So I explicitly use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH'&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a small configuration detail, but an important one for infrastructure pipelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debugging OIDC authentication
&lt;/h2&gt;

&lt;p&gt;When authentication fails, I usually check three things first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aud
sub
namespace_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical errors include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;What I check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; denied&lt;/td&gt;
&lt;td&gt;Project path / &lt;code&gt;sub&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Incorrect token audience&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;aud&lt;/code&gt; configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;InvalidIdentityToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Token expiration / OIDC provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Unauthorized&lt;/code&gt; from &lt;code&gt;kubectl&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;EKS access entry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I also keep this command in the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's very useful because it immediately shows which AWS identity the pipeline is actually using.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from this implementation is that OIDC is not as complicated as it first looks.&lt;/p&gt;

&lt;p&gt;The basic idea is simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitLab proves its identity
        ↓
AWS verifies the GitLab project
        ↓
AWS STS provides temporary credentials
        ↓
The pipeline accesses AWS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main advantages for me are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No permanent AWS credentials in GitLab.&lt;/li&gt;
&lt;li&gt;No Access Keys to rotate.&lt;/li&gt;
&lt;li&gt;Temporary credentials expire automatically.&lt;/li&gt;
&lt;li&gt;Each repository has its own IAM role.&lt;/li&gt;
&lt;li&gt;Permissions can be limited per pipeline.&lt;/li&gt;
&lt;li&gt;AWS activity can be traced through CloudTrail.&lt;/li&gt;
&lt;li&gt;GitLab can securely deploy to EKS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are a few extra details when EKS is involved, especially around cluster access and short-lived Kubernetes tokens, but once those pieces are understood, the overall architecture is quite simple.&lt;/p&gt;

&lt;p&gt;For CI/CD pipelines, I now prefer this approach over storing permanent AWS credentials whenever OIDC federation is available.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>gitlab</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
