<?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: Victor Robin</title>
    <description>The latest articles on DEV Community by Victor Robin (@ovrobin).</description>
    <link>https://dev.to/ovrobin</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%2F1932178%2F66c98146-53e2-4a02-b8e5-0ec0d0aaa82d.jpg</url>
      <title>DEV Community: Victor Robin</title>
      <link>https://dev.to/ovrobin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ovrobin"/>
    <language>en</language>
    <item>
      <title>Building a DevSecOps Pipeline on AWS EKS with Terraform, ArgoCD, and GitHub Actions</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Sat, 27 Jun 2026 01:18:52 +0000</pubDate>
      <link>https://dev.to/ovrobin/building-a-devsecops-pipeline-on-aws-eks-with-terraform-argocd-and-github-actions-5fi4</link>
      <guid>https://dev.to/ovrobin/building-a-devsecops-pipeline-on-aws-eks-with-terraform-argocd-and-github-actions-5fi4</guid>
      <description>&lt;p&gt;In this article, we'll walk through how I built a full DevSecOps pipeline on AWS EKS — from infrastructure provisioning with Terraform to GitOps delivery with ArgoCD, automated DNS with ExternalDNS, security scanning at every stage, and observability with Grafana. We'll cover every decision, every tool, and every step needed to replicate it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Deploying a microservices application to production isn't just about getting pods running. It's about building the surrounding system: automated infrastructure, secure pipelines, GitOps delivery, and observability — in a way that's repeatable and maintainable.&lt;/p&gt;

&lt;p&gt;For this project, I took Google's &lt;a href="https://github.com/GoogleCloudPlatform/microservices-demo" rel="noopener noreferrer"&gt;Online Boutique&lt;/a&gt; (an 11-service e-commerce demo app) and built a production-grade AWS platform around it. The goal was to treat everything as code: infrastructure, delivery, DNS, monitoring — all automated, nothing manual.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Terminologies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Terraform&lt;/strong&gt;: Infrastructure as Code tool used to provision and manage all AWS resources declaratively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EKS (Elastic Kubernetes Service)&lt;/strong&gt;: AWS's managed Kubernetes service. Handles the control plane; we manage the worker nodes via managed node groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ArgoCD&lt;/strong&gt;: A GitOps continuous delivery tool for Kubernetes. It watches a Git repository and automatically syncs the cluster state to match what's in the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt;: CI/CD platform built into GitHub. Used here for two pipelines: one for infrastructure, one for building and pushing container images.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Load Balancer Controller (LBC)&lt;/strong&gt;: A Kubernetes controller that provisions AWS ALBs from Ingress objects. All three services (shop, Grafana, SonarQube) share a single ALB via ingress groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ExternalDNS&lt;/strong&gt;: A Kubernetes controller that watches Ingress objects and automatically creates Route 53 DNS records when an ALB is provisioned. No manual DNS steps needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SonarQube&lt;/strong&gt;: Static code analysis tool. Enforces a Quality Gate — the pipeline fails if code doesn't meet the defined standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trivy&lt;/strong&gt;: Container image vulnerability scanner. Runs before every ECR push.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gitleaks&lt;/strong&gt;: Scans commits for accidentally exposed secrets before anything else runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IRSA (IAM Roles for Service Accounts)&lt;/strong&gt;: AWS mechanism that gives Kubernetes pods scoped IAM permissions without static credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The platform is split into two pipelines and five Terraform stacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Infrastructure pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vpc → eks-cluster + ecr (parallel) → rds → eks-tools → argocd-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stack reads outputs from the previous one via Terraform remote state; no hardcoded ARNs are passed between stacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Push to main (src/)
      │
      ▼
GitHub Actions (build.yml)
  ├── Gitleaks        secret scan
  ├── SonarQube       code quality gate
  ├── Docker build    only changed services detected via git diff
  ├── Trivy           image CVE scan
  └── ECR push + commit updated image tags
                      │
                      ▼
                  ArgoCD detects the new commit
                      │
                      ▼
                  Cluster synced — new images running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Infrastructure diagram
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuy6i11uwq1cookf6ntkm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuy6i11uwq1cookf6ntkm.png" alt=" " width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before following along, have these ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An active AWS account&lt;/li&gt;
&lt;li&gt;A domain name with a Route53 public hosted zone (&lt;code&gt;.xyz&lt;/code&gt; domains are usually under $1), i.e., your domain points to the nameserver in your hosted zone&lt;/li&gt;
&lt;li&gt;A GitHub account with the repo forked from &lt;a href="https://github.com/Vivixell/microservices-demo" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS CLI and Terraform installed locally (for the one-time bootstrap only)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Fork and clone
&lt;/h2&gt;

&lt;p&gt;Fork the repo and clone it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/YOUR_USERNAME/microservices-demo.git
&lt;span class="nb"&gt;cd &lt;/span&gt;microservices-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo 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;├── bootstrap/                # S3 state bucket (apply locally, one-time)
├── terraform/
│   ├── vpc/
│   ├── eks-cluster/
│   ├── eks-tools/            # LBC, ExternalDNS, ArgoCD, Grafana, SonarQube
│   ├── rds/
│   ├── ecr/
│   └── argocd-app/
├── kubernetes-manifests/     # All K8s objects — ArgoCD watches this
├── src/                      # Source code for all 11 microservices
├── teardown.sh               # Destroys all AWS resources ( can be run locally, but use the teardown pipeline)
└── .github/workflows/
    ├── terraform.yml         # Infrastructure pipeline
    ├── build.yml             # Build, scan and push pipeline
    └── teardown.yml          # Manual-only — triggers full teardown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Bootstrap the S3 state bucket (local, one-time)
&lt;/h2&gt;

&lt;p&gt;Terraform needs an S3 bucket to store remote state before it can run in CI. This is the only step you run locally; everything else is handled by the pipeline.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;bootstrap/main.tf&lt;/code&gt; and change the bucket name:&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_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YOUR-BUCKET-NAME"&lt;/span&gt;   &lt;span class="c1"&gt;# ← choose a unique name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then apply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;bootstrap/
terraform init
terraform validate
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates the bucket with versioning enabled and native S3 locking; no DynamoDB table needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: GitHub Actions IAM role (OIDC)
&lt;/h2&gt;

&lt;p&gt;The pipeline authenticates to AWS using OIDC; no static access keys are stored in GitHub secrets.&lt;/p&gt;

&lt;p&gt;In the AWS console, go to &lt;strong&gt;IAM → Identity providers → Add provider&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provider type: &lt;code&gt;OpenID Connect&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Provider URL: &lt;code&gt;https://token.actions.githubusercontent.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Audience: &lt;code&gt;sts.amazonaws.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create an IAM role with this trust policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Federated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"token.actions.githubusercontent.com:sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo:YOUR_GITHUB_USERNAME/microservices-demo:*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"token.actions.githubusercontent.com:aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attach &lt;code&gt;AdministratorAccess&lt;/code&gt; (this is a demo — scope it down for production). Note the role ARN.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Update the actuall values for &lt;strong&gt;YOUR_ACCOUNT_ID&lt;/strong&gt;, and &lt;strong&gt;YOUR_GITHUB_USERNAME&lt;/strong&gt; in the above policy.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 4: ACM Certificate
&lt;/h2&gt;

&lt;p&gt;Go to &lt;strong&gt;AWS Certificate Manager → Request certificate&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add your domains or use a wildcard:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  *.yourdomain.com 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;DNS validation&lt;/strong&gt;: AWS offers a one-click button to create the validation CNAME in Route 53&lt;/li&gt;
&lt;li&gt;Wait until the status shows &lt;strong&gt;Issued&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note the certificate ARN.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Update the code for your environment
&lt;/h2&gt;

&lt;p&gt;Go through each file below and swap in your values.&lt;/p&gt;

&lt;p&gt;i. &lt;strong&gt;S3 bucket name&lt;/strong&gt; — every &lt;code&gt;backend "s3"&lt;/code&gt; block and &lt;code&gt;terraform_remote_state&lt;/code&gt; block across all stacks:&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;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YOUR-BUCKET-NAME"&lt;/span&gt;   &lt;span class="c1"&gt;# ← same name from Step 2&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;ii. &lt;strong&gt;EKS access entry&lt;/strong&gt; — &lt;code&gt;terraform/eks-cluster/main.tf&lt;/code&gt; — lets your local &lt;code&gt;kubectl&lt;/code&gt; work:&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;access_entries&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;my_user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;principal_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::YOUR_ACCOUNT_ID:user/YOUR_USERNAME"&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;iii. &lt;strong&gt;ArgoCD repo URL&lt;/strong&gt; — &lt;code&gt;terraform/argocd-app/main.tf&lt;/code&gt;:&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;repo_url&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/YOUR_USERNAME/microservices-demo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iv. &lt;strong&gt;Grafana&lt;/strong&gt; — &lt;code&gt;terraform/eks-tools/grafana-values.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;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;grafana.ini&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;root_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://grafana.yourdomain.com&lt;/span&gt;
  &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;alb.ingress.kubernetes.io/certificate-arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_ACM_CERT_ARN&lt;/span&gt;
      &lt;span class="na"&gt;external-dns.alpha.kubernetes.io/hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana.yourdomain.com&lt;/span&gt;
    &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;grafana.yourdomain.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;v. &lt;strong&gt;SonarQube&lt;/strong&gt; — &lt;code&gt;terraform/eks-tools/sonarqube-values.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;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;alb.ingress.kubernetes.io/certificate-arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_ACM_CERT_ARN&lt;/span&gt;
    &lt;span class="na"&gt;external-dns.alpha.kubernetes.io/hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonarqube.yourdomain.com&lt;/span&gt;
  &lt;span class="na"&gt;hosts&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;sonarqube.yourdomain.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vi. &lt;strong&gt;Frontend&lt;/strong&gt; — &lt;code&gt;kubernetes-manifests/frontend.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;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;alb.ingress.kubernetes.io/certificate-arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_ACM_CERT_ARN&lt;/span&gt;
  &lt;span class="na"&gt;external-dns.alpha.kubernetes.io/hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop.yourdomain.com&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;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;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop.yourdomain.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave every other thing, especially &lt;code&gt;alb.ingress.kubernetes.io/group.name&lt;/code&gt;, unchanged across all three — this is what makes all three services share a single ALB.&lt;/p&gt;

&lt;p&gt;The exact values to update are &lt;strong&gt;YOUR_ACM_CERT_ARN&lt;/strong&gt;, and &lt;strong&gt;.yourdomain.com&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: GitHub Secrets
&lt;/h2&gt;

&lt;p&gt;Go to &lt;strong&gt;Settings → Secrets and variables → Actions → New repository secret&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS_ROLE_ARN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ARN from Step 3&lt;/td&gt;
&lt;td&gt;Now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SONAR_HOST_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://sonarqube.yourdomain.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Now (placeholder)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SONAR_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any random string&lt;/td&gt;
&lt;td&gt;Now (updated after Step 8)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;SONAR_HOST_URL&lt;/code&gt; and &lt;code&gt;SONAR_TOKEN&lt;/code&gt; must exist for the build pipeline to parse even before SonarQube is live — you'll replace them with real values after Step 8.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Run the Terraform pipeline
&lt;/h2&gt;

&lt;p&gt;Commit and push your changes to &lt;code&gt;main&lt;/code&gt;, or trigger manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Actions → Terraform → Run workflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6aeryot4rren8rlq87tl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6aeryot4rren8rlq87tl.png" alt=" " width="799" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pipeline applies all stacks in order; this takes &lt;strong&gt;20–30 minutes&lt;/strong&gt;, mostly EKS cluster creation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What gets created:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC with public/private subnets across 2 AZs&lt;/li&gt;
&lt;li&gt;EKS cluster (Kubernetes 1.32) with 2× t3.large nodes, autoscaling to 6&lt;/li&gt;
&lt;li&gt;ECR repositories for all 11 services&lt;/li&gt;
&lt;li&gt;RDS PostgreSQL for SonarQube&lt;/li&gt;
&lt;li&gt;All cluster tooling: LBC, ExternalDNS, ArgoCD, Grafana, SonarQube, cert-manager&lt;/li&gt;
&lt;li&gt;ArgoCD Application pointing at your fork&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once &lt;code&gt;eks-tools&lt;/code&gt; completes, ExternalDNS is running. As soon as ArgoCD syncs your Ingress objects and the ALB is provisioned, ExternalDNS automatically creates the Route 53 &lt;code&gt;A&lt;/code&gt; records for all three subdomains; no manual DNS steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: SonarQube setup
&lt;/h2&gt;

&lt;p&gt;Wait a few minutes for DNS to propagate, then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;code&gt;https://sonarqube.yourdomain.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Login: &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;admin&lt;/code&gt;; you'll be prompted to change the password&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;My Account → Security → Generate Token&lt;/strong&gt;, name it &lt;code&gt;github-actions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy the token; shown only once&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Update your GitHub secrets with the real values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SONAR_HOST_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://sonarqube.yourdomain.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SONAR_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Token from above&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Fix the Quality Gate [&lt;/strong&gt;optional*&lt;em&gt;, *better to leave for later&lt;/em&gt;]** — the default gate requires test coverage that Google's demo code doesn't have. Go to &lt;strong&gt;Administration → Quality Gates&lt;/strong&gt;, remove the coverage condition or create a permissive gate, and set it as the default.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can skip this if you follow my code word-for-word; nothing breaks.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 9: Run the build pipeline [&lt;strong&gt;Trigger Manually&lt;/strong&gt;]
&lt;/h2&gt;

&lt;p&gt;Push any change inside &lt;code&gt;src/&lt;/code&gt;, or trigger manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Actions → Build, Scan and Push → Run workflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07u7o6bfo2jgfu34kg9v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07u7o6bfo2jgfu34kg9v.png" alt=" " width="800" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the first run, all 11 services are built in parallel. On subsequent runs, only changed services are detected via git diff.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 10: Verify
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Configure local kubectl&lt;/span&gt;
aws eks update-kubeconfig &lt;span class="nt"&gt;--name&lt;/span&gt; online-boutique &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1

&lt;span class="c"&gt;# All pods running&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt;

&lt;span class="c"&gt;# ArgoCD app synced and healthy&lt;/span&gt;
kubectl get application &lt;span class="nt"&gt;-n&lt;/span&gt; argocd

&lt;span class="c"&gt;# Confirm ExternalDNS created Route 53 records&lt;/span&gt;
aws route53 list-resource-record-sets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hosted-zone-id&lt;/span&gt; YOUR_HOSTED_ZONE_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"ResourceRecordSets[?contains(Name, 'yourdomain')]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;https://shop.yourdomain.com&lt;/code&gt;: you should see the Online Boutique storefront.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj3uytubmxf6c0yls7xuk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj3uytubmxf6c0yls7xuk.png" alt=" " width="799" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7nrj2pyh5p5f92r2mdr2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7nrj2pyh5p5f92r2mdr2.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Accessing cluster tools
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Grafana&lt;/strong&gt;: &lt;code&gt;https://grafana.yourdomain.com&lt;/code&gt;&lt;br&gt;
username = admin&lt;br&gt;
password = use the CLI command below to get it, or check from the console in Secret Manager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws secretsmanager get-secret-value &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-id&lt;/span&gt; online-boutique/grafana-admin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'SecretString'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ArgoCD&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; argocd get secret argocd-initial-admin-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{.data.password}"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;

kubectl port-forward svc/argocd-server &lt;span class="nt"&gt;-n&lt;/span&gt; argocd 8080:443
&lt;span class="c"&gt;# Visit https://localhost:8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Teardown
&lt;/h2&gt;

&lt;p&gt;To destroy everything, trigger the teardown workflow manually from GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Actions → Teardown → Run workflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or run locally: This will spam your folder with Terraform binaries and files, so you want to avoid this and use 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;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x teardown.sh
./teardown.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The S3 state bucket is not deleted — remove it manually if needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DNS records not created&lt;/strong&gt;&lt;br&gt;
ExternalDNS only creates records after the ALB hostname appears in the Ingress status. Check logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system deployment/external-dns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ingress has no ADDRESS&lt;/strong&gt;&lt;br&gt;
The LBC is not running or healthy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system deployment/aws-load-balancer-controller
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ArgoCD shows ImagePullBackOff&lt;/strong&gt;&lt;br&gt;
The build pipeline hasn't run yet or Trivy blocked a push. Check the build pipeline logs in GitHub Actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SonarQube quality gate fails&lt;/strong&gt;&lt;br&gt;
Assign a permissive quality gate under &lt;strong&gt;SonarQube → Administration → Quality Gates&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;kubectl&lt;/code&gt; Unauthorised from your machine&lt;/strong&gt;&lt;br&gt;
Confirm your IAM user ARN matches the &lt;code&gt;access_entries&lt;/code&gt; block in &lt;code&gt;terraform/eks-cluster/main.tf&lt;/code&gt;, then re-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 eks update-kubeconfig &lt;span class="nt"&gt;--name&lt;/span&gt; online-boutique &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Taking it further
&lt;/h2&gt;

&lt;p&gt;This setup is intentionally straightforward — here are production-grade alternatives worth exploring as the project scales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log aggregation&lt;/strong&gt;: the current setup has no centralised logging. EFK (Elasticsearch, Fluentd, Kibana) or Grafana Loki would give you log correlation alongside the existing Prometheus metrics&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Service mesh&lt;/strong&gt;: adding Istio or Linkerd gives you mTLS between pods, fine-grained traffic control, and distributed tracing without touching application code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Image signing&lt;/strong&gt;: Cosign + Sigstore to sign images post-build and enforce signature verification at the cluster level with Kyverno or OPA Gatekeeper&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Secrets management&lt;/strong&gt;: External Secrets Operator pulling from AWS Secrets Manager instead of passing credentials through Helm values at apply time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Node provisioning&lt;/strong&gt;: replace the managed node group with Karpenter for faster, more cost-efficient scaling with Spot instance support&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-environment&lt;/strong&gt;: promote builds through &lt;code&gt;dev → staging → prod&lt;/code&gt; namespaces or clusters using ArgoCD ApplicationSets and Kustomize overlays&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;If you followed these steps, you now have a full DevSecOps platform running on AWS EKS; infrastructure managed by Terraform and applied by CI, images built and scanned on every push, GitOps delivery via ArgoCD, DNS fully automated by ExternalDNS, and everything observable through Grafana. I hope this was helpful. Drop any questions in the comments below!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>kubernetes</category>
      <category>terraform</category>
    </item>
    <item>
      <title>How to Convince Your Team to Adopt Infrastructure as Code</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Fri, 03 Apr 2026 20:22:27 +0000</pubDate>
      <link>https://dev.to/ovrobin/how-to-convince-your-team-to-adopt-infrastructure-as-code-1cj2</link>
      <guid>https://dev.to/ovrobin/how-to-convince-your-team-to-adopt-infrastructure-as-code-1cj2</guid>
      <description>&lt;p&gt;Writing Terraform code is easy. &lt;/p&gt;

&lt;p&gt;You can master state files, modules, and providers in a weekend. But getting an entire engineering department to stop clicking around in the AWS console? That is the actual hard part. &lt;/p&gt;

&lt;p&gt;Because writing Terraform is an engineering problem...&lt;br&gt;
...but adopting Infrastructure as Code is a cultural shift.&lt;/p&gt;

&lt;p&gt;Here is exactly how you convince your team to make the jump, build trust in automated deployments, and avoid the traps that kill most IaC initiatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Stop Pitching the Tech. Pitch the Business Case.
&lt;/h2&gt;

&lt;p&gt;When you pitch IaC to leadership, do not talk about &lt;code&gt;.tfstate&lt;/code&gt; files or the elegance of HCL. Leadership doesn't care about your tech stack. They care about risk, cost, and velocity.&lt;/p&gt;

&lt;p&gt;Translate technical debt into business language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Problem:&lt;/strong&gt; "Our environments drift, causing delayed releases." -&amp;gt; &lt;strong&gt;The Pitch:&lt;/strong&gt; "IaC guarantees identical environments, meaning faster time-to-market and fewer rollback scenarios."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Problem:&lt;/strong&gt; "We don't know who changed the security group." -&amp;gt; &lt;strong&gt;The Pitch:&lt;/strong&gt; "IaC provides a complete, Git-backed audit trail for every infrastructure change, solving our compliance nightmares."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Problem:&lt;/strong&gt; "Setting up a new dev environment takes three days." -&amp;gt; &lt;strong&gt;The Pitch:&lt;/strong&gt; "Reusable modules will cut environment provisioning down to 10 minutes, freeing up engineering hours for actual product work."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. The Incremental Strategy (How to Actually Do It)
&lt;/h2&gt;

&lt;p&gt;Do not try to boil the ocean. If you ask for a 3-month feature freeze to rewrite all existing infrastructure, you will be denied. You have to prove the value incrementally.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1: Start Net-New.&lt;/strong&gt; Pick one new piece of infrastructure. A single S3 bucket, a new IAM role, or a monitoring dashboard. Provision it entirely with Terraform. This creates an immediate success story with zero migration risk to production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2: Import the Pain Points.&lt;/strong&gt; Once the team sees Phase 1 work, begin importing critical existing resources. Use &lt;code&gt;terraform import&lt;/code&gt; to bring them under state management without recreating them. Prioritize the resources that change frequently or have caused recent outages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3: Establish the Guardrails.&lt;/strong&gt; You can't just throw Terraform at a team and hope for the best. You must establish rules: all changes require a Pull Request, &lt;code&gt;terraform plan&lt;/code&gt; output must be attached to the review, and state locking (via DynamoDB or Terraform Cloud) is non-negotiable. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4: Automate.&lt;/strong&gt; Finally, connect Terraform to your CI/CD pipeline. Infrastructure changes should go through the exact same automated deployment process as application code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. How It Actually Fails in the Real World
&lt;/h2&gt;

&lt;p&gt;The textbooks make IaC adoption sound linear. Having lived through this, here is how it actually falls apart:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Big Bang" Migration&lt;/strong&gt;&lt;br&gt;
Trying to migrate 100% of your legacy infrastructure at once is organizational suicide. You will spend months fighting dependency graphs while delivering zero new value to the business. Leave the stable legacy systems alone until you absolutely have to touch them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Solo Hero" Trap&lt;/strong&gt;&lt;br&gt;
Underestimating the learning curve is fatal. If you write 10,000 lines of Terraform but the rest of the team doesn't know how to read it, you haven't adopted IaC... you've just built a new silo where you are the single point of failure. You must give your team the time and safety to learn without the fear of breaking production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Executive Buy-In&lt;/strong&gt;&lt;br&gt;
If you don't have leadership explicitly stating that "manual console changes are no longer acceptable," people will revert to their old habits the second there is an urgent bug fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;A successful Terraform apply doesn't mean your infrastructure works; it just means the API accepted your code. &lt;/p&gt;

&lt;p&gt;True IaC adoption means building a culture that trusts the pipeline, reviews infrastructure like software, and understands that manual provisioning is no longer an option. &lt;/p&gt;

&lt;p&gt;Start small. Build trust. Automate the rest.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>devops</category>
      <category>evops</category>
    </item>
    <item>
      <title>Automating Terraform Testing: From Unit Tests to End-to-End Validation</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Thu, 02 Apr 2026 13:16:44 +0000</pubDate>
      <link>https://dev.to/ovrobin/automating-terraform-testing-from-unit-tests-to-end-to-end-validation-12in</link>
      <guid>https://dev.to/ovrobin/automating-terraform-testing-from-unit-tests-to-end-to-end-validation-12in</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Manual testing is essential when you are just starting out, but it does not scale. The moment your infrastructure grows beyond what one person can test in an afternoon, you need automated tests. Infrastructure that is tested automatically on every commit is infrastructure you can deploy with absolute confidence.&lt;/p&gt;

&lt;p&gt;In this guide, we will break down the three layers of Terraform automated testing Unit, Integration, and End-to-End (E2E), and build a CI/CD pipeline that runs them automatically using GitHub Actions and secure AWS OIDC authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow along and run this code yourself, you will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;AWS Account&lt;/strong&gt;,  &lt;strong&gt;Terraform&lt;/strong&gt; installed (v1.6.0 or newer for native testing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt; installed (v1.21 or newer for Terratest)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;GitHub account&lt;/strong&gt; to run the CI/CD pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Testing Strategy &amp;amp; Tradeoffs
&lt;/h2&gt;

&lt;p&gt;A mature testing strategy doesn't rely on just one tool; it uses a pyramid approach balancing speed, cost, and thoroughness.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Unit Tests (&lt;code&gt;terraform test&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Terraform 1.6+ ships with a native testing framework using &lt;code&gt;.tftest.hcl&lt;/code&gt; files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; It runs a &lt;code&gt;terraform plan&lt;/code&gt; in memory and evaluates your logic, variable validation, and configuration rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tradeoffs:&lt;/strong&gt; They are lightning-fast (seconds) and completely free because they do not deploy real infrastructure. However, they test &lt;em&gt;less&lt;/em&gt;, they cannot catch cloud provider API errors or verify if an EC2 instance actually boots properly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Integration Tests (Terratest)
&lt;/h4&gt;

&lt;p&gt;Integration tests are written in Go using the Gruntwork Terratest library. They test individual, reusable modules in a sandbox.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; It runs &lt;code&gt;terraform apply&lt;/code&gt;, waits for the infrastructure to boot, makes real HTTP requests to your Load Balancers to verify the app is running, and strictly enforces a &lt;code&gt;terraform destroy&lt;/code&gt; at the end to clean up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tradeoffs:&lt;/strong&gt; Thorough, but slower (5–15 minutes) and incurs minor cloud costs since real AWS resources are provisioned.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. End-to-End (E2E) Tests (Terratest)
&lt;/h4&gt;

&lt;p&gt;E2E tests validate your final, composed architecture (VPC + DB + App) working together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; Deploys your entire production-like stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tradeoffs:&lt;/strong&gt; Highly accurate but very slow (15–30 minutes) and costly. &lt;em&gt;Warning:&lt;/em&gt; Never run an E2E test with a &lt;code&gt;destroy&lt;/code&gt; command against your live production state file. E2E tests should be run in isolated sandbox accounts using randomized naming to prevent collisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Golden Rule:&lt;/strong&gt; Use all three. Run Unit Tests on every Pull Request for instant feedback. Run Integration Tests when merging to your main branch. Run E2E tests in a sandbox environment before major releases.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step-by-Step Guide: Running the Tests
&lt;/h3&gt;

&lt;p&gt;Want to see this in action? Clone the repository and try it yourself.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Clone the Repository
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
git clone https://github.com/Vivixell/aws-blue-green-infra/.git

&lt;span class="nb"&gt;cd &lt;/span&gt;aws-blue-green-infra

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Run the Unit Tests
&lt;/h3&gt;

&lt;p&gt;Navigate to the module directory and run the native Terraform test command. This will validate our strict security group rules and Blue/Green capacity logic without deploying anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;modules/webserver
terraform init
terraform &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;You should see a fast, green output indicating all assertions passed.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Run the Integration Tests
&lt;/h3&gt;

&lt;p&gt;Integration tests deploy real resources. Ensure you have your AWS credentials configured locally, then execute the Go test suite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../../test
go mod tidy

&lt;span class="c"&gt;# We explicitly target the Integration test to avoid running E2E tests locally&lt;/span&gt;
go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-timeout&lt;/span&gt; 45m &lt;span class="nt"&gt;-run&lt;/span&gt; TestWebserverClusterIntegration ./...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Grab a coffee! This will take about 10 minutes to deploy the VPC and ASG, hit the Load Balancer with HTTP requests, and tear it all back down.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: The CI/CD Pipeline
&lt;/h3&gt;

&lt;p&gt;Running tests locally is great, but automation is the ultimate goal. In the &lt;code&gt;.github/workflows/terraform-test.yml&lt;/code&gt; file, we have a pipeline that ties this all together.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding highly privileged AWS Access Keys in GitHub (a major security risk), we use &lt;strong&gt;OpenID Connect (OIDC)&lt;/strong&gt;. GitHub requests a temporary, short-lived token directly from AWS.&lt;/p&gt;

&lt;p&gt;Here is a snippet of how the pipeline is structured to run Unit Tests on PRs, and Integration tests on merges:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;unit-tests&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;Unit Tests&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Configure AWS Credentials via OIDC&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ROLE_ARN }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform test&lt;/span&gt;

  &lt;span class="na"&gt;integration-tests&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;Integration Tests&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'push'&lt;/span&gt; &lt;span class="c1"&gt;# Only runs when merged to main&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unit-tests&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Configure AWS Credentials via OIDC&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ROLE_ARN }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;go mod tidy&lt;/span&gt;
          &lt;span class="s"&gt;go test -v -timeout 45m -run TestWebserverClusterIntegration ./...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;By implementing &lt;code&gt;terraform test&lt;/code&gt; for fast syntax checking, Terratest for real-world validation, and GitHub Actions for continuous integration, you transform your infrastructure from fragile scripts into reliable, enterprise-grade software.&lt;/p&gt;

&lt;p&gt;Happy testing! &lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>aws</category>
      <category>eveops</category>
    </item>
    <item>
      <title>The Importance of Manual Testing in Terraform</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Wed, 01 Apr 2026 16:47:58 +0000</pubDate>
      <link>https://dev.to/ovrobin/the-importance-of-manual-testing-in-terraform-215p</link>
      <guid>https://dev.to/ovrobin/the-importance-of-manual-testing-in-terraform-215p</guid>
      <description>&lt;p&gt;It is incredibly tempting to write Terraform code, see the green &lt;code&gt;Apply complete!&lt;/code&gt; text, and assume your job is done. But there is a massive difference between infrastructure that provisions successfully and infrastructure that actually works.&lt;/p&gt;

&lt;p&gt;For Day 17 of my 30-Day Terraform Challenge, I stepped back from writing code to focus on something even more critical: breaking it. I built a structured manual testing process to verify my &lt;a href="https://github.com/Vivixell/aws-blue-green-infra" rel="noopener noreferrer"&gt;AWS Blue/Green architecture&lt;/a&gt;. Here is why manual testing is the unavoidable foundation of Infrastructure as Code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Manual Testing Still Matters
&lt;/h2&gt;

&lt;p&gt;With tools like Terratest available, manual testing might feel archaic. However, automated tests are only as good as the assertions you write. You cannot automate a test until you manually discover exactly what edge cases, timing issues, or cloud provider quirks need verifying. Manual testing is the exploratory phase that defines your automated test suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Structured Test Checklist
&lt;/h2&gt;

&lt;p&gt;A manual test without a checklist is just aimlessly clicking around the AWS console. To build a robust checklist, you must break your infrastructure down into four distinct verification categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Provisioning Verification:&lt;/strong&gt; Do the core Terraform commands (&lt;code&gt;init&lt;/code&gt;, &lt;code&gt;validate&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;apply&lt;/code&gt;) execute cleanly?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Correctness:&lt;/strong&gt; Do the tags, names, and strict Security Group rules in the AWS Console match your configuration?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Functional Verification:&lt;/strong&gt; Does the architecture actually behave as intended? (e.g., hitting the ALB URL, checking instance health).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Consistency:&lt;/strong&gt; Does running a subsequent &lt;code&gt;terraform plan&lt;/code&gt; return zero changes? &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Provisioning vs. Functional Verification
&lt;/h2&gt;

&lt;p&gt;A successful &lt;code&gt;terraform apply&lt;/code&gt; only tells you one thing: AWS accepted your API calls. This is &lt;strong&gt;provisioning verification&lt;/strong&gt;. It does not tell you if your Security Groups allow the right traffic, if your ALB is routing to the correct Target Group, or if your EC2 instances actually boot up your application.&lt;/p&gt;

&lt;p&gt;To prove the code works, you need &lt;strong&gt;functional verification&lt;/strong&gt;. For my Blue/Green deployment, this meant explicitly curling the ALB DNS name to ensure it returned the correct web page, and manually terminating an EC2 instance to ensure my Auto Scaling Group successfully self-healed. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Cleanup Discipline
&lt;/h2&gt;

&lt;p&gt;Perhaps the most crucial part of manual testing is cleaning up afterward. Terraform state files can become corrupted, or a &lt;code&gt;destroy&lt;/code&gt; command can fail partway through due to an API timeout. If you don't manually verify your cleanup using the AWS CLI (&lt;code&gt;aws ec2 describe-instances&lt;/code&gt;), you risk leaving orphaned resources running in the background. In the cloud, orphaned resources equal runaway bills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actual Test Results: Passes and Failures
&lt;/h2&gt;

&lt;p&gt;Testing isn't about proving your code is perfect; it's about exposing the flaws. Here are two examples from my test run today:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 1: Functional Routing (PASS)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command:&lt;/strong&gt; &lt;code&gt;curl -s http://prod-app-alb-123456.us-east-1.elb.amazonaws.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expected:&lt;/strong&gt; "Welcome to the BLUE Environment! (prod)"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actual:&lt;/strong&gt; "Welcome to the BLUE Environment! (prod)"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; PASS - The ALB successfully routed traffic to the active Target Group.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test 2: State Consistency (FAIL)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command:&lt;/strong&gt; &lt;code&gt;terraform plan&lt;/code&gt; (Run immediately after a successful apply)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expected:&lt;/strong&gt; "No changes. Your infrastructure matches the configuration."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actual:&lt;/strong&gt; 1 resource change detected — missing tag on security group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; FAIL - The &lt;code&gt;aws_security_group&lt;/code&gt; was missing the &lt;code&gt;merge(local.common_tags)&lt;/code&gt; block. I caught this state drift manually and fixed the module code before it scaled to hundreds of resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manual testing is not optional. It is the necessary prerequisite to confidence in the cloud.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>aws</category>
      <category>eveops</category>
    </item>
    <item>
      <title>From Sandbox to Production: Building a Resilient AWS Blue/Green Infrastructure with Terraform</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Tue, 31 Mar 2026 19:35:23 +0000</pubDate>
      <link>https://dev.to/ovrobin/from-sandbox-to-production-building-a-resilient-aws-bluegreen-infrastructure-with-terraform-304c</link>
      <guid>https://dev.to/ovrobin/from-sandbox-to-production-building-a-resilient-aws-bluegreen-infrastructure-with-terraform-304c</guid>
      <description>&lt;p&gt;There is a massive gap between Terraform code that simply "works" on your laptop and code that is truly production-ready. I recently tackled this exact gap as part of my 30-Day Terraform Challenge. The goal? Taking a standard AWS web server setup and transforming it into a reliable, modular, and secure system capable of handling zero-downtime Blue/Green deployments.&lt;/p&gt;

&lt;p&gt;If you are looking to elevate your Infrastructure as Code (IaC) from basic scripts to robust, defensible architecture, this guide will walk you through the core principles of production-grade Terraform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;To follow along with the concepts in this guide, you should have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A basic understanding of AWS networking and compute (VPCs, ALBs, ASGs, EC2).&lt;/li&gt;
&lt;li&gt;Familiarity with standard Terraform commands and syntax.&lt;/li&gt;
&lt;li&gt;(Optional) Go installed on your machine if you wish to run the automated infrastructure tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tools Used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform (&amp;gt;= 1.6.0):&lt;/strong&gt; Our core IaC engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Provider (~&amp;gt; 6.9):&lt;/strong&gt; To interact with AWS APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terratest (Go):&lt;/strong&gt; For automated infrastructure validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Folder Structure Overview
&lt;/h3&gt;

&lt;p&gt;Production code cannot live in a single, monolithic &lt;code&gt;main.tf&lt;/code&gt; file. It needs to be modular, allowing teams to compose infrastructure safely across different environments. We separated our reusable logic into a &lt;code&gt;modules&lt;/code&gt; directory, and called it from environment-specific root folders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── modules/
│   └── webserver/
│       ├── main.tf
│       ├── variables.tf
|       ├── provider.tf
│       ├── outputs.tf
│       └── README.md
|   
├── dev/
│   ├── main.tf
|   ├── backend.tf
│   └── outputs.tf
├── prod/
│   ├── main.tf
|   ├── backend.tf
│   └── outputs.tf
└── test/
    └── webserver_test.go

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Note: For complete usage instructions, inputs, and outputs of the module itself, you can check out the &lt;code&gt;README.md&lt;/code&gt; in the repository's module folder!)&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/Vivixell/aws-blue-green-infra" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step-by-Step Refactoring Guide
&lt;/h3&gt;

&lt;p&gt;Here is a breakdown of the specific refactoring steps required to make this infrastructure production-grade.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Reliability &amp;amp; Zero-Downtime Updates
&lt;/h4&gt;

&lt;p&gt;Reliability means the system survives updates seamlessly. We implemented a &lt;strong&gt;Blue/Green deployment strategy&lt;/strong&gt; using parallel Auto Scaling Groups (ASGs). To ensure updates happen without dropping traffic, we had to apply critical lifecycle rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; Modifying a launch template would immediately tear down running instances before the new ones were fully provisioned, causing an outage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_launch_template"&lt;/span&gt; &lt;span class="s2"&gt;"color"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name_prefix&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-lt-"&lt;/span&gt;
  &lt;span class="nx"&gt;image_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ami&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ubuntu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; We explicitly instruct Terraform to provision the replacement infrastructure before destroying the old one. This is a non-negotiable for production compute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_launch_template"&lt;/span&gt; &lt;span class="s2"&gt;"color"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name_prefix&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-lt-"&lt;/span&gt;
  &lt;span class="nx"&gt;image_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ami&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ubuntu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt;

  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;create_before_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;h4&gt;
  
  
  2. Security &amp;amp; Input Validation
&lt;/h4&gt;

&lt;p&gt;Security isn't just about IAM roles and restricting Security Groups. It is also about protecting the deployment pipeline from human error. By adding strict input validation, we prevent invalid or highly expensive configurations from ever reaching the cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; A developer could type any string for the environment variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"environment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; Terraform will immediately reject the apply if the inputs do not match our strict criteria, protecting our state and our AWS bill.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"environment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The deployment environment (e.g., dev, staging, prod)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;

  &lt;span class="nx"&gt;validation&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;contains&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"staging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Environment must be explicitly set to dev, staging, or prod."&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;h4&gt;
  
  
  3. Observability
&lt;/h4&gt;

&lt;p&gt;You cannot fix what you cannot see. Production systems require automated monitoring and alerting out of the box. Instead of manually clicking through the AWS console later, I integrated CloudWatch alarms directly into the Terraform module.&lt;/p&gt;

&lt;p&gt;We added a dynamic alarm that watches the CPU utilization of whichever ASG is currently active and routes alerts to an SNS topic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_metric_alarm"&lt;/span&gt; &lt;span class="s2"&gt;"high_cpu"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bg_envs&lt;/span&gt;
  &lt;span class="nx"&gt;alarm_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-high-cpu-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;comparison_operator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;
  &lt;span class="nx"&gt;evaluation_periods&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="nx"&gt;metric_name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CPUUtilization"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS/EC2"&lt;/span&gt;
  &lt;span class="nx"&gt;period&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
  &lt;span class="nx"&gt;statistic&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Average"&lt;/span&gt;
  &lt;span class="nx"&gt;threshold&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;

  &lt;span class="nx"&gt;dimensions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;AutoScalingGroupName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_autoscaling_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&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;alarm_actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&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;h4&gt;
  
  
  4. Automated Testing with Terratest
&lt;/h4&gt;

&lt;p&gt;Manual testing is slow, expensive, and prone to oversight. To truly trust our infrastructure, we need automated tests. Using Terratest (a Go library developed by Gruntwork), we wrote a script that actively deploys the infrastructure to a sandbox, runs real-world HTTP checks against the Load Balancer, and then tears it all down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;
&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"testing"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/gruntwork-io/terratest/modules/http-helper"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/gruntwork-io/terratest/modules/terraform"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestWebserverCluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parallel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;terraformOptions&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithDefaultRetryableErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;TerraformDir&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"../dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Vars&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
            &lt;span class="s"&gt;"cluster_name"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;"terratest-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"environment"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"active_environment"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"blue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"vpc_cidr"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;           &lt;span class="s"&gt;"10.0.0.0/16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"instance_type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;"t3.micro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"asg_capacity"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
                &lt;span class="s"&gt;"min"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"max"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"desired"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&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;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c"&gt;// Crucial: Ensures the infrastructure is destroyed even if the test fails&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;terraformOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitAndApply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;terraformOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;albDnsName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;terraformOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"alb_dns_name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"http://"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;albDnsName&lt;/span&gt;

    &lt;span class="c"&gt;// Asserts that the ALB is up and routing to the Blue environment&lt;/span&gt;
    &lt;span class="n"&gt;http_helper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpGetWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Welcome to the BLUE Environment!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&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;&lt;strong&gt;Why this matters:&lt;/strong&gt; Notice the &lt;code&gt;defer terraform.Destroy(t, terraformOptions)&lt;/code&gt; line. Automated tests cost real money because they spin up real AWS resources. If an assertion fails halfway through, the Go test panics and stops. By deferring the destroy command, we guarantee that no matter what happens during the test run, the environment is cleanly destroyed at the end. No orphan resources, no surprise AWS bills.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Terraform is incredibly powerful, but &lt;em&gt;how&lt;/em&gt; you write it dictates whether your infrastructure is a liability or an asset. Moving from "it works" to "it's production-ready" requires a massive shift in mindset toward defensible code, strict boundaries, and automated safety nets. &lt;/p&gt;

&lt;p&gt;Have you implemented automated testing for your IaC yet? Let me know your preferred workflow in the comments!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>terraform</category>
      <category>eveops</category>
    </item>
    <item>
      <title>Deploying Multi-Cloud Infrastructure with Terraform Modules</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Mon, 30 Mar 2026 20:27:10 +0000</pubDate>
      <link>https://dev.to/ovrobin/deploying-multi-cloud-infrastructure-with-terraform-modules-2gh</link>
      <guid>https://dev.to/ovrobin/deploying-multi-cloud-infrastructure-with-terraform-modules-2gh</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/Vivixell/Terraform-k8" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Monolith Problem
&lt;/h2&gt;

&lt;p&gt;Most engineers start writing Terraform by dropping a single AWS provider block at the top of their &lt;code&gt;main.tf&lt;/code&gt; and dumping all their resources underneath it. That works for a weekend project. It completely falls apart in production.&lt;/p&gt;

&lt;p&gt;When you need to deploy a globally distributed application, or provision underlying infrastructure &lt;em&gt;and&lt;/em&gt; deploy application workloads on top of it simultaneously, you need to master advanced provider orchestration.&lt;/p&gt;

&lt;p&gt;Today, I am sharing the blueprints for three advanced Terraform provider patterns: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Passing Aliased Providers into Modules.&lt;/li&gt;
&lt;li&gt;Local Container Orchestration (Docker).&lt;/li&gt;
&lt;li&gt;Provider Chaining (AWS EKS + Kubernetes).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Pattern 1: The Multi-Provider Module
&lt;/h2&gt;

&lt;p&gt;The golden rule of writing reusable Terraform modules is this: &lt;strong&gt;A module must never declare its own provider block.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a module hardcodes a region or an account, it becomes rigid. Instead, a module must demand that the calling configuration passes the providers down to it. We do this using &lt;code&gt;configuration_aliases&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Module Definition (&lt;code&gt;modules/app/main.tf&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Notice how the module explicitly requires two distinct AWS connections to function:&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;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.0"&lt;/span&gt;
      &lt;span class="nx"&gt;configuration_aliases&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replica&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;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"primary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt;
  &lt;span class="nx"&gt;bucket_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"primary-data-"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Root Caller (&lt;code&gt;live/main.tf&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;In the root directory, we define the actual API connections and "wire" them into the module using the &lt;code&gt;providers&lt;/code&gt; map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;alias&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"east"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;alias&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"west"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"global_app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../modules/app"&lt;/span&gt;

  &lt;span class="nx"&gt;providers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;east&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replica&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;west&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;h2&gt;
  
  
  Pattern 2: Local Container Orchestration
&lt;/h2&gt;

&lt;p&gt;Terraform is not just for cloud APIs. It can orchestrate anything with an accessible API—including your local Docker daemon. Before dealing with the complexity of cloud-managed Kubernetes, you can test container deployments locally using the &lt;code&gt;kreuzwerker/docker&lt;/code&gt; provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"docker_image"&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&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="s2"&gt;"nginx:latest"&lt;/span&gt;
  &lt;span class="nx"&gt;keep_locally&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"docker_container"&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;docker_image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nginx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-nginx"&lt;/span&gt;
  &lt;span class="nx"&gt;ports&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;internal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;external&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&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;Run &lt;code&gt;terraform apply&lt;/code&gt;, and Terraform will pull the image and spin up the container on port 8080 locally. No &lt;code&gt;docker run&lt;/code&gt; commands required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 3: Provider Chaining (AWS EKS + Kubernetes)
&lt;/h2&gt;

&lt;p&gt;This is where Terraform flexes its true enterprise capability.&lt;/p&gt;

&lt;p&gt;What if you want to provision an AWS EKS cluster, and then immediately deploy an Nginx pod into that cluster, all within a single &lt;code&gt;terraform apply&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;To do this, we use &lt;strong&gt;Provider Chaining&lt;/strong&gt;. We use the AWS provider to build the cluster, extract the cluster's endpoint and certificate authority data on the fly, and pass that data directly into the configuration of the Kubernetes provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Dynamic Authentication Block
&lt;/h3&gt;

&lt;p&gt;Instead of hardcoding static &lt;code&gt;kubeconfig&lt;/code&gt; files, we use an &lt;code&gt;exec&lt;/code&gt; block. This forces the Kubernetes provider to execute an AWS CLI command in the background to fetch a short-lived authentication token for the cluster we &lt;em&gt;just&lt;/em&gt; built.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Provision the Cluster (AWS Provider)&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"eks"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-aws-modules/eks/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 20.0"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"production-cluster"&lt;/span&gt;
  &lt;span class="c1"&gt;# ... vpc and subnet configuration ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Authenticate dynamically to the new cluster&lt;/span&gt;
&lt;span class="k"&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="k"&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="k"&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"&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;"eks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"get-token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--cluster-name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&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;span class="c1"&gt;# 3. Deploy the workload (Kubernetes Provider)&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"kubernetes_deployment"&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&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;metadata&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="s2"&gt;"nginx-deployment"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;replicas&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="c1"&gt;# ... container spec ...&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;&lt;em&gt;Note:&lt;/em&gt; The &lt;code&gt;depends_on = [module.eks]&lt;/code&gt; is critical. It prevents the Kubernetes provider from trying to deploy pods before the AWS provider has finished standing up the control plane.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;When you decouple providers from modules and learn to chain them together, Terraform transitions from a simple provisioning tool into a complete, end-to-end platform orchestrator.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>terraform</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Getting Started with Multiple Providers in Terraform</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Mon, 30 Mar 2026 16:51:58 +0000</pubDate>
      <link>https://dev.to/ovrobin/getting-started-with-multiple-providers-in-terraform-265h</link>
      <guid>https://dev.to/ovrobin/getting-started-with-multiple-providers-in-terraform-265h</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Every real-world cloud infrastructure eventually outgrows a single region. Whether you are building a multi-region Active-Passive disaster recovery architecture, deploying global CloudFront endpoints, or managing resources across different AWS accounts, a single default Terraform provider won't cut it. &lt;/p&gt;

&lt;p&gt;For Day 14 of my 30-Day Terraform Challenge, I dove deep into Terraform's provider system. In this guide, I will break down how providers actually work under the hood, how to lock their versions, and walk you through a step-by-step implementation of cross-region S3 replication using the Provider Alias pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Provider, Really?
&lt;/h2&gt;

&lt;p&gt;Terraform Core is essentially just a parsing engine. It reads your HCL code and builds a dependency graph. It doesn't actually know how to talk to AWS, Azure, or Kubernetes. &lt;/p&gt;

&lt;p&gt;That is where &lt;strong&gt;Providers&lt;/strong&gt; come in. A provider is an executable plugin (a Go binary) that Terraform downloads. It acts as the translation layer between your declarative HCL code and the target platform's REST APIs. When you write an &lt;code&gt;aws_s3_bucket&lt;/code&gt; block, the AWS provider is what actually makes the &lt;code&gt;PUT&lt;/code&gt; request to the AWS API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation, Versioning, and The Lock File
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;terraform init&lt;/code&gt;, Terraform reads the &lt;code&gt;required_providers&lt;/code&gt; block, reaches out to the Terraform Registry, and downloads the exact provider binaries you need.&lt;/p&gt;

&lt;p&gt;Because cloud APIs change constantly, &lt;strong&gt;you must always pin your provider versions&lt;/strong&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Constraint Syntax
&lt;/h3&gt;

&lt;p&gt;Here is a practical look at how we define version constraints:&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;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.6.0"&lt;/span&gt;

  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 6.0"&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;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;= 6.0.0:&lt;/code&gt; Exact version matching (Too rigid for most use cases).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;&amp;gt;= 6.0.0:&lt;/code&gt; Any version greater than or equal to 6.0.0 (Too risky, might pull a breaking 7.0 update).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;~&amp;gt; 6.9:&lt;/code&gt; The pessimistic constraint operator. This translates to "Use the highest available version in the 6.x.x range, but do NOT upgrade to 7.0". This is the industry standard for balancing security patches with stability.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;.terraform.lock.hcl&lt;/code&gt; File&lt;br&gt;
When &lt;code&gt;terraform init&lt;/code&gt; resolves your version constraints, it generates a &lt;code&gt;.terraform.lock.hcl&lt;/code&gt; file. This file records:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Version:&lt;/strong&gt; The exact version it selected (e.g., &lt;code&gt;6.9.37&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Constraints:&lt;/strong&gt; The rule you set that led to this selection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hashes:&lt;/strong&gt; Cryptographic checksums of the provider binary for multiple operating systems.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Best Practice:&lt;/strong&gt; You must commit this file to Git! It guarantees that your CI/CD pipeline and every engineer on your team downloads the exact same provider version, preventing the classic "It works on my machine" deployment failure.
&lt;/h2&gt;
&lt;h2&gt;
  
  
  Prerequisites for the Lab
&lt;/h2&gt;

&lt;p&gt;If you want to pull down the repository and run this architecture yourself, you will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Terraform installed (&lt;code&gt;&amp;gt;= 1.6.0&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS CLI installed and configured with Admin or PowerUser credentials.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Basic understanding of AWS IAM and S3.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step-by-Step Guide: Multi-Region S3 Replication
&lt;/h2&gt;

&lt;p&gt;By default, an AWS provider block connects to a single region. To deploy to a second region in the same codebase, we use &lt;strong&gt;Provider Aliases&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is how to run my Multi-Region S3 Disaster Recovery architecture.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Clone the Repository
&lt;/h3&gt;

&lt;p&gt;Clone the project and navigate to the directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Vivixell/terraform-aws-s3-replication
&lt;span class="nb"&gt;cd &lt;/span&gt;terraform-aws-s3-replication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Understand the Providers (&lt;code&gt;providers.tf&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;providers.tf&lt;/code&gt;. You will see the default provider (routing to Virginia) and an aliased provider (routing to Oregon)&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="c1"&gt;# Default Provider (Primary Region)&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Aliased Provider (Secondary Region)&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;alias&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"west"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Review the Infrastructure (&lt;code&gt;main.tf&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;main.tf&lt;/code&gt;. Notice how we dictate which region a resource belongs to.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Primary Bucket:
&lt;/h4&gt;

&lt;p&gt;Because there is no explicit provider argument, this bucket automatically deploys to &lt;code&gt;us-east-1&lt;/code&gt; using the default provider.&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_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"primary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ovr-primary-data-"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The Replica Bucket:
&lt;/h4&gt;

&lt;p&gt;Here is the alias pattern in action. By passing &lt;code&gt;provider = aws.west&lt;/code&gt;, Terraform routes this specific API call to the &lt;code&gt;us-west-2&lt;/code&gt; endpoint.&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_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"replica"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;west&lt;/span&gt; 
  &lt;span class="nx"&gt;bucket_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ovr-replica-backup-"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of &lt;code&gt;main.tf&lt;/code&gt; provisions the necessary IAM roles and the replication rule that binds the two buckets together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Deploy the Architecture
&lt;/h3&gt;

&lt;p&gt;Initialize your backend and lock file, check the plan, and apply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
terraform plan
terraform apply &lt;span class="nt"&gt;-auto-approve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminal will output the dynamically generated names of your primary and replica buckets.&lt;/p&gt;

&lt;p&gt;Step 5: Test the Replication&lt;br&gt;
Open your AWS Console and navigate to S3.&lt;/p&gt;

&lt;p&gt;Upload a test file (an image or a &lt;code&gt;.txt&lt;/code&gt; file) to your primary bucket in &lt;code&gt;us-east-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Wait about 30–60 seconds, then check your replica bucket in &lt;code&gt;us-west-2&lt;/code&gt;. The file will have been automatically duplicated across the country!&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6: Clean Up
&lt;/h3&gt;

&lt;p&gt;Because the &lt;code&gt;force_destroy = true&lt;/code&gt; flag is enabled on both buckets in the codebase, tearing down the environment is a single command. (Note: Terraform will delete all replicated files inside the buckets during this process).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform destroy &lt;span class="nt"&gt;-auto-approve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Understanding how to manipulate the provider meta-argument unlocks the ability to build true enterprise-grade, highly available architectures.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>aws</category>
      <category>iac</category>
    </item>
    <item>
      <title>How to Handle Sensitive Data Securely in Terraform</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Sun, 29 Mar 2026 17:55:37 +0000</pubDate>
      <link>https://dev.to/ovrobin/how-to-handle-sensitive-data-securely-in-terraform-262i</link>
      <guid>https://dev.to/ovrobin/how-to-handle-sensitive-data-securely-in-terraform-262i</guid>
      <description>&lt;p&gt;Every real-world infrastructure deployment involves secrets—database passwords, API keys, and TLS certificates. The number one security mistake engineers make is letting those secrets leak into their codebase or terminal outputs. &lt;/p&gt;

&lt;p&gt;For &lt;a href="https://github.com/Vivixell/Security-in-Terraform.git" rel="noopener noreferrer"&gt;todays&lt;/a&gt; Terraform Challenge, I built an impenetrable wall around my infrastructure's sensitive data using AWS Secrets Manager. &lt;/p&gt;

&lt;p&gt;Here is the definitive guide to the three ways secrets leak in Terraform, and exactly how to close every single path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Leak Path 1: Hardcoded in &lt;code&gt;.tf&lt;/code&gt; Files
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Mistake:&lt;/strong&gt; Writing a secret directly into a resource argument. The moment you run &lt;code&gt;git add&lt;/code&gt;, that password is permanently stored in your version control history for anyone in your organization (or the public) to see.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Vulnerable Pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app_database"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SuperSecretPassword123!"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Never&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ &lt;strong&gt;Secure Alternative (AWS Secrets Manager):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of typing the password, create it manually in AWS Secrets Manager. Then, use Terraform &lt;code&gt;data&lt;/code&gt; blocks to fetch it dynamically at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Fetch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;AWS&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_secretsmanager_secret"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db_credentials"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prod/db/credentials"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_secretsmanager_secret_version"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db_credentials"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;secret_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;data.aws_secretsmanager_secret.db_credentials.id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Decode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JSON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;locals&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;db_creds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Inject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;runtime&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app_database"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;local.db_creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;local.db_creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Result: The secret never exists in your configuration files.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Leak Path 2: Passed as a Variable with a Default
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Mistake:&lt;/strong&gt; Moving the secret to &lt;code&gt;variables.tf&lt;/code&gt;, but giving it a &lt;code&gt;default&lt;/code&gt; value. Default values are still committed to source control, meaning the leak still happens.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Vulnerable Pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;variable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db_password"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;type&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SuperSecretPassword123!"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Still&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;committed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Git!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ &lt;strong&gt;Secure Alternative (&lt;code&gt;sensitive = true&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Remove the default value completely. Furthermore, add the &lt;code&gt;sensitive = true&lt;/code&gt; flag. This prevents Terraform from accidentally printing the secret to the terminal screen when you run &lt;code&gt;terraform plan&lt;/code&gt; or &lt;code&gt;terraform apply&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;variable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db_password"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Database administrator password"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;type&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;sensitive&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;will&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;now&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;passed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;securely&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;via&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;TF_VAR_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;environment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variables&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;secure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CI/CD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pipeline.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db_connection_string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;value&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mysql://${local.db_creds["&lt;/span&gt;&lt;span class="err"&gt;username&lt;/span&gt;&lt;span class="s2"&gt;"]}:${local.db_creds["&lt;/span&gt;&lt;span class="err"&gt;password&lt;/span&gt;&lt;span class="s2"&gt;"]}@${aws_db_instance.app_database.endpoint}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;sensitive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Outputs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;sensitive&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;instead&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;raw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CLI&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Leak Path 3: Stored in Plaintext in the State File
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Mistake:&lt;/strong&gt; Even if you use AWS Secrets Manager and the &lt;code&gt;sensitive = true&lt;/code&gt; flag perfectly, &lt;strong&gt;Terraform still stores the final evaluated values of your resources in&lt;/strong&gt; &lt;code&gt;terraform.tfstate&lt;/code&gt; &lt;strong&gt;in plaintext&lt;/strong&gt;. If an attacker gains access to your state file, they have the keys to your kingdom. &lt;code&gt;sensitive = true&lt;/code&gt; only hides it from the terminal; it does &lt;em&gt;not&lt;/em&gt; encrypt the state file.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Secure Alternative (The State File Security Audit):&lt;/strong&gt;&lt;br&gt;
Because secrets inevitably end up in state, the state file itself must be locked down using a remote backend (like AWS S3) with strict security boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Production S3 Backend Security Checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server-Side Encryption:&lt;/strong&gt; Enabled (&lt;code&gt;encrypt = true&lt;/code&gt; and &lt;code&gt;AES256&lt;/code&gt; default encryption rules on the bucket).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Versioning:&lt;/strong&gt; Enabled (to recover from accidental state corruption).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Public Access Blocked:&lt;/strong&gt; &lt;code&gt;block_public_acls&lt;/code&gt; and &lt;code&gt;block_public_policy&lt;/code&gt; strictly enforced.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;State Locking:&lt;/strong&gt; DynamoDB table attached to prevent concurrent write corruption, or use S3 nataive lockfile settings &lt;code&gt;use_lockfile = true&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Least Privilege IAM:&lt;/strong&gt; Only specific CI/CD roles are allowed &lt;code&gt;s3:GetObject&lt;/code&gt; on the state bucket.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, to ensure no local state files or variable files are ever accidentally committed to GitHub, your &lt;code&gt;.gitignore&lt;/code&gt; must always include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Terraform Security Gitignore
.terraform/
*.tfstate
*.tfstate.backup
*.tfvars
override.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Security is not optional in production infrastructure. Master your secrets management before you deploy, not after a breach.&lt;/p&gt;




</description>
      <category>aws</category>
      <category>devops</category>
      <category>security</category>
      <category>terraform</category>
    </item>
    <item>
      <title>How Conditionals Make Terraform Infrastructure Dynamic and Efficient</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Thu, 26 Mar 2026 20:45:09 +0000</pubDate>
      <link>https://dev.to/ovrobin/how-conditionals-make-terraform-infrastructure-dynamic-and-380g</link>
      <guid>https://dev.to/ovrobin/how-conditionals-make-terraform-infrastructure-dynamic-and-380g</guid>
      <description>&lt;p&gt;If you are maintaining separate Terraform codebases for your Development, Staging, and Production environments, you are doing twice the work for half the reliability. &lt;/p&gt;

&lt;p&gt;The gold standard of Infrastructure as Code is the &lt;strong&gt;Environment-Aware Module&lt;/strong&gt;: a single, unified codebase that intelligently adapts its sizing, features, and resources based on the environment it is deployed into. &lt;/p&gt;

&lt;p&gt;Today on my Terraform Challenge, I'll be diving deep into Terraform's conditional logic. Here is how Platform Teams use conditionals to eliminate code duplication and build smarter deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Ternary Operator &amp;amp; Environment-Aware Locals
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; You want &lt;code&gt;t3.micro&lt;/code&gt; instances in Dev to save money, but &lt;code&gt;t3.small&lt;/code&gt; instances in Production to handle traffic. Scattering &lt;code&gt;if/else&lt;/code&gt; logic directly inside your resource blocks makes your code incredibly difficult to read and update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; Centralize your logic using the ternary operator (&lt;code&gt;condition ? true_value : false_value&lt;/code&gt;) inside a &lt;code&gt;locals&lt;/code&gt; block. This creates a single source of truth for your module's environment awareness.&lt;/p&gt;

&lt;p&gt;[Image of Environment-aware Terraform module architecture]&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (Hardcoded &amp;amp; Rigid):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="c1"&gt;# Requires passing this manually every time&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (Dynamic &amp;amp; Centralized):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"environment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The deployment environment"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# The boolean flag&lt;/span&gt;
  &lt;span class="nx"&gt;is_production&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt;

  &lt;span class="c1"&gt;# Centralized environment sizing&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_production&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"t3.small"&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
  &lt;span class="nx"&gt;min_size&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_production&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="c1"&gt;# Clean, readable, and dynamic!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. The Optional Resource Pattern (&lt;code&gt;count = condition ? 1 : 0&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; You need strict CloudWatch Alarms and Auto Scaling policies in Production, but deploying them in Dev is a waste of AWS budget. You cannot dynamically "comment out" a resource block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; Use the &lt;code&gt;count&lt;/code&gt; meta-argument as an On/Off switch. If the condition is true, Terraform sets the count to 1 (creates it). If false, it sets the count to 0 (skips it entirely).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"enable_scaling_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bool&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_autoscaling_policy"&lt;/span&gt; &lt;span class="s2"&gt;"scale_up"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# The On/Off Switch&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enable_scaling_policy&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"production-scale-up"&lt;/span&gt;
  &lt;span class="nx"&gt;autoscaling_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_autoscaling_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;asg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;adjustment_type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ChangeInCapacity"&lt;/span&gt;
  &lt;span class="nx"&gt;scaling_adjustment&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Safely Referencing Conditionally Created Resources
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; If you use the &lt;code&gt;count = 0&lt;/code&gt; trick above to skip creating a scaling policy, but your &lt;code&gt;outputs.tf&lt;/code&gt; file still tries to export that policy's ARN, your &lt;code&gt;terraform plan&lt;/code&gt; will crash. Terraform cannot output the ID of a resource that doesn't exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; You must reference conditionally created resources using the index &lt;code&gt;[0]&lt;/code&gt; (since &lt;code&gt;count&lt;/code&gt; treats them as a list), and use a ternary operator to output &lt;code&gt;null&lt;/code&gt; if the resource was skipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (Will crash if count is 0):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"scaling_policy_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_autoscaling_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scale_up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (Safe and robust):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"scaling_policy_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# If enabled, output the ARN of the first (and only) item. Otherwise, output null.&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enable_scaling_policy&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;aws_autoscaling_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scale_up&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;arn&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Input Validation Blocks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; Your entire environment-aware module relies on the &lt;code&gt;environment&lt;/code&gt; variable being exactly "dev", "staging", or "production". If a junior developer accidentally passes &lt;code&gt;environment = "prod"&lt;/code&gt;, your ternary conditionals will evaluate to false, deploying Dev-sized infrastructure into your Production AWS account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; Use variable validation blocks. This forces Terraform to evaluate the input during the &lt;code&gt;plan&lt;/code&gt; phase and immediately throw a custom error if the string isn't an exact match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"environment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Deployment environment"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;

  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# The condition must evaluate to true, or the plan fails.&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;contains&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"staging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Fatal: Environment must be exactly 'dev', 'staging', or 'production'."&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;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Mastering conditionals transforms you from someone who writes Terraform scripts into someone who architects resilient cloud systems. By centralizing logic in &lt;code&gt;locals&lt;/code&gt;, making resources toggleable, and validating inputs strictly, your modules become virtually bulletproof.&lt;/p&gt;

&lt;p&gt;Are you using validation blocks in your Terraform modules yet? Let me know in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  30DayTerraformChallenge #Terraform #IaC #DevOps #AWSUserGroupKenya #EveOps #CloudEngineering
&lt;/h1&gt;

</description>
      <category>eveops</category>
      <category>devops</category>
      <category>terraform</category>
      <category>iac</category>
    </item>
    <item>
      <title>Mastering Loops and Conditionals in Terraform</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Wed, 25 Mar 2026 20:31:40 +0000</pubDate>
      <link>https://dev.to/ovrobin/mastering-loops-and-conditionals-in-terraform-4351</link>
      <guid>https://dev.to/ovrobin/mastering-loops-and-conditionals-in-terraform-4351</guid>
      <description>&lt;p&gt;When you first start writing Terraform, you declare every resource manually. If you need three private subnets, you write three &lt;code&gt;aws_subnet&lt;/code&gt; blocks. If you need five IAM users, you write five &lt;code&gt;aws_iam_user&lt;/code&gt; blocks. &lt;/p&gt;

&lt;p&gt;This works for simple tutorials, but in a true enterprise environment, hardcoding infrastructure is a massive anti-pattern. Real infrastructure needs to be dynamic, scaling up or down based on variable inputs without requiring massive code rewrites.&lt;/p&gt;

&lt;p&gt;To write professional-grade Terraform, you must master its four primary dynamic tools: &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;for_each&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt; expressions, and conditionals. Here is the definitive guide to how they work, when to use them, and the one dangerous trap that catches almost every junior engineer.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. &lt;code&gt;count&lt;/code&gt;: The Simple Loop
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;count&lt;/code&gt; meta-argument is the simplest way to create multiple identical copies of a resource. You pass it an integer, and Terraform creates that many instances, tracking them with an index number (&lt;code&gt;count.index&lt;/code&gt;) starting at 0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_instance"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;count&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;ami&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ami-0c55b159cbfafe1f0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;instance_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;tags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web-server-${count.index}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Results&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;web-server&lt;/span&gt;&lt;span class="mi"&gt;-0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;web-server&lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;etc.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt; Use &lt;code&gt;count&lt;/code&gt; when the resources are truly identical and interchangeable, and you simply need N number of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The &lt;code&gt;count&lt;/code&gt; Index Problem (The Danger Zone)
&lt;/h2&gt;

&lt;p&gt;It is tempting to use &lt;code&gt;count&lt;/code&gt; with the &lt;code&gt;length()&lt;/code&gt; function to iterate over a list of strings, like a list of usernames. Do not do this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Broken Architecture&lt;/strong&gt;&lt;br&gt;
Imagine you have a list of developers who need IAM users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;variable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"devs"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"charlie"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_iam_user"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"team"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;length(var.devs)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;name&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;var.devs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;count.index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform maps these internally by their index:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;aws_iam_user.team[0]&lt;/code&gt; = alice&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;aws_iam_user.team[1]&lt;/code&gt; = bob&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;aws_iam_user.team[2]&lt;/code&gt; = charlie&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt; Suppose "alice" leaves the company, so you remove her from the list. The list is now &lt;code&gt;["bob", "charlie"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of just deleting Alice, Terraform looks at the new indexes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Index 0 is now bob. Terraform renames Alice's user to Bob.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Index 1 is now charlie. Terraform renames Bob's user to Charlie.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Index 2 no longer exists. Terraform deletes Charlie's original user.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You just caused massive destructive churn across your entire team's access because a list shifted.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. &lt;code&gt;for_each&lt;/code&gt;: The Safe Loop
&lt;/h2&gt;

&lt;p&gt;To solve the list-shifting problem, Terraform introduced &lt;code&gt;for_each&lt;/code&gt;. Instead of using a fragile numerical index, &lt;code&gt;for_each&lt;/code&gt; iterates over a &lt;code&gt;map&lt;/code&gt; or a &lt;code&gt;set&lt;/code&gt; of strings, using the actual string values as the unique identifier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Correct Architecture&lt;/strong&gt;&lt;br&gt;
If we refactor the IAM user example using a &lt;code&gt;set&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;variable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"devs"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;type&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;set(string)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"charlie"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_iam_user"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"team"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;for_each&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;var.devs&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;name&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;each.value&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, Terraform tracks them like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;aws_iam_user.team["alice"]&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;aws_iam_user.team["bob"]&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you remove "alice" from the variable, Terraform only deletes &lt;code&gt;aws_iam_user.team["alice"]&lt;/code&gt;. Bob and Charlie are completely untouched because their identifiers never changed.&lt;/p&gt;

&lt;p&gt;When to use it: Use &lt;code&gt;for_each&lt;/code&gt; whenever you are creating multiple resources that have unique identities (like subnets with different CIDR blocks, or users with different names).&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Conditionals: The Ternary Operator
&lt;/h2&gt;

&lt;p&gt;Sometimes you don't need a loop; you just need an ON/OFF switch. Terraform handles conditional logic using the ternary operator: &lt;code&gt;condition ? true_value : false_value&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The most powerful way to use this is by combining it with count to make an entire resource optional based on an environment variable.&lt;/p&gt;

&lt;p&gt;For example, you might want an Auto Scaling Policy in Production, but not in Dev to save costs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;variable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enable_scaling_policy"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"If true, creates an Auto Scaling policy"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;type&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bool&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;default&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_autoscaling_policy"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scale_up"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;If&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;created).&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;If&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;skipped).&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;count&lt;/span&gt;&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;var.enable_scaling_policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;name&lt;/span&gt;&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production-scale-up"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;autoscaling_group_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;aws_autoscaling_group.asg.name&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;adjustment_type&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ChangeInCapacity"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;scaling_adjustment&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. &lt;code&gt;for&lt;/code&gt; Expressions: Reshaping Data
&lt;/h2&gt;

&lt;p&gt;Do not confuse &lt;code&gt;for_each&lt;/code&gt; with &lt;code&gt;for&lt;/code&gt; expressions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;for_each&lt;/code&gt; creates resources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;for&lt;/code&gt; expressions reshape data.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Often, you need to output data from your module, but the raw Terraform state object is too messy. A &lt;code&gt;for&lt;/code&gt; expression lets you loop through a complex object and extract exactly what you want, usually formatting it into a clean list or map.&lt;/p&gt;

&lt;p&gt;Here is how you extract a clean dictionary mapping your private subnet names directly to their generated AWS Subnet IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"private_subnet_map"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A clean map of private subnet keys to their actual AWS Subnet IDs"&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Loops&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;through&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;aws_subnet.private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;grabbing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(k)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;subnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;aws_subnet.private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;subnet.id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;private_subnet_map&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"private_app_1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subnet-0a1b2c3d4e5f6g7h8"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"private_app_2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subnet-0z9y8x7w6v5u4t3s2"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Mastering these four tools transitions you from writing static Terraform scripts to engineering highly reusable, scalable infrastructure modules.&lt;/p&gt;

&lt;p&gt;Stop copy-pasting resource blocks. Start looping.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>terraform</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Advanced Terraform Module Usage: Versioning, Gotchas, and Reuse Across Environments</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Tue, 24 Mar 2026 19:06:30 +0000</pubDate>
      <link>https://dev.to/ovrobin/advanced-terraform-module-usage-versioning-gotchas-and-reuse-across-environments-hl2</link>
      <guid>https://dev.to/ovrobin/advanced-terraform-module-usage-versioning-gotchas-and-reuse-across-environments-hl2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftyvfgltz5f67ikxm0ups.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftyvfgltz5f67ikxm0ups.png" alt=" " width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you followed my &lt;a href="https://dev.to/ovrobin/building-reusable-infrastructure-the-art-of-terraform-modules-431h"&gt;Day 8&lt;/a&gt; breakdown, you know that moving from a monolithic &lt;code&gt;main.tf&lt;/code&gt; to a reusable Terraform module is a massive architectural leap. But building a module is only half the battle. &lt;/p&gt;

&lt;p&gt;If you don't understand how Terraform resolves paths, handles state logic, or pins versions, your beautiful module will quickly become a nightmare for other engineers to use. &lt;/p&gt;

&lt;p&gt;For Day 9 of the 30-Day Terraform Challenge, I am diving deep into enterprise module management. We are going to cover the three most common module "gotchas" that break deployments, how to properly version your code using Git, and the exact multi-environment pattern Platform Teams use to test new infrastructure safely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: The 3 Terraform Module Gotchas
&lt;/h2&gt;

&lt;p&gt;When you transition from writing root environments to writing child modules, Terraform's behavior changes in subtle ways. Here are three mistakes that are incredibly easy to make and deeply frustrating to debug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 1: The File Path Trap
&lt;/h3&gt;

&lt;p&gt;Imagine you have a &lt;code&gt;user-data.sh&lt;/code&gt; script sitting in your module folder. You might be tempted to reference it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;❌&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;THE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;WRONG&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;WAY&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_launch_template"&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"app"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;user_data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;filebase&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./user-data.sh"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it fails:&lt;/strong&gt; Terraform resolves the &lt;code&gt;./&lt;/code&gt; relative to the directory where you run &lt;code&gt;terraform apply&lt;/code&gt; (the root environment), not where the module lives. When your Prod environment calls this module, Terraform will look for &lt;code&gt;prod/user-data.sh&lt;/code&gt; and crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Always use the &lt;code&gt;path.module&lt;/code&gt; expression. This tells Terraform to dynamically locate the script based on the module's actual location.&lt;/p&gt;

&lt;h4&gt;
  
  
  ✅ THE RIGHT WAY
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_launch_template"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;user_data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;filebase&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${path.module}/user-data.sh"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other possible values are:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgvt8wmrw09egl9lylzf9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgvt8wmrw09egl9lylzf9.png" alt=" " width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 2: The Inline Block "Perpetual Diff"
&lt;/h3&gt;

&lt;p&gt;Some AWS resources, like Security Groups, allow you to define rules inline or as separate resources. When writing modules, inline blocks are dangerous.&lt;/p&gt;

&lt;h4&gt;
  
  
  ❌ THE WRONG WAY (Inline)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;ingress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;from_port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;to_port&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;protocol&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tcp"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it fails:&lt;/strong&gt; If a developer calls your module and later decides they need to attach a custom VPN rule to that same Security Group using an external aws_security_group_rule, Terraform will panic. The module claims strict ownership of the inline block and will constantly try to delete the developer's new rule, resulting in an endless loop of infrastructure changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Always use standalone resources inside modules to allow external extensibility.&lt;/p&gt;

&lt;h4&gt;
  
  
  ✅ THE RIGHT WAY (Standalone)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web-sg"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_security_group_rule"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;type&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ingress"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;security_group_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;aws_security_group.web.id&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;from_port&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;to_port&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;protocol&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tcp"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gotcha 3: The Blunt depends_on
&lt;/h3&gt;

&lt;p&gt;Sometimes, a developer calling your module will try to force an execution order using depends_on.&lt;/p&gt;

&lt;h4&gt;
  
  
  ❌ THE WRONG WAY
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webserver"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;source&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../modules/webserver"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;depends_on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;aws_database_instance.main&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it fails:&lt;/strong&gt; This forces Terraform to treat your entire webserver cluster as a single, opaque block. If anything changes in the database (even just updating a tag), Terraform might mistakenly taint the entire module and attempt to destroy and recreate your Auto Scaling Group and Load Balancer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Let Terraform's native dependency graph do the work. Pass explicit resource outputs into the module as variables.&lt;/p&gt;

&lt;h4&gt;
  
  
  ✅ THE RIGHT WAY
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webserver"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;source&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../modules/webserver"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;db_address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;aws_database_instance.main.address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Implicit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dependency&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;created&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;safely!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 2: Module Versioning &amp;amp; Source Syntax
&lt;/h2&gt;

&lt;p&gt;In a production environment, you never point your infrastructure at a local folder. You point it at a version-controlled registry. This guarantees that a change to the module code doesn't instantly break every environment using it.&lt;/p&gt;

&lt;p&gt;To version a module, you simply use Git tags.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Tagging&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;stable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;repository&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tag&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v0.0.1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Initial stable release"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;push&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;origin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;v&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once tagged, how you format the &lt;code&gt;source&lt;/code&gt; URL determines exactly what Terraform pulls down. Here is what the syntax looks like across different ecosystems:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Local Source (Good for initial drafting, bad for teams):
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webserver"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../modules/webserver"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The Git Repository Source (The Enterprise Standard):
&lt;/h3&gt;

&lt;p&gt;Notice the double slash &lt;code&gt;//.&lt;/code&gt; This tells Terraform where the repository ends and the specific subfolder begins, while &lt;code&gt;?ref=&lt;/code&gt; targets the exact Git tag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;module &lt;span class="s2"&gt;"webserver"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; github.com/Vivixell/Reusable-Infrastructure//modules/webserver?ref&lt;span class="o"&gt;=&lt;/span&gt;v0.0.1&lt;span class="s2"&gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Terraform Public Registry Source:
&lt;/h3&gt;

&lt;p&gt;When pulling from HashiCorp's official registry, the syntax simplifies. The version gets its own explicit argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vpc"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;source&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"terraform-aws-modules/vpc/aws"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5.0.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 3: The Multi-Environment Deployment Pattern
&lt;/h2&gt;

&lt;p&gt;Now that our module is versioned remotely, we can implement the true Platform Engineering lifecycle: &lt;strong&gt;Dev tests the new features, Prod stays pinned to stability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I released &lt;code&gt;v0.0.1&lt;/code&gt; of &lt;a href="https://github.com/Vivixell/Reusable-Infrastructure/tree/master/modules/webserver" rel="noopener noreferrer"&gt;my webserver module&lt;/a&gt;, and then added a new &lt;code&gt;custom_tags&lt;/code&gt; feature and released &lt;code&gt;v0.0.2&lt;/code&gt;. Here is how my root environments consume them simultaneously without conflict:&lt;/p&gt;

&lt;h4&gt;
  
  
  The Production Environment (Pinned to Stable)
&lt;/h4&gt;

&lt;p&gt;Production code should never use the master branch or the "latest" tag. It is strictly pinned to the battle-tested &lt;code&gt;v0.0.1&lt;/code&gt; release.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# prod/main.tf&lt;/span&gt;
module &lt;span class="s2"&gt;"webserver_cluster"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; github.com/Vivixell/Reusable-Infrastructure//modules/webserver?ref&lt;span class="o"&gt;=&lt;/span&gt;v0.0.1&lt;span class="s2"&gt;"

  cluster_name  = "&lt;/span&gt;prod-app&lt;span class="s2"&gt;"
  instance_type = "&lt;/span&gt;t3.small&lt;span class="s2"&gt;"
  # ... standard inputs ( check the repo for the complete code)
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/Vivixell/Terraform-Module-Usage-Advance-" rel="noopener noreferrer"&gt;check the repo for the complete code&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The Development Environment (Testing Bleeding Edge)
&lt;/h4&gt;

&lt;p&gt;The Dev team is actively testing the new tagging feature. Their environment points to &lt;code&gt;v0.0.2&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dev/main.tf&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webserver_cluster"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[github.com/Vivixell/Reusable-Infrastructure//modules/webserver?ref=v0.0.2](https://github.com/Vivixell/Reusable-Infrastructure//modules/webserver?ref=v0.0.2)"&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;cluster_name&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev-app"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;instance_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Testing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;feature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;introduced&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;v&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;custom_tags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;Environment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Development"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;Owner&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OVR"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run &lt;code&gt;terraform init&lt;/code&gt; in these separate folders, Terraform downloads the exact, isolated versions of the code. If &lt;code&gt;v0.0.2&lt;/code&gt; has a catastrophic bug, Production remains completely safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Understanding file paths, inline blocks, and versioning is what separates writing Terraform scripts from building actual Infrastructure as Code architecture.&lt;/p&gt;

&lt;p&gt;Are you pinning your module versions, or are you living dangerously on the main branch? Let me know in the comments.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>aws</category>
      <category>eveops</category>
    </item>
    <item>
      <title>Building Reusable Infrastructure: The Art of Terraform Modules</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Mon, 23 Mar 2026 20:31:19 +0000</pubDate>
      <link>https://dev.to/ovrobin/building-reusable-infrastructure-the-art-of-terraform-modules-431h</link>
      <guid>https://dev.to/ovrobin/building-reusable-infrastructure-the-art-of-terraform-modules-431h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb4kzwq8xmzjc3hjge8dw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb4kzwq8xmzjc3hjge8dw.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are writing Terraform by putting all your resources into a single &lt;strong&gt;&lt;code&gt;main.tf&lt;/code&gt;&lt;/strong&gt; file, you aren't building infrastructure; you are just writing a very long deployment script.&lt;/p&gt;

&lt;p&gt;On Day 6 of my Terraform journey, I built a highly available web cluster. It worked perfectly. But if my team suddenly asked for a Staging environment, my only option would have been to copy and paste 200 lines of code. That is a maintenance nightmare.&lt;/p&gt;

&lt;p&gt;Today, for Day 8 of the 30-Day Terraform Challenge, I ripped that monolithic architecture apart and converted it into a reusable Terraform Module. Here is a breakdown of how module architecture actually works, the calling patterns, and the difference between a module your team will love and one they will hate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of a Module Directory
&lt;/h2&gt;

&lt;p&gt;A module is simply a container for multiple resources that are used together. The moment you start using modules, your mental model must split into two concepts: the Child Module (the blueprint) and the Root Module (the execution environment).&lt;/p&gt;

&lt;p&gt;Here is the directory structure I built to manage this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform-project/
├── modules/
│   └── webserver/       &lt;span class="c"&gt;# The Child Module (The Blueprint)&lt;/span&gt;
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       ├── versions.tf
│       └── README.md
├── dev/                 &lt;span class="c"&gt;# The Root Module (The Execution)&lt;/span&gt;
│   ├── main.tf          
│   ├── backend.tf       
│   └── .terraform.lock.hcl
└── prod/                &lt;span class="c"&gt;# The Root Module (The Execution)&lt;/span&gt;
    ├── main.tf          
    ├── backend.tf       
    └── .terraform.lock.hcl

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Golden Rules of the Child Module
&lt;/h2&gt;

&lt;p&gt;Notice what is missing from the &lt;code&gt;modules/webserver&lt;/code&gt; folder: there is no provider &lt;code&gt;"aws"&lt;/code&gt; block, and no backend &lt;code&gt;"s3"&lt;/code&gt; block. A good module is completely agnostic. It should not know if it is being deployed to &lt;code&gt;us-east-1&lt;/code&gt; or &lt;code&gt;eu-west-2&lt;/code&gt;, and it shouldn't care where its state file is stored. The Root environments (&lt;code&gt;dev/&lt;/code&gt; and &lt;code&gt;prod/&lt;/code&gt;) handle the authentication and pass it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inputs, Outputs, and the Calling Pattern
&lt;/h2&gt;

&lt;p&gt;To make the blueprint reusable, you have to strip out every hardcoded value.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Inputs (&lt;code&gt;variables.tf&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Instead of naming my VPC &lt;code&gt;"my-vpc"&lt;/code&gt;, I introduced a &lt;code&gt;cluster_name&lt;/code&gt; variable. I also removed the default values for my network CIDR blocks so the Root module is forced to provide them.&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="c1"&gt;# modules/webserver/variables.tf&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"cluster_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The prefix for all resources (e.g., dev-app, prod-app)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"vpc_cidr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The CIDR block for the VPC"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the module's &lt;code&gt;main.tf&lt;/code&gt;, every resource tag now dynamically references that input:&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="c1"&gt;# modules/webserver/main.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&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;cidr_block&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;vpc_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"${var.cluster_name}-vpc"&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;h3&gt;
  
  
  2. The Calling Pattern
&lt;/h3&gt;

&lt;p&gt;With the blueprint ready, spinning up an entire production-grade environment in &lt;code&gt;prod/main.tf&lt;/code&gt; requires just a few lines of code. The Root module &lt;code&gt;"calls"&lt;/code&gt; the Child module using the &lt;code&gt;source&lt;/code&gt; argument.&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="c1"&gt;# prod/main.tf&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"webserver"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../modules/webserver"&lt;/span&gt; 

  &lt;span class="nx"&gt;cluster_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-app"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.1.0.0/16"&lt;/span&gt; 
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.small"&lt;/span&gt;

  &lt;span class="c1"&gt;# ... subnet maps go here ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Outputs (&lt;code&gt;outputs.tf&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;When resources are buried inside a module, the Root environment cannot automatically see them. If I want to know the DNS name of my Load Balancer after I deploy, the module must explicitly export it.&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="c1"&gt;# modules/webserver/outputs.tf&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"alb_dns_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Root module then catches that output and displays it to the console:&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="c1"&gt;# prod/main.tf&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"production_url"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&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;webserver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb_dns_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Easy vs. Painful Modules: Best Practices
&lt;/h2&gt;

&lt;p&gt;Writing a module is easy. Writing a good module requires architectural foresight.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Avoid Hardcoding with "Sensible Defaults"
&lt;/h3&gt;

&lt;p&gt;A painful module hardcodes configurations. If my module hardcoded the Load Balancer to port &lt;code&gt;80&lt;/code&gt;, and the Dev team needed to test a Node.js app on port &lt;code&gt;8080&lt;/code&gt;, they would be blocked.&lt;/p&gt;

&lt;p&gt;An easy-to-use module provides a sensible default. In my &lt;code&gt;variables.tf&lt;/code&gt;, I set the HTTP port to &lt;code&gt;80&lt;/code&gt; by default. If the user doesn't specify a port, it works out of the box. If they need something custom, they can easily override it in their Root module.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Module Scope: When to Split
&lt;/h3&gt;

&lt;p&gt;Right now, my &lt;code&gt;webserver&lt;/code&gt; module deploys the network (VPC, Subnets, NAT) and the compute layer (ALB, ASG, EC2). For my personal lab, this is fine.&lt;/p&gt;

&lt;p&gt;In the real world, this is an anti-pattern. Networking lifecycles are very different from application lifecycles. Best practice dictates splitting this into two modules: a &lt;code&gt;vpc-network&lt;/code&gt; module and an &lt;code&gt;app-compute&lt;/code&gt; module. This prevents a bad application deployment from accidentally tearing down the company's core routing tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Write a Useful README
&lt;/h3&gt;

&lt;p&gt;A Terraform module without a &lt;code&gt;README.md&lt;/code&gt; is practically useless to another engineer. Your README must act as the API documentation for your infrastructure. It should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An architecture diagram or summary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An exact copy-paste example of how to call the module (&lt;code&gt;module { source = ... }&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A markdown table detailing every required input variable, its type, and its purpose.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Moving from flat files to modules is the threshold where you stop writing scripts and start acting as a true Platform Engineer. You are building guardrails, standardizing deployments, and making it fundamentally easier for your team to safely consume cloud resources.&lt;/p&gt;

&lt;p&gt;You can check out the full refactored code and directory structure on my  &lt;a href="https://github.com/Vivixell/Reusable-Infrastructure" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>iac</category>
      <category>terraform</category>
      <category>eveops</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
