<?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.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>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>
    <item>
      <title>Deploying a Highly Available Web App on AWS Using Terraform</title>
      <dc:creator>Victor Robin</dc:creator>
      <pubDate>Thu, 19 Mar 2026 21:45:16 +0000</pubDate>
      <link>https://dev.to/ovrobin/deploying-a-highly-available-web-app-on-aws-using-terraform-3p7j</link>
      <guid>https://dev.to/ovrobin/deploying-a-highly-available-web-app-on-aws-using-terraform-3p7j</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%2Fmftte8k4ghxxuuwvsog8.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%2Fmftte8k4ghxxuuwvsog8.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Welcome to Day 4 of my 30 Days of Terraform challenge. If you followed along with my &lt;a href="https://dev.to/ovrobin/deploying-your-first-server-with-terraform-a-beginners-guide-e4l"&gt;first article&lt;/a&gt; you remember we deployed a single server into a VPC using terraform. That was a great starting point but it had a massive flaw because a single server is a single point of failure. If that availability zone goes down or the server crashes your application goes offline with it.&lt;/p&gt;

&lt;p&gt;Today we are evolving our infrastructure from a simple hardcoded server into a fully configurable and highly available clustered deployment. We are going to build a custom Virtual Private Cloud block with public and private subnets and place our application servers in the private subnets for security while an Application Load Balancer routes internet traffic to them.&lt;/p&gt;

&lt;p&gt;Let us dive right into the architecture and the code.&lt;/p&gt;

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

&lt;p&gt;In this setup we are building a robust network foundation. We will create a VPC spanning two Availability Zones to ensure redundancy. Our architecture includes an Application Load Balancer residing in the public subnets to receive web traffic and an Auto Scaling Group managing our EC2 instances in the private subnets.&lt;/p&gt;

&lt;p&gt;This means our actual application servers are completely hidden from the direct internet and can only be accessed through the load balancer. We are also utilizing variables and data sources extensively so the code is dynamic and reusable instead of being rigidly hardcoded.&lt;/p&gt;

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

&lt;p&gt;Before you start deploying this architecture you need to make sure your environment is ready.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Terraform installed on your local machine&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS CLI installed and configured with your credentials&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Appropriate IAM permissions configured for your AWS user account&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You must ensure your IAM user or role has the permissions to create VPC resources like subnets and route tables as well as EC2 instances and Load Balancing components. A lack of these permissions will cause the deployment to fail immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step by Step Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Cloning the Repository
&lt;/h3&gt;

&lt;p&gt;You can grab the complete source code for this deployment directly from my GitHub repository to follow along.&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/Highly-Available-Web-App-on-AWS-Using-Terraform.git

&lt;span class="nb"&gt;cd &lt;/span&gt;Highly-Available-Web-App-on-AWS-Using-Terraform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Running the Code
&lt;/h3&gt;

&lt;p&gt;Once you have the code on your local machine the deployment process follows the standard Terraform workflow.&lt;/p&gt;

&lt;p&gt;Initialize the working directory to download the AWS provider plugins:&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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review the execution plan to see exactly what Terraform will create in your AWS account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the configuration to build the infrastructure:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Once the deployment finishes Terraform will output the DNS name of your new Load Balancer so you can paste it into your browser and see the live application.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Understanding the Code
&lt;/h3&gt;

&lt;p&gt;Let us break down the most critical parts of this configuration to understand how it all pieces together.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Power of Variables
&lt;/h4&gt;

&lt;p&gt;Hardcoding values is a bad practice because it forces you to rewrite your core logic whenever a requirement changes. We moved our configurations into variables.tf using maps and objects to keep things organized.&lt;/p&gt;

&lt;p&gt;Here is how we handle our application ports and Auto Scaling capacities dynamically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server_ports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A dictionary mapping application layers to their ports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;port&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="nx"&gt;port&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
      &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Standard web traffic&lt;/span&gt;&lt;span class="dl"&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;variable&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;asg_capacity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Capacity settings for the Auto Scaling Group&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;min&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
    &lt;span class="nx"&gt;max&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
    &lt;span class="nx"&gt;desired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;min&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;max&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="nx"&gt;desired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;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;h4&gt;
  
  
  The Security Groups
&lt;/h4&gt;

&lt;p&gt;We operate on a zero trust model here. The load balancer security group allows public traffic on port 80 but our instance security group explicitly rejects all internet traffic and only accepts connections coming directly from the load balancer security group.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws_security_group&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;instance_sg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;instance-sg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Allow traffic from ALB only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws_vpc_security_group_ingress_rule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;instance_http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_id&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_sg&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;referenced_security_group_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb_sg&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;from_port&lt;/span&gt;                    &lt;span class="o"&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;server_ports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;
  &lt;span class="nx"&gt;to_port&lt;/span&gt;                      &lt;span class="o"&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;server_ports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;
  &lt;span class="nx"&gt;ip_protocol&lt;/span&gt;                  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The Launch Template and Auto Scaling Group
&lt;/h4&gt;

&lt;p&gt;Instead of creating standalone EC2 instances we define a blueprint using a Launch Template. We dynamically fetch the latest Ubuntu AMI and pass a startup script that installs Apache and displays the specific private IP of the server so we can visually confirm the load balancer is working across different instances.&lt;/p&gt;

&lt;p&gt;The Auto Scaling Group then takes this template and ensures we always have our desired number of servers running across our private subnets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws_launch_template&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my_app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name_prefix&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-app-lt-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;image_id&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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="o"&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;vpc_security_group_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_sg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 

  &lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;base64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;EOF&lt;/span&gt;&lt;span class="cp"&gt;
    #!/bin/bash
&lt;/span&gt;    &lt;span class="nx"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;
    &lt;span class="nx"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="nx"&gt;apache2&lt;/span&gt;
    &lt;span class="nx"&gt;systemctl&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="nx"&gt;apache2&lt;/span&gt;
    &lt;span class="nx"&gt;systemctl&lt;/span&gt; &lt;span class="nx"&gt;enable&lt;/span&gt; &lt;span class="nx"&gt;apache2&lt;/span&gt;
    &lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;h1&amp;gt;Hello from OVR Private Subnet! My IP is: $(hostname -I)&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/var/&lt;/span&gt;&lt;span class="nx"&gt;www&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;
  &lt;span class="nx"&gt;EOF&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws_autoscaling_group&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my_asg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;             &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-app-asg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;desired_capacity&lt;/span&gt; &lt;span class="o"&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;asg_capacity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;desired&lt;/span&gt;
  &lt;span class="nx"&gt;max_size&lt;/span&gt;         &lt;span class="o"&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;asg_capacity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;
  &lt;span class="nx"&gt;min_size&lt;/span&gt;         &lt;span class="o"&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;asg_capacity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;

  &lt;span class="nx"&gt;vpc_zone_identifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;subnet&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_private_subnet&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;target_group_arns&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_tg&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="nx"&gt;launch_template&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_launch_template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_app&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;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$Latest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;                 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-asg-instance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;propagate_at_launch&lt;/span&gt; &lt;span class="o"&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;h3&gt;
  
  
  Challenges I Faced
&lt;/h3&gt;

&lt;p&gt;Honestly the actual code writing and deployment went completely smoothly without any logic roadblocks on my end. The modular approach of mapping out the variables and understanding the flow of traffic made the process straightforward and highly predictable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Possible Challenges You Might Have
&lt;/h3&gt;

&lt;p&gt;While the code is solid you might run into environmental issues depending on your local setup.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Network Connectivity Drops:&lt;/strong&gt; You might see an error like lookup sts.us-east-1.amazonaws.com: no such host when running the apply command. This simply means your local machine temporarily lost its connection to the AWS authentication servers usually due to a VPN drop or a local DNS issue.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IAM Permission Errors:&lt;/strong&gt; If you are not using an Administrator account you will need to ensure your user has explicit permissions to create VPCs subnets load balancers and auto scaling groups otherwise AWS will reject the API calls.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Possible Improvements
&lt;/h3&gt;

&lt;p&gt;This architecture is robust but there is always room to grow in cloud engineering. A great next step would be implementing HTTPS by requesting an SSL certificate from AWS Certificate Manager and attaching it to a secure listener on the load balancer. We could also break this monolithic file structure down further by separating the networking resources into a dedicated Terraform module to increase reusability across multiple environments.&lt;/p&gt;

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

&lt;p&gt;Moving from a single instance to a load balanced auto scaling environment is one of the most important leaps you make as a cloud engineer. You are no longer just launching servers but rather designing resilient systems that can survive failures and scale with demand.&lt;/p&gt;

&lt;p&gt;Stay tuned for the next phase of the challenge where we will dive even deeper into infrastructure automation.&lt;/p&gt;

</description>
      <category>terraformchallenge</category>
      <category>hashicorp</category>
      <category>eveops</category>
      <category>iac</category>
    </item>
  </channel>
</rss>
