<?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>Kubernetes HPA on Amazon EKS: Scaling from 2 to 6 Pods Under Load</title>
      <dc:creator>Amira Abidi</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:53:32 +0000</pubDate>
      <link>https://dev.to/amira_abidi/kubernetes-hpa-on-amazon-eks-scaling-from-2-to-6-pods-under-load-49k0</link>
      <guid>https://dev.to/amira_abidi/kubernetes-hpa-on-amazon-eks-scaling-from-2-to-6-pods-under-load-49k0</guid>
      <description>&lt;p&gt;What happens when your application receives more traffic than its current capacity can handle?&lt;/p&gt;

&lt;p&gt;While building &lt;strong&gt;LibraryCorner&lt;/strong&gt;, my AWS Cloud/DevOps project, I tested Kubernetes Horizontal Pod Autoscaler (HPA) on Amazon EKS to see how the application would respond to increasing CPU utilization.&lt;/p&gt;

&lt;p&gt;The result was more interesting than simply watching pods appear: the test demonstrated automatic scaling, observable performance changes, and a clearly defined capacity limit.&lt;/p&gt;

&lt;p&gt;Here's what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The setup
&lt;/h2&gt;

&lt;p&gt;I configured Kubernetes HPA to scale the application between &lt;strong&gt;2 and 6 pods&lt;/strong&gt;, targeting 55% average CPU utilization.&lt;/p&gt;

&lt;p&gt;The test followed the complete lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with two pods at rest.&lt;/li&gt;
&lt;li&gt;Gradually increase the load.&lt;/li&gt;
&lt;li&gt;Observe CPU utilization and application latency.&lt;/li&gt;
&lt;li&gt;Watch HPA scale out.&lt;/li&gt;
&lt;li&gt;Reach the configured capacity limit.&lt;/li&gt;
&lt;li&gt;Stop the load and observe recovery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The objective wasn't just to increase the replica count. It was to verify that the system reacted to real metrics and returned to its initial state afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. From 2 pods to 6
&lt;/h2&gt;

&lt;p&gt;At rest, the application ran with two pods and low resource utilization.&lt;/p&gt;

&lt;p&gt;As the load increased, CPU utilization rose significantly. At the peak, it reached &lt;strong&gt;216%&lt;/strong&gt;, and HPA increased the number of replicas to four, then six.&lt;/p&gt;

&lt;p&gt;The scaling process looked 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;       Increasing traffic
               |
               v
       CPU utilization rises
               |
               v
       Kubernetes HPA reacts
               |
               v
          2 → 4 → 6 pods
               |
               v
       More application capacity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HPA uses observed metrics to calculate the desired replica count. Kubernetes then schedules and starts additional pods, provided the cluster has enough capacity.&lt;/p&gt;

&lt;p&gt;The important detail is that scaling isn't instantaneous. New pods need time to start and become ready, so application latency can temporarily increase while demand rises.&lt;/p&gt;

&lt;p&gt;That was visible in my test: latency degraded around the traffic peak, then returned to normal as additional pods became available.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The results: 15,000+ requests, zero failures
&lt;/h2&gt;

&lt;p&gt;The load test produced encouraging results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;15,000+ requests&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero failed requests&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;CPU utilization reached &lt;strong&gt;216%&lt;/strong&gt; at the peak&lt;/li&gt;
&lt;li&gt;Pod count increased from &lt;strong&gt;2 to 6&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Latency remained below the configured alert threshold at the 95th percentile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dashboard captured the entire sequence: traffic increased, latency rose, additional pods started, and performance recovered as the system scaled.&lt;/p&gt;

&lt;p&gt;This is why I consider observability essential to autoscaling. The dashboard helped connect the HPA decisions to actual application behavior rather than just showing a changing replica count.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Why stop at six pods?
&lt;/h2&gt;

&lt;p&gt;Six wasn't an arbitrary number.&lt;/p&gt;

&lt;p&gt;At the time of the experiment, the cluster's node networking capacity limited the number of pods that could be scheduled. Increasing the HPA maximum beyond the available capacity could have left new pods pending.&lt;/p&gt;

&lt;p&gt;I therefore configured the maximum at six and added an alert to signal when the workload reached that limit.&lt;/p&gt;

&lt;p&gt;At peak load, the alert fired and a notification was emitted.&lt;/p&gt;

&lt;p&gt;One important distinction: &lt;strong&gt;the alert did not trigger scaling&lt;/strong&gt;. CPU metrics drove HPA. The alert simply indicated that the configured maximum had been reached and that additional capacity might be needed.&lt;/p&gt;

&lt;p&gt;This is a useful operational signal: autoscaling can work exactly as configured and still reach a point where it cannot scale further.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Scaling back down
&lt;/h2&gt;

&lt;p&gt;When the load stopped, the alert resolved automatically, and HPA returned the workload to two pods after the stabilization window.&lt;/p&gt;

&lt;p&gt;That final step matters.&lt;/p&gt;

&lt;p&gt;Autoscaling isn't just about adding replicas under pressure. It also needs to remove unnecessary replicas without reacting too aggressively to short-lived changes.&lt;/p&gt;

&lt;p&gt;The complete lifecycle was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rest → Load → Scale out → Capacity alert → Recovery → Scale down&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This validated three aspects of the project together: elasticity, observability, and automatic reactions based on metrics.&lt;/p&gt;

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

&lt;p&gt;This experiment reinforced three lessons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Autoscaling depends on capacity.&lt;/strong&gt; HPA can request more pods, but the cluster must have enough networking and compute resources to run them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Monitoring makes behavior explainable.&lt;/strong&gt; CPU, replica count, latency and alerts helped me understand what happened throughout the test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Scaling needs end-to-end validation.&lt;/strong&gt; Reaching six pods is not enough. You also need to check application errors, latency, recovery, and what happens when the maximum is reached.&lt;/p&gt;

&lt;p&gt;Coming from QA and validation, this last point feels natural to me: I want to verify the complete behavior, not just whether a Kubernetes resource has the expected status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;My LibraryCorner load test demonstrated that HPA could scale the application from two to six pods, handle more than 15,000 requests without failures, and return to two pods after the load ended.&lt;/p&gt;

&lt;p&gt;It also exposed an important constraint: application autoscaling cannot compensate indefinitely for limited cluster capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Autoscaling isn't simply about adding pods. It's about responding to demand, observing the result, and knowing when you've reached your limits.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>kubernetes</category>
      <category>performance</category>
    </item>
    <item>
      <title>Disaster Recovery: Restoring PostgreSQL on Amazon RDS</title>
      <dc:creator>Amira Abidi</dc:creator>
      <pubDate>Tue, 06 Oct 2026 14:53:50 +0000</pubDate>
      <link>https://dev.to/amira_abidi/disaster-recovery-restoring-postgresql-on-amazon-rds-33pi</link>
      <guid>https://dev.to/amira_abidi/disaster-recovery-restoring-postgresql-on-amazon-rds-33pi</guid>
      <description>&lt;p&gt;A backup is easy to create.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can you actually recover your application when you need to?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While building &lt;strong&gt;LibraryCorner&lt;/strong&gt;, my AWS project running on Amazon EKS with PostgreSQL on Amazon RDS, I wanted to look beyond simply deploying a database and enabling backups.&lt;/p&gt;

&lt;p&gt;I wanted to understand what happens when things go wrong.&lt;/p&gt;

&lt;p&gt;Because having a backup is not the same as having a recovery strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backup is only the starting point
&lt;/h2&gt;

&lt;p&gt;For a database-backed application, disaster recovery is more than storing a copy of your data.&lt;/p&gt;

&lt;p&gt;The real recovery flow looks more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failure
   ↓
Choose recovery point
   ↓
Restore PostgreSQL
   ↓
Reconnect application
   ↓
Validate data
   ↓
Validate application
   ↓
Return to service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every step matters.&lt;/p&gt;

&lt;p&gt;A restored database that the application cannot reach is not a successful recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  My LibraryCorner architecture
&lt;/h2&gt;

&lt;p&gt;In LibraryCorner, the application runs on &lt;strong&gt;Amazon EKS&lt;/strong&gt;, while PostgreSQL is hosted on &lt;strong&gt;Amazon RDS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At a high level:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The production database uses a multi-AZ configuration to improve availability.&lt;/p&gt;

&lt;p&gt;But this led me to an important distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;High availability is not the same as disaster recovery.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Multi-AZ helps protect against certain infrastructure failures.&lt;/p&gt;

&lt;p&gt;Backups and snapshots provide another layer of protection when recovery is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would recovery look like?
&lt;/h2&gt;

&lt;p&gt;Imagine that the database needs to be replaced.&lt;/p&gt;

&lt;p&gt;The first question isn't simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can I restore it?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Which recovery point should I restore?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where two concepts become important.&lt;/p&gt;

&lt;h3&gt;
  
  
  RPO — Recovery Point Objective
&lt;/h3&gt;

&lt;p&gt;How much data can I afford to lose?&lt;/p&gt;

&lt;p&gt;A system that can tolerate an hour of data loss has very different requirements from one that needs almost zero data loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  RTO — Recovery Time Objective
&lt;/h3&gt;

&lt;p&gt;How long can the application remain unavailable?&lt;/p&gt;

&lt;p&gt;A recovery strategy that takes several hours may be perfectly acceptable for one application and completely unacceptable for another.&lt;/p&gt;

&lt;p&gt;RPO and RTO therefore aren't just theoretical concepts.&lt;/p&gt;

&lt;p&gt;They influence the architecture you choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restoring the database isn't the end
&lt;/h2&gt;

&lt;p&gt;Suppose the PostgreSQL instance has been restored successfully.&lt;/p&gt;

&lt;p&gt;The RDS console says:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Available ✓&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Is the recovery finished?&lt;/p&gt;

&lt;p&gt;Not yet.&lt;/p&gt;

&lt;p&gt;The application still needs to communicate with the restored database.&lt;/p&gt;

&lt;p&gt;The recovery process is therefore closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Backup / Snapshot
        ↓
Restore RDS
        ↓
Database available
        ↓
Application reconnects
        ↓
Functional validation
        ↓
Recovery complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where my QA background strongly influences how I approach recovery.&lt;/p&gt;

&lt;p&gt;I don't want to validate only the infrastructure.&lt;/p&gt;

&lt;p&gt;I want to validate the &lt;strong&gt;complete system&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would I validate?
&lt;/h2&gt;

&lt;p&gt;First, the database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can I connect?&lt;/li&gt;
&lt;li&gt;Is the expected schema present?&lt;/li&gt;
&lt;li&gt;Is the expected data available?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can it connect to PostgreSQL?&lt;/li&gt;
&lt;li&gt;Can it retrieve existing data?&lt;/li&gt;
&lt;li&gt;Can it create new data?&lt;/li&gt;
&lt;li&gt;Do the main API operations work?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And finally, the infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is network connectivity working?&lt;/li&gt;
&lt;li&gt;Are access controls still correct?&lt;/li&gt;
&lt;li&gt;Is monitoring operational?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is essentially the same mindset I used in QA:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't just check that the component is running. Check that the system actually works.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Before working on Cloud and DevOps, I thought of backups mainly as a safety mechanism.&lt;/p&gt;

&lt;p&gt;Now I see them as one part of a larger process:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backup → Restore → Reconnect → Validate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A resilient architecture needs both availability and recoverability.&lt;/p&gt;

&lt;p&gt;And recovery should not be something you discover for the first time during an incident.&lt;/p&gt;

&lt;p&gt;Ideally, the restore process should be tested.&lt;/p&gt;

&lt;p&gt;Because a backup you have never successfully restored is ultimately an assumption.&lt;/p&gt;

&lt;p&gt;My QA background taught me to question those assumptions.&lt;/p&gt;

&lt;p&gt;Cloud is teaching me how to turn them into repeatable processes.&lt;/p&gt;

&lt;p&gt;And that's probably the most important lesson from this project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A backup gives you a recovery point. A tested restore gives you confidence.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>rds</category>
      <category>postgres</category>
      <category>devops</category>
    </item>
    <item>
      <title>What QA Taught Me About DevOps</title>
      <dc:creator>Amira Abidi</dc:creator>
      <pubDate>Tue, 29 Sep 2026 09:02:03 +0000</pubDate>
      <link>https://dev.to/amira_abidi/what-qa-taught-me-about-devops-59c</link>
      <guid>https://dev.to/amira_abidi/what-qa-taught-me-about-devops-59c</guid>
      <description>&lt;p&gt;When I started moving from QA and validation toward Cloud and DevOps, I initially thought I had to leave my QA background behind.&lt;/p&gt;

&lt;p&gt;I was wrong.&lt;/p&gt;

&lt;p&gt;The more I worked with AWS, Terraform, Kubernetes and CI/CD, the more I realized that many habits I developed as a QA engineer were still useful — sometimes even more than I expected.&lt;/p&gt;

&lt;p&gt;Here are the lessons from QA that I now carry into my Cloud and DevOps projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Think about failure before success
&lt;/h2&gt;

&lt;p&gt;QA teaches you not to ask only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Does it work?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You also ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens if the service is unavailable?&lt;/li&gt;
&lt;li&gt;What happens if a dependency fails?&lt;/li&gt;
&lt;li&gt;What happens with invalid input?&lt;/li&gt;
&lt;li&gt;What happens under load?&lt;/li&gt;
&lt;li&gt;What happens after a restart?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mindset became very useful when working with Kubernetes and AWS.&lt;/p&gt;

&lt;p&gt;For example, when building my LibraryCorner project on Amazon EKS, I didn't want to validate only that the application was running.&lt;/p&gt;

&lt;p&gt;I also looked at scaling, health checks, database availability, monitoring and recovery.&lt;/p&gt;

&lt;p&gt;The question changed from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Can I deploy it?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“How does it behave when something goes wrong?”&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Automation is more than saving time
&lt;/h2&gt;

&lt;p&gt;Automation was already part of my QA work.&lt;/p&gt;

&lt;p&gt;Tests, validation processes and repetitive checks are much more reliable when they can be executed automatically.&lt;/p&gt;

&lt;p&gt;Moving into DevOps made this principle even stronger.&lt;/p&gt;

&lt;p&gt;Infrastructure can be automated with Terraform.&lt;/p&gt;

&lt;p&gt;Application delivery can be automated with CI/CD.&lt;/p&gt;

&lt;p&gt;Kubernetes can automatically maintain the desired state.&lt;/p&gt;

&lt;p&gt;Monitoring can continuously check what is happening.&lt;/p&gt;

&lt;p&gt;The goal isn't simply to eliminate manual work.&lt;/p&gt;

&lt;p&gt;It is to make processes &lt;strong&gt;repeatable, predictable and easier to trust&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Reproducibility matters
&lt;/h2&gt;

&lt;p&gt;One of the most frustrating situations in testing is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“It worked on my machine.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A good test environment should be reproducible.&lt;/p&gt;

&lt;p&gt;The same principle applies to infrastructure.&lt;/p&gt;

&lt;p&gt;With Terraform, I can describe infrastructure as code instead of manually creating resources in the AWS console.&lt;/p&gt;

&lt;p&gt;With Kubernetes manifests and Kustomize, I can define application configurations consistently across environments.&lt;/p&gt;

&lt;p&gt;This changed the way I think about infrastructure.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“How did I configure this?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I can ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Can I reproduce this configuration?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's a much better question.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Observability is part of validation
&lt;/h2&gt;

&lt;p&gt;QA engineers spend a lot of time looking for evidence.&lt;/p&gt;

&lt;p&gt;Logs, test results, error messages and system behavior help answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“What actually happened?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Observability follows the same principle.&lt;/p&gt;

&lt;p&gt;While working on LibraryCorner, I implemented Prometheus and Grafana monitoring for my EKS environment.&lt;/p&gt;

&lt;p&gt;Metrics aren't just dashboards that look nice.&lt;/p&gt;

&lt;p&gt;They provide evidence about what the system is doing.&lt;/p&gt;

&lt;p&gt;CPU usage, pod count, resource consumption and application behavior can help explain why something happened.&lt;/p&gt;

&lt;p&gt;For me, observability is therefore closely related to validation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;you cannot validate what you cannot see.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Testing doesn't disappear in DevOps
&lt;/h2&gt;

&lt;p&gt;Before moving into Cloud, I sometimes associated DevOps with infrastructure, pipelines and deployments.&lt;/p&gt;

&lt;p&gt;Today, I see it differently.&lt;/p&gt;

&lt;p&gt;Testing is still there.&lt;/p&gt;

&lt;p&gt;It simply becomes part of a larger feedback loop.&lt;/p&gt;

&lt;p&gt;Code changes → tests → build → deployment → monitoring → feedback.&lt;/p&gt;

&lt;p&gt;The objective is to detect problems earlier and shorten the time between a change and reliable feedback.&lt;/p&gt;

&lt;p&gt;That is one of the reasons my QA background fits naturally into this environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. My mindset changed more than my tools
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from this transition is that moving from QA to DevOps isn't only about learning new technologies.&lt;/p&gt;

&lt;p&gt;Yes, I had to learn AWS, Terraform, Kubernetes, Linux, networking and CI/CD.&lt;/p&gt;

&lt;p&gt;But the most valuable thing I brought with me wasn't a specific tool.&lt;/p&gt;

&lt;p&gt;It was a way of thinking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Challenge assumptions.&lt;/li&gt;
&lt;li&gt;Look for failure scenarios.&lt;/li&gt;
&lt;li&gt;Automate repetitive work.&lt;/li&gt;
&lt;li&gt;Make environments reproducible.&lt;/li&gt;
&lt;li&gt;Collect evidence.&lt;/li&gt;
&lt;li&gt;Monitor what happens after deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These principles remain useful regardless of whether the system is a test environment, a Kubernetes cluster or an AWS architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  From QA to Cloud
&lt;/h2&gt;

&lt;p&gt;My transition from QA/validation to Cloud and DevOps didn't mean starting from zero.&lt;/p&gt;

&lt;p&gt;It meant building on top of what I already knew.&lt;/p&gt;

&lt;p&gt;QA taught me to question systems.&lt;/p&gt;

&lt;p&gt;Cloud and DevOps are teaching me how to build, automate and operate them.&lt;/p&gt;

&lt;p&gt;And I'm discovering that these two perspectives complement each other more than I initially expected.&lt;/p&gt;

&lt;p&gt;That's probably the most important lesson I've learned so far in this transition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your previous experience doesn't necessarily disappear when you change careers. Sometimes, it becomes your advantage.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <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>
