<?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: Mukami</title>
    <description>The latest articles on DEV Community by Mukami (@tink-origami).</description>
    <link>https://dev.to/tink-origami</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%2F3829683%2Fcc4c3ea0-78ad-491a-82f2-a0508368436b.png</url>
      <title>DEV Community: Mukami</title>
      <link>https://dev.to/tink-origami</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tink-origami"/>
    <language>en</language>
    <item>
      <title>The Importance of Manual Testing in Terraform</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Mon, 30 Mar 2026 14:06:49 +0000</pubDate>
      <link>https://dev.to/tink-origami/the-importance-of-manual-testing-in-terraform-1812</link>
      <guid>https://dev.to/tink-origami/the-importance-of-manual-testing-in-terraform-1812</guid>
      <description>&lt;h2&gt;
  
  
  Why "It Works" Isn't Enough Until You Prove It
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 17 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned that my infrastructure "worked" until I actually tested it.&lt;/p&gt;

&lt;p&gt;I had a webserver cluster. Terraform applied without errors. Everything looked perfect in the AWS Console. I was confident.&lt;/p&gt;

&lt;p&gt;Then I ran a structured manual test. The results were humbling.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Code Success ≠ Functional Success
&lt;/h2&gt;

&lt;p&gt;Terraform told me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 11 resources created&lt;/li&gt;
&lt;li&gt;✅ No errors&lt;/li&gt;
&lt;li&gt;✅ State matches configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But when I actually tried to use my infrastructure:&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="nv"&gt;$ &lt;/span&gt;curl http://my-alb-dns
502 Bad Gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The code worked. The infrastructure didn't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is why manual testing matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Test Checklist
&lt;/h2&gt;

&lt;p&gt;I built a structured test plan covering five categories:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Provisioning Verification
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform init&lt;/code&gt; completes without errors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform validate&lt;/code&gt; passes cleanly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform plan&lt;/code&gt; shows expected resources&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform apply&lt;/code&gt; completes successfully&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Resource Correctness
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Resources visible in AWS Console&lt;/li&gt;
&lt;li&gt;Names match variables&lt;/li&gt;
&lt;li&gt;Tags match expected values&lt;/li&gt;
&lt;li&gt;Security group rules exactly as defined&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Functional Verification
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;ALB DNS resolves&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl&lt;/code&gt; returns expected response&lt;/li&gt;
&lt;li&gt;ASG instances pass health checks&lt;/li&gt;
&lt;li&gt;Instance termination triggers replacement&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. State Consistency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform plan&lt;/code&gt; returns "No changes"&lt;/li&gt;
&lt;li&gt;State file matches AWS resources&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Cleanup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform destroy&lt;/code&gt; completes&lt;/li&gt;
&lt;li&gt;AWS Console verification shows no resources&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;Passed: 12 tests&lt;/strong&gt; ✅&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provisioning worked perfectly&lt;/li&gt;
&lt;li&gt;All resources created with correct tags&lt;/li&gt;
&lt;li&gt;State consistency was perfect&lt;/li&gt;
&lt;li&gt;Destroy cleaned up properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failed: 2 tests&lt;/strong&gt; ❌&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ALB DNS resolution (timeout)&lt;/li&gt;
&lt;li&gt;ALB returned 502 Bad Gateway&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;The infrastructure was created, but the application wasn't working. Why?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ALB DNS takes time to propagate&lt;/strong&gt; — I tested too early&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health checks were failing&lt;/strong&gt; — Instances weren't responding to HTTP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-data script may have failed&lt;/strong&gt; — Apache probably wasn't running&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code was correct. The application was not.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Manual Testing Taught Me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Terraform applies successfully ≠ infrastructure works&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Terraform only checks that resources are created. It doesn't verify that your application is actually running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS propagation is real&lt;/strong&gt; — Just because the ALB exists doesn't mean it's reachable immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health checks are the real indicator&lt;/strong&gt; — A running instance isn't enough. It needs to respond correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cleanup is harder than it looks&lt;/strong&gt; — After &lt;code&gt;terraform destroy&lt;/code&gt;, I found leftover instances. Manual verification is essential.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Value of a Test Checklist
&lt;/h2&gt;

&lt;p&gt;Before today, I'd run &lt;code&gt;terraform apply&lt;/code&gt; and call it done.&lt;/p&gt;

&lt;p&gt;Now I have a checklist that catches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS propagation issues&lt;/li&gt;
&lt;li&gt;Application startup failures&lt;/li&gt;
&lt;li&gt;Health check problems&lt;/li&gt;
&lt;li&gt;Cleanup gaps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each failed test is a gap I can fix and later automate.&lt;/p&gt;




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

&lt;p&gt;After &lt;code&gt;terraform destroy&lt;/code&gt;, I verified with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-instances &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=*test-webserver*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I found five instances still running. Terraform destroyed the ASG but instances were still terminating. Manual verification caught what automation missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Always verify cleanup. Don't trust &lt;code&gt;destroy&lt;/code&gt; alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Manual Test Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;terraform init&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;terraform validate&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;terraform plan&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;terraform apply&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resources in AWS&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags correct&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security group rules&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALB DNS resolution&lt;/td&gt;
&lt;td&gt;❌ FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALB returns webpage&lt;/td&gt;
&lt;td&gt;❌ FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASG instances running&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State consistency&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;terraform destroy&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleanup verification&lt;/td&gt;
&lt;td&gt;✅ PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;12 passed, 2 failed.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Manual testing isn't about checking boxes. It's about finding gaps before they become outages.&lt;/p&gt;

&lt;p&gt;If I had deployed this infrastructure without testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users would see 502 errors&lt;/li&gt;
&lt;li&gt;I'd be debugging under pressure&lt;/li&gt;
&lt;li&gt;The problem would take longer to find&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, I found the failure in a controlled environment. I can now fix it and write automated tests to prevent it from happening again.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Big Lesson
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Terraform applies successfully ≠ Infrastructure works&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The gap between "code success" and "functional success" is where outages happen. Manual testing closes that gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Fix the user-data script to ensure Apache starts reliably&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;wait_for_capacity_timeout&lt;/code&gt; to ASG&lt;/li&gt;
&lt;li&gt;Wait 2-3 minutes after apply before testing&lt;/li&gt;
&lt;li&gt;Write automated tests to catch these issues in CI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;P.S. The 502 Bad Gateway was humbling. But finding it manually before deployment was a win. Test early, test often, test manually before you automate.&lt;/em&gt; 🚀&lt;/p&gt;

</description>
      <category>testing</category>
      <category>aws</category>
      <category>terraform</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Creating Production-Grade Infrastructure with Terraform</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Mon, 30 Mar 2026 07:21:58 +0000</pubDate>
      <link>https://dev.to/tink-origami/creating-production-grade-infrastructure-with-terraform-3b1p</link>
      <guid>https://dev.to/tink-origami/creating-production-grade-infrastructure-with-terraform-3b1p</guid>
      <description>&lt;h2&gt;
  
  
  The Gap Between "It Works" and "It's Production-Ready"
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 16 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned that my "working" infrastructure was nowhere near production-ready.&lt;/p&gt;

&lt;p&gt;I had a webserver cluster. It deployed. It served traffic. I was proud of it.&lt;/p&gt;

&lt;p&gt;Then I ran it against a production-grade checklist. The result was humbling.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Production-Grade Checklist
&lt;/h2&gt;

&lt;p&gt;I audited my infrastructure against 5 categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;What I Was Missing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Structure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;Some hardcoded values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reliability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;prevent_destroy&lt;/code&gt;, no wait timeouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;td&gt;SSH open to world, no validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;No consistent tags, no alarms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintainability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;Missing input validation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap was significant. Here's how I closed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Refactor 1: Consistent Tagging
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; Tags scattered across resources, inconsistent.&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;tags&lt;/span&gt; &lt;span class="err"&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}-instance"&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; Centralized tags with &lt;code&gt;locals&lt;/code&gt; and &lt;code&gt;merge()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;common_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;Environment&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;environment&lt;/span&gt;
    &lt;span class="nx"&gt;ManagedBy&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Terraform"&lt;/span&gt;
    &lt;span class="nx"&gt;Project&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;project_name&lt;/span&gt;
    &lt;span class="nx"&gt;Team&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;team_name&lt;/span&gt;
    &lt;span class="nx"&gt;Day&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"16"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;common_tags&lt;/span&gt;&lt;span class="err"&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}-alb"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every resource has the same base tags. Cost allocation, ownership tracking, and operations all benefit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Refactor 2: Lifecycle Protection
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; No protection against accidental deletion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; Added &lt;code&gt;prevent_destroy&lt;/code&gt; to critical resources.&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_lb"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# ... config ...&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;prevent_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# Can't accidentally delete ALB&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;Without this, one wrong &lt;code&gt;terraform destroy&lt;/code&gt; wipes production. With it, Terraform errors before doing damage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Refactor 3: CloudWatch Alarms
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; No monitoring. If CPU spiked, I wouldn't know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; Alarms that notify via SNS.&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_sns_topic"&lt;/span&gt; &lt;span class="s2"&gt;"alerts"&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}-alerts"&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_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;alarm_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.cluster_name}-high-cpu"&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;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;alarm_description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CPU exceeds 80% for 4 minutes"&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;web&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;p&gt;Now when CPU hits 80% for 4 minutes, I get an alert. I can scale before users notice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Refactor 4: Input Validation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; Any value was accepted. &lt;code&gt;environment = "prod"&lt;/code&gt; would work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; Validation blocks catch mistakes early.&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;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;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;"production"&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;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 dev, staging, or production."&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="s2"&gt;"instance_type"&lt;/span&gt; &lt;span class="p"&gt;{&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;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"^t[23]&lt;/span&gt;&lt;span class="err"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&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;"Instance type must be a t2 or t3 family type."&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;Try &lt;code&gt;terraform plan -var="environment=prod"&lt;/code&gt; (missing "uction"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Invalid value for variable
Environment must be dev, staging, or production.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caught at plan time, not after deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Refactor 5: ASG Wait Timeout
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; No &lt;code&gt;wait_for_capacity_timeout&lt;/code&gt; — Terraform would move on before instances were healthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; Added patience.&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_autoscaling_group"&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;health_check_grace_period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
  &lt;span class="nx"&gt;wait_for_capacity_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10m"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Terraform waits up to 10 minutes for instances to pass health checks before destroying old ones. Critical for zero-downtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Before and After
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tags&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inconsistent&lt;/td&gt;
&lt;td&gt;Centralized, all resources tagged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deletion Protection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;prevent_destroy&lt;/code&gt; on ALB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monitoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;CloudWatch alarms + SNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Validation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;All variables validated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ASG Wait&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;10-minute timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSH Access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;td&gt;Restricted (configurable)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;&lt;strong&gt;Production-grade isn't about features. It's about resilience.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My code "worked." But it wouldn't survive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A bad &lt;code&gt;terraform destroy&lt;/code&gt; command&lt;/li&gt;
&lt;li&gt;A CPU spike at 3 AM&lt;/li&gt;
&lt;li&gt;A teammate typing "prod" instead of "production"&lt;/li&gt;
&lt;li&gt;An instance taking 2 minutes to boot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each refactor addresses a failure mode I hadn't considered.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Checklist Matters
&lt;/h2&gt;

&lt;p&gt;The production-grade checklist isn't just a list. It's a map of failure modes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tagging&lt;/strong&gt; → Who owns this? Who pays for it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;prevent_destroy&lt;/code&gt;&lt;/strong&gt; → What happens if I fat-finger this?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alarms&lt;/strong&gt; → How will I know something is wrong?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation&lt;/strong&gt; → What if someone passes wrong values?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts&lt;/strong&gt; → What if things take longer than expected?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every checkbox answers a "what if" question.&lt;/p&gt;




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

&lt;p&gt;Today I transformed "working" infrastructure into "production-ready" infrastructure.&lt;/p&gt;

&lt;p&gt;The difference isn't features. It's resilience, observability, and safety.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're deploying code that matters, run it through a production checklist.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. The moment I added &lt;code&gt;prevent_destroy&lt;/code&gt; to my ALB, I felt safer. The moment I added validation, I felt smarter. The moment I added alarms, I felt like a real engineer. Small changes, big impact.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>terraform</category>
      <category>30daychallenge</category>
      <category>aws</category>
    </item>
    <item>
      <title>Deploying Multi-Cloud Infrastructure with Terraform Modules</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Sun, 29 Mar 2026 09:21:41 +0000</pubDate>
      <link>https://dev.to/tink-origami/deploying-multi-cloud-infrastructure-with-terraform-modules-hln</link>
      <guid>https://dev.to/tink-origami/deploying-multi-cloud-infrastructure-with-terraform-modules-hln</guid>
      <description>&lt;h2&gt;
  
  
  From S3 Buckets to EKS Clusters — All in One Configuration
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 15 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned that Terraform isn't just for AWS. It's for everything.&lt;/p&gt;

&lt;p&gt;One configuration. Multiple providers. S3 buckets across regions. Docker containers locally. A full Kubernetes cluster on EKS. All from the same tool.&lt;/p&gt;

&lt;p&gt;Here's how it all came together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Multi-Provider Modules
&lt;/h2&gt;

&lt;p&gt;The first challenge: creating a module that works across multiple AWS regions.&lt;/p&gt;

&lt;p&gt;Modules can't hardcode providers. That would break reusability. Instead, they must accept provider configurations from the caller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The module (no provider block inside):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;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&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.app_name}-primary"&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;"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;replica&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.app_name}-replica"&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;The caller (provides the providers):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;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;"primary"&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;"eu-north-1"&lt;/span&gt;
&lt;span class="p"&gt;}&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;"replica"&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;"eu-west-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;"multi_region_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/multi-region-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;primary&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;replica&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is how Terraform scales to global infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Docker Provider — Local Testing
&lt;/h2&gt;

&lt;p&gt;Before deploying to Kubernetes, I tested locally with Docker:&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;docker&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;"kreuzwerker/docker"&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; 3.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;span class="nx"&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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&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;One &lt;code&gt;terraform apply&lt;/code&gt; later:&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="nv"&gt;$ &lt;/span&gt;docker ps
CONTAINER ID   IMAGE          COMMAND                  PORTS                  NAMES
2ea179f7333b   nginx:latest   &lt;span class="s2"&gt;"/docker-entrypoint.…"&lt;/span&gt;   0.0.0.0:8080-&amp;gt;80/tcp   terraform-nginx

&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:8080
&amp;lt;&lt;span class="o"&gt;!&lt;/span&gt;DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;&lt;span class="nb"&gt;head&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A container running on my machine, provisioned entirely by Terraform. No Docker commands. No manual setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: EKS Cluster — The Big One
&lt;/h2&gt;

&lt;p&gt;This was the most complex deployment yet. An entire Kubernetes cluster on AWS EKS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC first (using community module):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&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/vpc/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;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eks-vpc"&lt;/span&gt;
  &lt;span class="nx"&gt;cidr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;

  &lt;span class="nx"&gt;azs&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eu-north-1a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"eu-north-1b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"eu-north-1c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;private_subnets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.2.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.3.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;public_subnets&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.101.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.102.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.103.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;enable_nat_gateway&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Then the EKS cluster:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;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;"terraform-challenge-cluster"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.29"&lt;/span&gt;

  &lt;span class="nx"&gt;vpc_id&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;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_ids&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;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnets&lt;/span&gt;

  &lt;span class="nx"&gt;eks_managed_node_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;min_size&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="nx"&gt;max_size&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
      &lt;span class="nx"&gt;desired_size&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;instance_types&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"t3.small"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Kubernetes provider (authenticates using AWS token):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

  &lt;span class="nx"&gt;exec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;api_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"client.authentication.k8s.io/v1beta1"&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws"&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="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;exec&lt;/code&gt; block runs &lt;code&gt;aws eks get-token&lt;/code&gt; to generate a temporary authentication token. No hardcoded credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: Deploying to Kubernetes
&lt;/h2&gt;

&lt;p&gt;With the cluster running, I deployed nginx:&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;"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;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="nx"&gt;labels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt; &lt;span class="p"&gt;}&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="nx"&gt;selector&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;match_labels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;template&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;labels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt; &lt;span class="p"&gt;}&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;container&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="s2"&gt;"nginx:latest"&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"&lt;/span&gt;
          &lt;span class="nx"&gt;port&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;container_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&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="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;"kubernetes_service"&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;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-service"&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;selector&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&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;port&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;target_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&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="s2"&gt;"LoadBalancer"&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;
  
  
  Part 5: The Moment It Worked
&lt;/h2&gt;

&lt;p&gt;After 8 minutes of cluster provisioning (felt like an eternity), the nodes appeared:&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="nv"&gt;$ &lt;/span&gt;kubectl get nodes
NAME                                          STATUS   ROLES    AGE   VERSION
ip-10-0-1-219.eu-north-1.compute.internal     Ready    &amp;lt;none&amp;gt;   21m   v1.29
ip-10-0-2-67.eu-north-1.compute.internal      Ready    &amp;lt;none&amp;gt;   21m   v1.29
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the nginx pods:&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="nv"&gt;$ &lt;/span&gt;kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
nginx-xxxxxxxxxx-xxxxx   1/1     Running   0          30s
nginx-xxxxxxxxxx-yyyyy   1/1     Running   0          30s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, the LoadBalancer:&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="nv"&gt;$ &lt;/span&gt;kubectl get service nginx
NAME    TYPE           EXTERNAL-IP
nginx   LoadBalancer   a4410db0bc9904a48978a65e7108ee18-2037003514.eu-north-1.elb.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A publicly accessible nginx server, running on Kubernetes, provisioned entirely by Terraform.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Modules must accept providers.&lt;/strong&gt; You can't hardcode regions inside a reusable module. Use &lt;code&gt;configuration_aliases&lt;/code&gt; and pass providers from the root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Docker provider is great for local testing.&lt;/strong&gt; Before deploying to EKS, I tested the same container image locally. Saved time and money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EKS takes time.&lt;/strong&gt; 8-10 minutes for the control plane. Another 2-3 minutes for nodes. Patience is required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RBAC is the final hurdle.&lt;/strong&gt; Even with the cluster running, your IAM user needs explicit permissions via Access Entry and policy association.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One tool, many providers.&lt;/strong&gt; AWS, Docker, Kubernetes — all from the same Terraform configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cost Warning
&lt;/h2&gt;

&lt;p&gt;EKS isn't free. A cluster costs ~$0.10/hour plus EC2 nodes (~$0.04/hour each). My 2-hour test cost about $0.50. Always destroy when done.&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;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Today I deployed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 buckets in two AWS regions (using provider aliases)&lt;/li&gt;
&lt;li&gt;A Docker container locally (using Docker provider)&lt;/li&gt;
&lt;li&gt;A full EKS cluster with 2 nodes (using AWS EKS module)&lt;/li&gt;
&lt;li&gt;Nginx pods on Kubernetes (using Kubernetes provider)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All from one Terraform configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is why I love Terraform. One language. One workflow. Every cloud.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>eks</category>
      <category>terraform</category>
      <category>30daychallenge</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Getting Started with Multiple Providers in Terraform</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Sun, 29 Mar 2026 08:07:33 +0000</pubDate>
      <link>https://dev.to/tink-origami/getting-started-with-multiple-providers-in-terraform-5g54</link>
      <guid>https://dev.to/tink-origami/getting-started-with-multiple-providers-in-terraform-5g54</guid>
      <description>&lt;h2&gt;
  
  
  How to Deploy Across Multiple AWS Regions Without Losing Your Mind
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 14 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned that Terraform isn't limited to one region or one account.&lt;/p&gt;

&lt;p&gt;One provider config. Multiple regions. Multiple accounts. Same codebase.&lt;/p&gt;

&lt;p&gt;Here's how it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is a Provider?
&lt;/h2&gt;

&lt;p&gt;A provider is a plugin that translates Terraform code into API calls. The AWS provider knows how to create S3 buckets. The Kubernetes provider knows how to create pods. The random provider generates... random stuff.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;terraform init&lt;/code&gt;, Terraform downloads these plugins from the Terraform Registry. No manual installation. No hunting for binaries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pinning Provider Versions
&lt;/h2&gt;

&lt;p&gt;Never leave your provider version blank. That's how things break unexpectedly.&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.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; 5.0"&lt;/span&gt;  &lt;span class="c1"&gt;# Any 5.x, but not 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;p&gt;The &lt;code&gt;~&amp;gt; 5.0&lt;/code&gt; constraint means: use version 5.0 or higher, but less than 6.0. You get bug fixes and security patches, but no breaking changes.&lt;/p&gt;

&lt;p&gt;After &lt;code&gt;terraform init&lt;/code&gt;, check &lt;code&gt;.terraform.lock.hcl&lt;/code&gt;. It records the exact version downloaded. &lt;strong&gt;Commit this file to Git.&lt;/strong&gt; It ensures your whole team uses the same provider version.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Default Provider
&lt;/h2&gt;

&lt;p&gt;A basic provider config applies to every resource in your configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"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;"eu-north-1"&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;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-bucket"&lt;/span&gt;  &lt;span class="c1"&gt;# Deploys in eu-north-1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform uses this default provider for every resource that doesn't specify otherwise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multiple Regions: Provider Aliases
&lt;/h2&gt;

&lt;p&gt;Want resources in two regions? Define an alias:&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;"eu-north-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;"ireland"&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;"eu-west-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Aliased provider — tertiary 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;"frankfurt"&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;"eu-central-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can deploy resources anywhere:&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;# Uses default provider (eu-north-1)&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;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"primary-bucket"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Uses ireland alias&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;"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;ireland&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"replica-bucket"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Uses frankfurt alias&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;"backup"&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;frankfurt&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backup-bucket"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform knows exactly which API endpoint to call for each resource.&lt;/p&gt;




&lt;h2&gt;
  
  
  S3 Cross-Region Replication Example
&lt;/h2&gt;

&lt;p&gt;Here's a practical use case: replicate data across regions for disaster recovery.&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;# Primary bucket in eu-north-1&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;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app-primary-data"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Replica bucket in eu-west-1&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;"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;ireland&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app-replica-data"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Replication configuration&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_replication_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"replication"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&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;id&lt;/span&gt;

  &lt;span class="nx"&gt;rule&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="s2"&gt;"replicate-all"&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;

    &lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;bucket&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&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;arn&lt;/span&gt;
      &lt;span class="nx"&gt;storage_class&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"STANDARD"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every file uploaded to the primary bucket is automatically replicated to Ireland. If eu-north-1 goes down, your data is safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multiple AWS Accounts
&lt;/h2&gt;

&lt;p&gt;For multi-account setups, use &lt;code&gt;assume_role&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;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;"prod"&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;"eu-north-1"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::111111111111:role/TerraformDeployRole"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&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;"staging"&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;"eu-north-1"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::222222222222:role/TerraformDeployRole"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IAM role in each account needs permissions to create the resources you're managing. Terraform assumes the role, performs the operations, then drops the credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lock File Explained
&lt;/h2&gt;

&lt;p&gt;After &lt;code&gt;terraform init&lt;/code&gt;, you get &lt;code&gt;.terraform.lock.hcl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"registry.terraform.io/hashicorp/aws"&lt;/span&gt; &lt;span class="p"&gt;{&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;"5.100.0"&lt;/span&gt;      &lt;span class="c1"&gt;# Exact version installed&lt;/span&gt;
  &lt;span class="nx"&gt;constraints&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="c1"&gt;# Your version constraint&lt;/span&gt;
  &lt;span class="nx"&gt;hashes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"h1:abc123def456..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# Checksum for verification&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 commit this file?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Everyone uses the same provider version&lt;/li&gt;
&lt;li&gt;No "works on my machine" problems&lt;/li&gt;
&lt;li&gt;Hashes verify downloads haven't been tampered with&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Chapter 7 Learnings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What happens during &lt;code&gt;terraform init&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads your &lt;code&gt;required_providers&lt;/code&gt; block&lt;/li&gt;
&lt;li&gt;Downloads provider binaries from the Terraform Registry&lt;/li&gt;
&lt;li&gt;Records exact versions in &lt;code&gt;.terraform.lock.hcl&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;version&lt;/code&gt; vs &lt;code&gt;~&amp;gt; version&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;version = "5.0"&lt;/code&gt; — exact version only&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~&amp;gt; 5.0&lt;/code&gt; — any 5.x version, but not 6.0 (allows patches)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How Terraform chooses a provider:&lt;/strong&gt; Uses the default provider unless you specify &lt;code&gt;provider = alias&lt;/code&gt; in the resource.&lt;/p&gt;




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

&lt;p&gt;Provider aliases unlock multi-region infrastructure. One configuration can now deploy globally.&lt;/p&gt;

&lt;p&gt;The lock file is your friend. Commit it. It prevents version drift across your team.&lt;/p&gt;

&lt;p&gt;Multi-account deployments are just aliases with &lt;code&gt;assume_role&lt;/code&gt;. Same pattern, different accounts.&lt;/p&gt;




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

&lt;p&gt;Terraform isn't limited to one region or one account. With provider aliases, you can deploy anywhere.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multiple regions&lt;/td&gt;
&lt;td&gt;Provider aliases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple accounts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;assume_role&lt;/code&gt; + aliases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version consistency&lt;/td&gt;
&lt;td&gt;Pin versions + commit lock file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;One configuration. Global infrastructure.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. The lock file seems like a small detail, but it's saved me from "but it worked on my laptop" conversations more times than I can count. Commit it.&lt;/em&gt; 🔒&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>providers</category>
      <category>30daychallenge</category>
    </item>
    <item>
      <title>How to Handle Sensitive Data Securely in Terraform</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Sat, 28 Mar 2026 13:15:59 +0000</pubDate>
      <link>https://dev.to/tink-origami/how-to-handle-sensitive-data-securely-in-terraform-4cci</link>
      <guid>https://dev.to/tink-origami/how-to-handle-sensitive-data-securely-in-terraform-4cci</guid>
      <description>&lt;h2&gt;
  
  
  The Three Ways Secrets Leak (And How to Stop Every One)
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 13 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned that even when you think your secrets are safe, they're probably not.&lt;/p&gt;

&lt;p&gt;Secrets leak in Terraform in three predictable ways. I found all three. I fixed all three. And I documented what I learned so you don't have to make the same mistakes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Leak Paths
&lt;/h2&gt;

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

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"super-secret-password"&lt;/span&gt;  &lt;span class="c1"&gt;# ❌&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This password is now in your Git history. Forever. Even if you delete it, it's still in the commit history. Anyone with access to your repo can see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"db_password"&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="nx"&gt;sensitive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="c1"&gt;# No default — Terraform will prompt&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the password never touches your code.&lt;/p&gt;




&lt;h3&gt;
  
  
  Leak Path 2: Variable Defaults
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"db_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"super-secret-password"&lt;/span&gt;  &lt;span class="c1"&gt;# ❌ Still in code&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default values are stored in your &lt;code&gt;.tf&lt;/code&gt; files. Same problem as hardcoding. The secret is right there in version control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; No defaults for secrets. Ever.&lt;/p&gt;




&lt;h3&gt;
  
  
  Leak Path 3: The State File
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The reality:&lt;/strong&gt; Even if you fix the first two, Terraform stores &lt;strong&gt;every resource attribute&lt;/strong&gt; in &lt;code&gt;terraform.tfstate&lt;/code&gt; in plaintext.&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;terraform.tfstate | &lt;span class="nb"&gt;grep &lt;/span&gt;password
&lt;span class="s2"&gt;"password"&lt;/span&gt;: &lt;span class="s2"&gt;"MySecurePassword123!"&lt;/span&gt;  &lt;span class="c"&gt;# ❌ Right there!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anyone with read access to the state file can see all your secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use remote state with encryption (S3 + &lt;code&gt;encrypt = true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Restrict access with IAM policies&lt;/li&gt;
&lt;li&gt;Never commit state to Git&lt;/li&gt;
&lt;li&gt;Enable versioning to recover from mistakes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  AWS Secrets Manager: The Right Way
&lt;/h2&gt;

&lt;p&gt;Instead of hardcoding, store secrets in AWS Secrets Manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret"&lt;/span&gt; &lt;span class="s2"&gt;"db_credentials"&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;"prod/db/credentials"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret_version"&lt;/span&gt; &lt;span class="s2"&gt;"db_credentials"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;secret_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_credentials&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;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db_credentials&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsondecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_secretsmanager_secret_version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret_string&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_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_credentials&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="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_credentials&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this gives you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Secrets never appear in your &lt;code&gt;.tf&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;✅ Fetched at runtime&lt;/li&gt;
&lt;li&gt;✅ Centralized secret management&lt;/li&gt;
&lt;li&gt;✅ Rotation capabilities&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Marking Secrets as Sensitive
&lt;/h2&gt;

&lt;p&gt;Terraform's &lt;code&gt;sensitive = true&lt;/code&gt; prevents secrets from appearing in terminal output:&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;output&lt;/span&gt; &lt;span class="s2"&gt;"db_password"&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;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_credentials&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="nx"&gt;sensitive&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Plan output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;db_password = &amp;lt;sensitive&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ &lt;strong&gt;Important:&lt;/strong&gt; This does NOT prevent secrets from being stored in state. It only hides them from the terminal.&lt;/p&gt;




&lt;h2&gt;
  
  
  State File Security Checklist
&lt;/h2&gt;

&lt;p&gt;After deploying, I verified all of these:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;S3 backend with encryption:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-terraform-state"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# AES-256 encryption&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;Block public access&lt;/strong&gt; on S3 bucket&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Enable versioning&lt;/strong&gt; to recover from mistakes&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Restrict IAM policy&lt;/strong&gt; to only Terraform runners&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;.gitignore&lt;/strong&gt; to prevent accidental commits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.terraform/
*.tfstate
*.tfstate.backup
*.tfvars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What the Plan Looked Like
&lt;/h2&gt;

&lt;p&gt;When I ran &lt;code&gt;terraform plan&lt;/code&gt;, the output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;(sensitive value)&lt;/span&gt;
&lt;span class="py"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;(sensitive value)&lt;/span&gt;
&lt;span class="py"&gt;db_password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;(sensitive value)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual secrets never appeared in my terminal. But when I checked the state file:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;terraform.tfstate | &lt;span class="nb"&gt;grep &lt;/span&gt;password
&lt;span class="s2"&gt;"password"&lt;/span&gt;: &lt;span class="s2"&gt;"MySecurePassword123!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The password was right there in plaintext.&lt;/strong&gt; This was the "aha!" moment — state encryption isn't optional. It's mandatory.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 6 Learnings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;sensitive = true&lt;/code&gt; prevent secrets from being stored in state?&lt;/strong&gt;&lt;br&gt;
No. It only hides them from terminal output. Secrets are still in state. You must secure the state file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vault vs Secrets Manager:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Secrets Manager:&lt;/strong&gt; AWS-native, simpler, good for AWS-only environments, integrates with RDS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HashiCorp Vault:&lt;/strong&gt; Multi-cloud, dynamic secrets, fine-grained policies, more complex to set up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why secrets appear in state:&lt;/strong&gt; Terraform stores all resource attributes to track infrastructure. The only solution is to secure the state file itself with encryption and access controls.&lt;/p&gt;




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

&lt;p&gt;Secrets management isn't just about not hardcoding passwords. It's about:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Using a secrets manager&lt;/strong&gt; — AWS Secrets Manager, HashiCorp Vault&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never hardcoding&lt;/strong&gt; — no passwords in &lt;code&gt;.tf&lt;/code&gt; files, no defaults&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marking sensitive outputs&lt;/strong&gt; — &lt;code&gt;sensitive = true&lt;/code&gt; hides from terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Securing the state file&lt;/strong&gt; — encrypted S3 backend, restricted access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proper .gitignore&lt;/strong&gt; — never commit state or tfvars&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The state file is the last line of defence. Secure it like you would a password manager.&lt;/p&gt;




&lt;h2&gt;
  
  
  My .gitignore
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Terraform
.terraform/
.terraform.lock.hcl
*.tfstate
*.tfstate.backup
*.tfvars
*.tfvars.json

# Local secrets
*.secret
secrets.tfvars

# Environment
.env
.env.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;Three leak paths. Three fixes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Leak Path&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hardcoded in .tf&lt;/td&gt;
&lt;td&gt;Use variables with &lt;code&gt;sensitive = true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variable defaults&lt;/td&gt;
&lt;td&gt;Remove defaults, use Secrets Manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State file&lt;/td&gt;
&lt;td&gt;Encrypted remote backend, restricted access&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Security isn't optional in production infrastructure.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/secretsmanager/" rel="noopener noreferrer"&gt;AWS Secrets Manager Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/values/outputs#sensitive-suppressing-values-in-cli-output" rel="noopener noreferrer"&gt;Terraform Sensitive Values&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state/sensitive-data" rel="noopener noreferrer"&gt;Protecting Sensitive Data in State&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;P.S. The moment I saw my password in the state file was humbling. No matter how careful you are with your code, if you don't secure your state, you're leaving secrets in plain sight.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>security</category>
    </item>
    <item>
      <title>Mastering Zero-Downtime Deployments with Terraform</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Sat, 28 Mar 2026 11:58:27 +0000</pubDate>
      <link>https://dev.to/tink-origami/mastering-zero-downtime-deployments-with-terraform-kem</link>
      <guid>https://dev.to/tink-origami/mastering-zero-downtime-deployments-with-terraform-kem</guid>
      <description>&lt;h2&gt;
  
  
  How I Updated Production Without Taking My App Offline (Almost)
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 12 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned the difference between "it works" and "it works without anyone noticing."&lt;/p&gt;

&lt;p&gt;Deploying infrastructure updates without downtime is one of the hardest problems in operations. It's also one of the most valuable skills you can have. Today I made it happen.&lt;/p&gt;

&lt;p&gt;Well, almost. Let me show you what worked, what broke, and what I learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Terraform's Default Behavior is Destructive
&lt;/h2&gt;

&lt;p&gt;When you update a Launch Template or Auto Scaling Group, Terraform does something terrifying by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Destroy old ASG → All instances terminate → Website goes down ❌
2. Create new ASG → New instances spin up → Website comes back
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Between step 1 and step 2, there's a window of silence. Sometimes seconds. Sometimes minutes. In production, that's a disaster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix?&lt;/strong&gt; A little lifecycle rule called &lt;code&gt;create_before_destroy&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: create_before_destroy
&lt;/h2&gt;

&lt;p&gt;This simple lifecycle block flips the order:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of destroy → create, it does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Create new resources
2. Wait for them to be healthy
3. Destroy old resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No gap. No downtime.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The ASG Naming Problem
&lt;/h2&gt;

&lt;p&gt;There's a catch. AWS won't let two Auto Scaling Groups have the same name. When &lt;code&gt;create_before_destroy&lt;/code&gt; creates the new ASG before destroying the old one, they'd both have the same name and Terraform would error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use &lt;code&gt;name_prefix&lt;/code&gt; instead of a fixed name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;name_prefix&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"web-asg-${random_id.asg.hex}-"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;random_id&lt;/code&gt; that changes when your code changes:&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;"random_id"&lt;/span&gt; &lt;span class="s2"&gt;"asg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;keepers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# New ID when user_data changes&lt;/span&gt;
    &lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${path.module}/user-data.sh"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;byte_length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each deployment gets a unique name. Problem solved.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Test: Updating from v1 to v2
&lt;/h2&gt;

&lt;p&gt;I set up a traffic monitor in one terminal:&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="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl http://my-alb-dns-name
  &lt;span class="nb"&gt;sleep &lt;/span&gt;2
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another terminal, I updated my &lt;code&gt;user_data&lt;/code&gt; script and ran &lt;code&gt;terraform apply&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's what happened:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Version 1: Hello World!&amp;lt;/h1&amp;gt; - 14:34:04
&amp;lt;h1&amp;gt;Version 1: Hello World!&amp;lt;/h1&amp;gt; - 14:34:06
&amp;lt;h1&amp;gt;Version 1: Hello World!&amp;lt;/h1&amp;gt; - 14:34:08
...
502 Bad Gateway - 14:35:08    # Uh oh!
502 Bad Gateway - 14:35:10
502 Bad Gateway - 14:35:12
...
&amp;lt;h1&amp;gt;Version 2: Updated! Zero Downtime Works!&amp;lt;/h1&amp;gt; - 14:35:25
&amp;lt;h1&amp;gt;Version 2: Updated! Zero Downtime Works!&amp;lt;/h1&amp;gt; - 14:35:27
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What happened?&lt;/strong&gt; I got 17 seconds of 502 errors. Not quite zero downtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Did I Get Errors?
&lt;/h2&gt;

&lt;p&gt;The new instances took longer to become healthy than the old instances took to terminate. The ALB had no healthy targets for a brief window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Increase &lt;code&gt;health_check_grace_period&lt;/code&gt; and add &lt;code&gt;wait_for_capacity_timeout&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_autoscaling_group"&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;health_check_grace_period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;   &lt;span class="c1"&gt;# 5 minutes to boot&lt;/span&gt;
  &lt;span class="nx"&gt;wait_for_capacity_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10m"&lt;/span&gt; &lt;span class="c1"&gt;# Wait for healthy instances&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;p&gt;With these settings, Terraform waits until the new instances pass health checks before destroying the old ones. No more 502s.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Blue/Green Alternative
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;create_before_destroy&lt;/code&gt; is great, but it has limitations. For mission-critical apps, teams use &lt;strong&gt;blue/green deployments&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blue&lt;/strong&gt; = current live environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Green&lt;/strong&gt; = new version, ready to go&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Switch traffic instantly with a listener rule:&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_lb_listener_rule"&lt;/span&gt; &lt;span class="s2"&gt;"blue_green"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;action&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="s2"&gt;"forward"&lt;/span&gt;
    &lt;span class="nx"&gt;target_group_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active_environment&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"blue"&lt;/span&gt; &lt;span class="err"&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;blue&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="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;green&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;p&gt;Change &lt;code&gt;active_environment = "green"&lt;/code&gt;, run &lt;code&gt;terraform apply&lt;/code&gt;, and traffic switches in a single API call. Zero downtime. Instant rollback.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;The default is dangerous.&lt;/strong&gt; Never assume Terraform will keep your app online. Always test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;create_before_destroy&lt;/code&gt; is your friend.&lt;/strong&gt; But you need to handle ASG naming with &lt;code&gt;name_prefix&lt;/code&gt; and &lt;code&gt;random_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health checks matter.&lt;/strong&gt; Give your instances time to boot. &lt;code&gt;health_check_grace_period = 300&lt;/code&gt; is your safety net.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blue/green is cleaner.&lt;/strong&gt; More setup, but atomic switches and instant rollbacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;502s taught me more than success would have.&lt;/strong&gt; I saw exactly where my setup failed and how to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Code That Worked
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Unique ASG name&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"random_id"&lt;/span&gt; &lt;span class="s2"&gt;"asg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;keepers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user-data.sh"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;byte_length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&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_autoscaling_group"&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;name_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"web-asg-${random_id.asg.hex}-"&lt;/span&gt;
  &lt;span class="nx"&gt;health_check_grace_period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
  &lt;span class="nx"&gt;wait_for_capacity_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10m"&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;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_launch_template"&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;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;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Zero-downtime deployments aren't magic. They're a combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The right lifecycle rules (&lt;code&gt;create_before_destroy&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Smart naming (&lt;code&gt;name_prefix&lt;/code&gt; + &lt;code&gt;random_id&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Patience (&lt;code&gt;health_check_grace_period&lt;/code&gt;, &lt;code&gt;wait_for_capacity_timeout&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The right strategy (blue/green for critical apps)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Today I learned that "it works" isn't enough. It has to work when nobody's watching. And now I know how to make that happen.
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;P.S. Those 502 errors were humbling. But they taught me more than a perfect run ever would. Sometimes failure is the best teacher.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How Conditionals Make Terraform Infrastructure Dynamic and Efficient</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Fri, 27 Mar 2026 13:26:35 +0000</pubDate>
      <link>https://dev.to/tink-origami/how-conditionals-make-terraform-infrastructure-dynamic-and-efficient-4m0d</link>
      <guid>https://dev.to/tink-origami/how-conditionals-make-terraform-infrastructure-dynamic-and-efficient-4m0d</guid>
      <description>&lt;h2&gt;
  
  
  One Module, Two Environments, Zero Code Duplication
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 11 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned that conditionals are the secret sauce that turns a rigid configuration into a flexible, environment-aware system.&lt;/p&gt;

&lt;p&gt;Yesterday I had a module that worked for dev. Today I have a module that works for dev, staging, AND production — all from the same codebase.&lt;/p&gt;

&lt;p&gt;No copy-pasting. No separate branches. Just one &lt;code&gt;environment&lt;/code&gt; variable driving everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Hardcoded Values for Every Environment
&lt;/h2&gt;

&lt;p&gt;Before today, if I wanted different instance sizes for dev and production, I had to:&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;# dev/main.tf&lt;/span&gt;
&lt;span class="nx"&gt;instance_type&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="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nx"&gt;max_size&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="c1"&gt;# production/main.tf&lt;/span&gt;
&lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.medium"&lt;/span&gt;
&lt;span class="nx"&gt;min_size&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="nx"&gt;max_size&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same code. Different files. Change something? Update both places. Forget one? Inconsistent infrastructure.&lt;/p&gt;

&lt;p&gt;This works until you have 5 environments. Then it becomes a maintenance nightmare.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Centralized Conditional Logic with Locals
&lt;/h2&gt;

&lt;p&gt;Instead of scattering &lt;code&gt;? :&lt;/code&gt; operators everywhere, I put all my conditional decisions in one place:&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;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# Environment checks&lt;/span&gt;
  &lt;span class="nx"&gt;is_production&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;environment&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt;
  &lt;span class="nx"&gt;is_staging&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;environment&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"staging"&lt;/span&gt;
  &lt;span class="nx"&gt;is_dev&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;environment&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"dev"&lt;/span&gt;

  &lt;span class="c1"&gt;# Compute sizing based on environment&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&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.medium"&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_staging&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="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Auto Scaling settings&lt;/span&gt;
  &lt;span class="nx"&gt;min_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&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="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_staging&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="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;max_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&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;10&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_staging&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Feature toggles&lt;/span&gt;
  &lt;span class="nx"&gt;enable_autoscaling&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_dev&lt;/span&gt;        &lt;span class="c1"&gt;# Enabled for staging and production&lt;/span&gt;
  &lt;span class="nx"&gt;enable_detailed_monitoring&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;enable_ssh&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="nx"&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="c1"&gt;# Disabled in production&lt;/span&gt;
  &lt;span class="nx"&gt;deletion_protection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this is better:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All logic in one place — easy to see what changes per environment&lt;/li&gt;
&lt;li&gt;No scattered ternaries through resource arguments&lt;/li&gt;
&lt;li&gt;Easy to test — change one variable, see all impacts&lt;/li&gt;
&lt;li&gt;Easy to maintain — add a new environment? Update one block&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conditional Resource Creation with &lt;code&gt;count&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;count = condition ? 1 : 0&lt;/code&gt; pattern makes entire resources optional:&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;# Only create autoscaling policies for staging and production&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_autoscaling_policy"&lt;/span&gt; &lt;span class="s2"&gt;"scale_out"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enable_autoscaling&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;"${var.cluster_name}-scale-out"&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="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;web&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="c1"&gt;# Only create CloudWatch alarms for production&lt;/span&gt;
&lt;span class="nx"&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;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enable_detailed_monitoring&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;alarm_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.cluster_name}-high-cpu"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;enable_autoscaling = false&lt;/code&gt;, the policy isn't created at all. No resources. No costs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Safe Output References
&lt;/h2&gt;

&lt;p&gt;When resources are conditional, you can't reference them directly. This fails when count = 0:&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;# BROKEN — errors when resource doesn't exist&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"alarm_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_cloudwatch_metric_alarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;high_cpu&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is a ternary guard:&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;# CORRECT — returns null when resource doesn't exist&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"alarm_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;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enable_detailed_monitoring&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; 
    &lt;span class="nx"&gt;aws_cloudwatch_metric_alarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;high_cpu&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;p&gt;For outputs that return objects:&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;output&lt;/span&gt; &lt;span class="s2"&gt;"autoscaling_policies"&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;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enable_autoscaling&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;scale_out&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_out&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;name&lt;/span&gt;
    &lt;span class="nx"&gt;scale_in&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_in&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;name&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this guard, Terraform throws &lt;code&gt;Invalid index&lt;/code&gt; errors when the resource doesn't exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Validation Block: Catch Mistakes Early
&lt;/h2&gt;

&lt;p&gt;Add a validation block to prevent invalid inputs:&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;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: dev, staging, or production"&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;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev"&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;"production"&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;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 one of: dev, staging, 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;p&gt;Try to pass an invalid value:&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;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"environment=stg"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;│ Error: Invalid value for variable
│ Environment must be one of: dev, staging, production.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caught at plan time — before anything is deployed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Results: Dev vs Production
&lt;/h2&gt;

&lt;p&gt;Same module. Same code. Different outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dev Environment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;instance_type&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="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nx"&gt;max_size&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="nx"&gt;enable_autoscaling&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nx"&gt;enable_monitoring&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nx"&gt;enable_ssh&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;autoscaling_policies&lt;/span&gt; &lt;span class="err"&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;Production Environment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.medium"&lt;/span&gt;
&lt;span class="nx"&gt;min_size&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="nx"&gt;max_size&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="nx"&gt;enable_autoscaling&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;enable_monitoring&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;enable_ssh&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nx"&gt;autoscaling_policies&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;scale_in&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-webserver-scale-in"&lt;/span&gt;
  &lt;span class="nx"&gt;scale_out&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-webserver-scale-out"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dev gets small, cheap, debuggable. Production gets big, scalable, secure. All from one module.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Conditional Data Source Pattern
&lt;/h2&gt;

&lt;p&gt;Sometimes you need to either create new infrastructure or use existing. This pattern handles both:&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;variable&lt;/span&gt; &lt;span class="s2"&gt;"use_existing_vpc"&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="c1"&gt;# Conditionally look up existing VPC&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"existing"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&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;use_existing_vpc&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;id&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;existing_vpc_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Conditionally create new VPC&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;"new"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&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;use_existing_vpc&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&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="nx"&gt;vpc_id&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;use_existing_vpc&lt;/span&gt; &lt;span class="err"&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_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;existing&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;id&lt;/span&gt; &lt;span class="err"&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;new&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;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Greenfield deployments&lt;/strong&gt; — create everything new&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brownfield deployments&lt;/strong&gt; — use existing infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One toggle switches between them.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Conditional expressions choose values.&lt;/strong&gt; They answer "what should this be?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional resource creation chooses existence.&lt;/strong&gt; It answers "should this exist at all?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't directly choose between two resource types&lt;/strong&gt; with one conditional. You need two resources with count on each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Locals are better than scattered ternaries.&lt;/strong&gt; Centralize your logic. Future you will thank you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation blocks are your first line of defense.&lt;/strong&gt; Catch invalid inputs before they cause damage.&lt;/p&gt;




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

&lt;p&gt;Today I transformed my module from hardcoded values to a single &lt;code&gt;environment&lt;/code&gt; variable that drives everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instance sizing (t3.micro → t3.medium)&lt;/li&gt;
&lt;li&gt;Cluster size (1 → 3 → 10 instances)&lt;/li&gt;
&lt;li&gt;Feature toggles (autoscaling, monitoring, SSH)&lt;/li&gt;
&lt;li&gt;Protection settings (deletion protection)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One codebase. Multiple environments. Zero duplication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is how infrastructure at scale works.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. The best part? When someone asks "what does dev look like vs production?" I just point to my locals block. Everything is there. One place. No hunting through 500 lines of code.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>terraform</category>
      <category>beginners</category>
      <category>aws</category>
      <category>30daychallenge</category>
    </item>
    <item>
      <title>Mastering Loops and Conditionals in Terraform</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Fri, 27 Mar 2026 11:51:38 +0000</pubDate>
      <link>https://dev.to/tink-origami/mastering-loops-and-conditionals-in-terraform-8em</link>
      <guid>https://dev.to/tink-origami/mastering-loops-and-conditionals-in-terraform-8em</guid>
      <description>&lt;p&gt;&lt;strong&gt;Day 10 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned that Terraform is actually a programming language in disguise.&lt;/p&gt;

&lt;p&gt;For the past 9 days, I've been writing infrastructure like it's 2010: one resource at a time, copy-pasting blocks, and feeling clever when I only duplicated code three times instead of five.&lt;/p&gt;

&lt;p&gt;Today I discovered loops and conditionals. And I'm never going back.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Copy-Paste Engineering
&lt;/h2&gt;

&lt;p&gt;Let me show you what I was doing before today:&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;# Creating 3 users? Copy-paste!&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"alice"&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;"alice"&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_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"bob"&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;"bob"&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_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"charlie"&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;"charlie"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Security group with 3 rules? Copy-paste!&lt;/span&gt;
&lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;from_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;from_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;from_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. Until you need 10 users. Or 50 security group rules. Or you need to change something across all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There had to be a better way.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is.&lt;/p&gt;




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

&lt;p&gt;&lt;code&gt;count&lt;/code&gt; creates multiple copies of a resource. Think of it as a for-loop for infrastructure.&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;# Create 3 identical IAM users&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;"user-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&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;code&gt;count.index&lt;/code&gt; gives you the position (0, 1, 2). This creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user-0&lt;/li&gt;
&lt;li&gt;user-1&lt;/li&gt;
&lt;li&gt;user-2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also use it with a list:&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;variable&lt;/span&gt; &lt;span class="s2"&gt;"user_names"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&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="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"charlie"&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_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&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;user_names&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;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_names&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&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;
  
  
  The Problem Nobody Warns You About
&lt;/h3&gt;

&lt;p&gt;Here's where &lt;code&gt;count&lt;/code&gt; gets dangerous.&lt;/p&gt;

&lt;p&gt;Imagine your user list: &lt;code&gt;["alice", "bob", "charlie"]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Terraform tracks resources by their &lt;strong&gt;index position&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Index 0 = alice&lt;/li&gt;
&lt;li&gt;Index 1 = bob&lt;/li&gt;
&lt;li&gt;Index 2 = charlie&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you remove "alice" from the list: &lt;code&gt;["bob", "charlie"]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The list shifts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Index 0 = bob (was alice!)&lt;/li&gt;
&lt;li&gt;Index 1 = charlie (was bob!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What Terraform sees:&lt;/strong&gt; Index 0 changed from alice to bob → destroy alice, create bob. Index 1 changed from bob to charlie → destroy bob, create charlie.&lt;/p&gt;

&lt;p&gt;Bob and Charlie get &lt;strong&gt;destroyed and recreated&lt;/strong&gt; even though they still exist. Any data, IPs, or attached resources are lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the count trap.&lt;/strong&gt; It's subtle, destructive, and catches everyone once.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 2: &lt;code&gt;for_each&lt;/code&gt; — The Safe Loop
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;for_each&lt;/code&gt; solves the index problem by using &lt;strong&gt;keys&lt;/strong&gt; instead of positions.&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;# Using a set&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"user_names"&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;set&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;default&lt;/span&gt; &lt;span class="p"&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="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"charlie"&lt;/span&gt;&lt;span class="p"&gt;]&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_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"users"&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;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_names&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;each&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Terraform tracks resources by their &lt;strong&gt;value&lt;/strong&gt; (alice, bob, charlie), not their position.&lt;/p&gt;

&lt;p&gt;Remove "alice" from the set? Only Alice is destroyed. Bob and Charlie stay exactly where they are.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Power of Maps
&lt;/h3&gt;

&lt;p&gt;Where &lt;code&gt;for_each&lt;/code&gt; really shines is with maps, because you can carry extra configuration per item:&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;"users"&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;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;department&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;admin&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bool&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;alice&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"engineering"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;admin&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="nx"&gt;bob&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"marketing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="nx"&gt;admin&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="p"&gt;}&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_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"users"&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;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&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;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&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;Department&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt;
    &lt;span class="nx"&gt;Admin&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tostring&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each user gets unique tags based on their configuration. Try doing that with &lt;code&gt;count&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 3: &lt;code&gt;for&lt;/code&gt; Expressions — Transform Data Without Creating Resources
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;for&lt;/code&gt; expressions reshape data. They don't create infrastructure—they transform what you already have.&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;# Uppercase all names&lt;/span&gt;
&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"upper_names"&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="nx"&gt;in&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;user_names&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;upper&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="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# ["ALICE", "BOB", "CHARLIE"]&lt;/span&gt;

&lt;span class="c1"&gt;# Create a map from a list&lt;/span&gt;
&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"user_map"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;idx&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;in&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;user_names&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# { "0" = "alice", "1" = "bob", "2" = "charlie" }&lt;/span&gt;

&lt;span class="c1"&gt;# Filter and transform&lt;/span&gt;
&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"admin_users"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;for&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;user&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="nx"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;admin&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;span class="c1"&gt;# Only admin users appear in the output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are incredibly useful for creating clean outputs that other configurations can consume.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 4: Ternary Conditionals — Make Decisions
&lt;/h2&gt;

&lt;p&gt;The ternary operator is Terraform's &lt;code&gt;if/else&lt;/code&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="nx"&gt;condition&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;true_value&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;false_value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Making Resources Optional
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"enable_autoscaling"&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;true&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_autoscaling_policy"&lt;/span&gt; &lt;span class="s2"&gt;"scale_out"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&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;enable_autoscaling&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="c1"&gt;# ... policy configuration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;enable_autoscaling = false&lt;/code&gt;, count becomes 0 and the policy is never created.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment-Specific Configuration
&lt;/h3&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="nx"&gt;default&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="nx"&gt;locals&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;environment&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"t3.medium"&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;extra_tags&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="err"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Criticality&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"high"&lt;/span&gt;
    &lt;span class="nx"&gt;Backup&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"enabled"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have one codebase that works for dev (small, cheap) and production (big, protected).&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Here's how I refactored my web server cluster module:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; Hardcoded security group rules, no autoscaling options, repetitive outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&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="c1"&gt;# Optional autoscaling policies&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_out"&lt;/span&gt; &lt;span class="p"&gt;{&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_autoscaling&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;"&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;-scale-out"&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;web&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;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;span class="c1"&gt;# Flexible security group rules&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group_rule"&lt;/span&gt; &lt;span class="s2"&gt;"additional"&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;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;additional_sg_rules&lt;/span&gt;

  &lt;span class="nx"&gt;type&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ingress"&lt;/span&gt;
  &lt;span class="nx"&gt;from_port&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;from_port&lt;/span&gt;
  &lt;span class="nx"&gt;to_port&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to_port&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_id&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&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="c1"&gt;# Clean outputs&lt;/span&gt;
&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"autoscaling_policies"&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="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enable_autoscaling&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;scale_out&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_out&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;name&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="err"&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;h2&gt;
  
  
  When to Use What: The Cheat Sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Avoid When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Identical resources, fixed numbers&lt;/td&gt;
&lt;td&gt;Lists that might change order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;for_each&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Resources with unique identities, maps, changing lists&lt;/td&gt;
&lt;td&gt;Creating thousands of resources (state file gets huge)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;for expressions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Data transformation, outputs, locals&lt;/td&gt;
&lt;td&gt;Creating actual infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ternary&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple conditional values, resource toggles&lt;/td&gt;
&lt;td&gt;Complex nested logic (use locals instead)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Count Trap: A Real Example
&lt;/h2&gt;

&lt;p&gt;I built a simple IAM user setup with &lt;code&gt;count&lt;/code&gt;. It worked perfectly. Then I removed a user from the middle of the list. &lt;code&gt;terraform plan&lt;/code&gt; showed that &lt;strong&gt;every user after that position would be recreated&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I tested with random IDs attached to each user. After removing one, the IDs of remaining users changed. They were destroyed and recreated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost?&lt;/strong&gt; If these were EC2 instances, I'd lose their IPs, attached volumes, and any stateful data. If these were databases, I'd lose data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix?&lt;/strong&gt; I refactored to use &lt;code&gt;for_each&lt;/code&gt;. Now removing any user affects only that user.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Loops eliminate repetition.&lt;/strong&gt; One block now does the work of ten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;count&lt;/code&gt; is simple but dangerous.&lt;/strong&gt; It works until your list changes. Then it breaks things in ways that are hard to debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;for_each&lt;/code&gt; is safer.&lt;/strong&gt; It respects identity, not position. Use it whenever your resources have unique names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditionals make code reusable.&lt;/strong&gt; One module now serves dev and production, with features that turn on and off based on variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;for&lt;/code&gt; expressions are the glue.&lt;/strong&gt; They transform raw resources into clean outputs that others can consume.&lt;/p&gt;




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

&lt;p&gt;Before today, I wrote infrastructure by copy-pasting. Now I write it with loops and conditionals.&lt;/p&gt;

&lt;p&gt;My code is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shorter&lt;/strong&gt; — 200 lines instead of 500&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safer&lt;/strong&gt; — removing items doesn't break everything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smarter&lt;/strong&gt; — dev gets small instances, production gets big ones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More maintainable&lt;/strong&gt; — change one place, all environments update&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Terraform isn't just a tool for creating resources. It's a language for describing infrastructure. And today, I finally started treating it like one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. If you're still writing 10 separate resource blocks for 10 similar resources, stop. Learn &lt;code&gt;for_each&lt;/code&gt;. Your future self will thank you.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>hashicorp</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Advanced Terraform Module Usage: Versioning, Gotchas, and Reuse Across Environments</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Thu, 26 Mar 2026 09:41:09 +0000</pubDate>
      <link>https://dev.to/tink-origami/advanced-terraform-module-usage-versioning-gotchas-and-reuse-across-environments-5779</link>
      <guid>https://dev.to/tink-origami/advanced-terraform-module-usage-versioning-gotchas-and-reuse-across-environments-5779</guid>
      <description>&lt;p&gt;&lt;strong&gt;Day 9 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned the hard-won lessons that separate "I know how to write a module" from "I can safely share modules with a team."&lt;/p&gt;

&lt;p&gt;Yesterday I built my first module. Today I learned why modules break in production, how to version them like real software, and why pinning versions is the difference between "it works" and "it works every time, for everyone."&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Modules Aren't Magic
&lt;/h2&gt;

&lt;p&gt;Yesterday's module worked perfectly when I called it from a local path. But the moment I tried to share it? Things got messy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three gotchas caught me off guard:&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Gotcha 1: File Paths Lie to You
&lt;/h3&gt;

&lt;p&gt;I had a user data script in my module:&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;user_data&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worked fine when testing locally. Then I called the module from a different directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Error reading file "user-data.sh": no such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; &lt;code&gt;file()&lt;/code&gt; resolves paths relative to where Terraform is run, not relative to the module!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Always use &lt;code&gt;${path.module}&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the path is always correct, no matter who calls the module or from where.&lt;/p&gt;




&lt;h3&gt;
  
  
  Gotcha 2: Inline Blocks vs Separate Resources
&lt;/h3&gt;

&lt;p&gt;My security group had inline ingress rules:&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_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"instance"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&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;to_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked fine. But when someone using my module wanted to add another rule? They couldn't. The module controlled everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use separate security group rule resources:&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_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"instance"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# No inline rules!&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_security_group_rule"&lt;/span&gt; &lt;span class="s2"&gt;"allow_http"&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="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&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="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="nx"&gt;to_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now callers can add their own rules without modifying the module. The module provides a foundation; they add the customization.&lt;/p&gt;




&lt;h3&gt;
  
  
  Gotcha 3: Module Outputs Create Hidden Dependencies
&lt;/h3&gt;

&lt;p&gt;I had a resource that needed to wait for the module:&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_instance"&lt;/span&gt; &lt;span class="s2"&gt;"monitoring"&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="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webserver_cluster&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;Looks fine, right? &lt;strong&gt;Wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;depends_on&lt;/code&gt; creates a dependency on &lt;strong&gt;every resource&lt;/strong&gt; inside the module. If any resource in the module changes, Terraform recreates my monitoring instance — even if it wasn't related.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Depend on specific outputs:&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_instance"&lt;/span&gt; &lt;span class="s2"&gt;"monitoring"&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="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webserver_cluster&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now my monitoring instance only cares if the ALB DNS changes — not if a tag on a random instance changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Version Your Modules
&lt;/h2&gt;

&lt;p&gt;Once I fixed the gotchas, I needed to share my module safely. The answer: &lt;strong&gt;versioning.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Push to GitHub
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial module release"&lt;/span&gt;
git remote add origin https://github.com/123Origami/terraform-aws-webserver-cluster.git
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Tag a Version
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git tag &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"v0.0.1"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"First release: Basic web server cluster"&lt;/span&gt;
git push origin main &lt;span class="nt"&gt;--tags&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now my module is versioned! Anyone can use it with:&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;module&lt;/span&gt; &lt;span class="s2"&gt;"webserver_cluster"&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;"github.com/123Origami/terraform-aws-webserver-cluster?ref=v0.0.1"&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: Make a Change, Tag New Version
&lt;/h3&gt;

&lt;p&gt;I added a new feature (&lt;code&gt;custom_user_data&lt;/code&gt;), committed it, and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git tag &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"v0.0.2"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Added custom user data support"&lt;/span&gt;
git push origin main &lt;span class="nt"&gt;--tags&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I have two versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;v0.0.1&lt;/strong&gt; — stable, production-ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v0.0.2&lt;/strong&gt; — new feature, needs testing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Pattern: Dev Tests, Production Pins
&lt;/h2&gt;

&lt;p&gt;This is where it gets good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dev environment uses v0.0.2:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# live/dev/services/webserver-cluster/main.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"webserver_cluster"&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;"github.com/123Origami/terraform-aws-webserver-cluster?ref=v0.0.2"&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;"webservers-dev"&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.micro"&lt;/span&gt;
  &lt;span class="nx"&gt;custom_user_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${path.module}/dev-setup.sh"&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;Production stays on v0.0.1:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# live/production/services/webserver-cluster/main.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"webserver_cluster"&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;"github.com/123Origami/terraform-aws-webserver-cluster?ref=v0.0.1"&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;"webservers-production"&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.medium"&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 pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dev tests the new version immediately&lt;/li&gt;
&lt;li&gt;Production stays stable until I validate v0.0.2 in dev&lt;/li&gt;
&lt;li&gt;When I'm confident, I update production to v0.0.2&lt;/li&gt;
&lt;li&gt;If something breaks, I roll back to v0.0.1 in seconds&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The terraform init Magic
&lt;/h2&gt;

&lt;p&gt;When I run &lt;code&gt;terraform init&lt;/code&gt; in dev:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Initializing modules...
Downloading git::https://github.com/123Origami/terraform-aws-webserver-cluster.git?ref=v0.0.2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production downloads v0.0.1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Initializing modules...
Downloading git::https://github.com/123Origami/terraform-aws-webserver-cluster.git?ref=v0.0.1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same module. Different versions. Different environments. This is how real teams work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Version Pinning Is Non-Negotiable
&lt;/h2&gt;

&lt;p&gt;Imagine this nightmare:&lt;/p&gt;

&lt;p&gt;Engineer A runs &lt;code&gt;terraform apply&lt;/code&gt; at 9:00 AM — downloads module at v0.0.1, everything works.&lt;/p&gt;

&lt;p&gt;Engineer B runs &lt;code&gt;terraform apply&lt;/code&gt; at 10:00 AM — module source has been updated to v0.0.2 (new feature, breaking change). Infrastructure now inconsistent.&lt;/p&gt;

&lt;p&gt;No one knows why. "It worked on my machine" becomes "it worked in my apply."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without version pinning, you're gambling.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Module README: Your Contract with Users
&lt;/h2&gt;

&lt;p&gt;Every shared module needs a README. It's not optional. Here's what I included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# AWS Web Server Cluster Module&lt;/span&gt;

&lt;span class="gu"&gt;## What it does&lt;/span&gt;
Creates a highly available web server cluster with ALB and ASG.

&lt;span class="gu"&gt;## Usage&lt;/span&gt;
module "webserver_cluster" {
  source = "github.com/your-username/terraform-aws-webserver-cluster?ref=v0.0.1"
  cluster_name = "my-app"
}

&lt;span class="gu"&gt;## Inputs&lt;/span&gt;
| Name | Type | Default | Required |
|------|------|---------|----------|
| cluster_name | string | - | yes |
| instance_type | string | t3.micro | no |
| min_size | number | 1 | no |

&lt;span class="gu"&gt;## Outputs&lt;/span&gt;
| Name | Description |
|------|-------------|
| alb_url | URL to access the cluster |
| asg_name | Name of the Auto Scaling Group |

&lt;span class="gu"&gt;## Known Gotchas&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`${path.module}`&lt;/span&gt; for any file paths inside the module
&lt;span class="p"&gt;-&lt;/span&gt; Security group rules are separate resources for flexibility
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good README means users don't have to read your code to use it.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;The gotchas are subtle but deadly.&lt;/strong&gt; File paths, dependency chains, and inline resources — all of them work fine until they don't. Fix them once, in the module, and everyone benefits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versioning is how modules become trustworthy.&lt;/strong&gt; Without a version pin, you're sharing a moving target. With versioning, you're sharing a contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dev-test-prod pattern is simple but powerful.&lt;/strong&gt; Test new versions in dev. Let them bake. When they're stable, roll to staging, then production. If something breaks, roll back one version.&lt;/p&gt;




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

&lt;p&gt;A module without versioning is just a folder of code. A module with versioning is a tool your whole team can rely on.&lt;/p&gt;

&lt;p&gt;Today I turned my web server module from a personal convenience into something I could share with anyone, anywhere, with confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version your modules. Pin your versions. Test in dev, trust in prod.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/123Origami/terraform-aws-webserver-cluster" rel="noopener noreferrer"&gt;My Versioned Module on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/modules/sources" rel="noopener noreferrer"&gt;Terraform Module Versioning Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>30daychallenge</category>
      <category>terraform</category>
      <category>beginners</category>
      <category>aws</category>
    </item>
    <item>
      <title># Building Reusable Infrastructure with Terraform Modules ## Or: How I Finally Stopped Copy-Pasting the Same 200 Lines of Code</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Thu, 26 Mar 2026 08:36:19 +0000</pubDate>
      <link>https://dev.to/tink-origami/-building-reusable-infrastructure-with-terraform-modules-or-how-i-finally-stopped-copy-pasting-34nh</link>
      <guid>https://dev.to/tink-origami/-building-reusable-infrastructure-with-terraform-modules-or-how-i-finally-stopped-copy-pasting-34nh</guid>
      <description>&lt;p&gt;&lt;strong&gt;Day 8 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned the secret that separates people who "know Terraform" from people who actually build infrastructure at scale.&lt;/p&gt;

&lt;p&gt;Modules.&lt;/p&gt;

&lt;p&gt;You know that feeling when you've written the same security group configuration three times? Or when you're about to copy-paste your entire web server cluster for the fifth environment? That's the feeling modules were made to eliminate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Copy-Paste Engineering
&lt;/h2&gt;

&lt;p&gt;Let me show you what I was doing before today:&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;# dev/main.tf&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb"&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;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev-web-alb"&lt;/span&gt;
  &lt;span class="c1"&gt;# ... 200 more lines ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# staging/main.tf  &lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb"&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;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"staging-web-alb"&lt;/span&gt;
  &lt;span class="c1"&gt;# ... THE SAME 200 lines, different name ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# production/main.tf&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb"&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;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-web-alb"&lt;/span&gt;
  &lt;span class="c1"&gt;# ... 200 lines, again ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what we call &lt;strong&gt;Copy-Paste Engineering&lt;/strong&gt;. It's fast. It works. And it's a nightmare to maintain.&lt;/p&gt;

&lt;p&gt;Change the health check path? That's 3 files. Fix a security group rule? 3 files. Update the AMI filter? You guessed it — 3 files. And if you forget one? Now dev and production are different, and nobody knows why.&lt;/p&gt;

&lt;p&gt;There had to be a better way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Modules
&lt;/h2&gt;

&lt;p&gt;A module is just a folder of Terraform code that you can call from other Terraform configurations. That's it. No magic. No special syntax.&lt;/p&gt;

&lt;p&gt;But that simple concept changes everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Module Structure
&lt;/h3&gt;

&lt;p&gt;Here's what I built today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules/
└── services/
    └── webserver-cluster/
        ├── main.tf          # The infrastructure (ALB, ASG, SG)
        ├── variables.tf     # What you can configure
        ├── outputs.tf       # What you get back
        └── README.md        # How to use it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Variables (What You Can Configure)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&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;"Name for all cluster resources"&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="c1"&gt;# No default — caller MUST provide this&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;"instance_type"&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;"EC2 instance type"&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;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&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;"min_size"&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;"Minimum instances in ASG"&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;number&lt;/span&gt;
  &lt;span class="nx"&gt;default&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;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"max_size"&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;"Maximum instances in ASG"&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;number&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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;"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;"Environment name (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;default&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every configurable aspect of the infrastructure is an input variable. Nothing is hardcoded.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Outputs (What You Get Back)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&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;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DNS name of the load balancer"&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;web&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;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"alb_url"&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;"Full URL to access the cluster"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://${aws_lb.web.dns_name}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"asg_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;"Name of the Auto Scaling Group"&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_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Callers get back exactly what they need — no more, no less.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Main File (The Infrastructure)
&lt;/h3&gt;

&lt;p&gt;Inside &lt;code&gt;main.tf&lt;/code&gt; is all the code I've been writing all week. But now it uses variables instead of hardcoded values:&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_lb"&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;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.cluster_name}-alb"&lt;/span&gt;  &lt;span class="c1"&gt;# No hardcoding!&lt;/span&gt;
  &lt;span class="nx"&gt;security_groups&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_security_group&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;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;subnets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_subnets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Magic: Calling the Module
&lt;/h2&gt;

&lt;p&gt;Here's where it gets beautiful. For dev:&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;# live/dev/services/webserver-cluster/main.tf&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"webserver_cluster"&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/services/webserver-cluster"&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;"webservers-dev"&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.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="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;max_size&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;environment&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="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"alb_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_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb_url&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# live/production/services/webserver-cluster/main.tf&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"webserver_cluster"&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/services/webserver-cluster"&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;"webservers-production"&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.medium"&lt;/span&gt;   &lt;span class="c1"&gt;# Bigger! &lt;/span&gt;
  &lt;span class="nx"&gt;min_size&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;max_size&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"alb_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_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb_url&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same module. Different inputs. Zero code duplication.&lt;/p&gt;

&lt;p&gt;When I need to update the health check path? I change it in ONE file — the module. Every environment gets the update automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Moment I Knew It Worked
&lt;/h2&gt;

&lt;p&gt;I deployed dev first:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;live/dev/services/webserver-cluster
&lt;span class="nv"&gt;$ &lt;/span&gt;terraform init
&lt;span class="nv"&gt;$ &lt;/span&gt;terraform apply

Apply &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; Outputs:
alb_url &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://webservers-dev-alb-xxxxx.elb.amazonaws.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I opened the URL. There it was — my web page with "webservers-dev" on top.&lt;/p&gt;

&lt;p&gt;Then I looked at production (didn't deploy, just previewed):&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;live/production/services/webserver-cluster
&lt;span class="nv"&gt;$ &lt;/span&gt;terraform plan

&lt;span class="c"&gt;# Notice the instance type:&lt;/span&gt;
module.webserver_cluster.aws_launch_template.web
    instance_type &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.medium"&lt;/span&gt;  &lt;span class="c"&gt;# Dev used t3.micro!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same code, producing different infrastructure. This is how real teams work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Module Design Decisions I Had to Make
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What to Expose vs What to Hide
&lt;/h3&gt;

&lt;p&gt;I chose to expose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster name&lt;/strong&gt; — caller must provide it (no default)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instance type&lt;/strong&gt; — different sizes for different environments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Min/max sizes&lt;/strong&gt; — dev can run with 1 instance, production needs 3+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment&lt;/strong&gt; — for tagging and naming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I kept internal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AMI lookup&lt;/strong&gt; — everyone gets the latest Amazon Linux 2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC selection&lt;/strong&gt; — always use the default VPC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security group structure&lt;/strong&gt; — always the same pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule: &lt;strong&gt;Expose what changes between environments. Hide what stays the same.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What Happens If Someone Forgets a Required Variable?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"broken"&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/services/webserver-cluster"&lt;/span&gt;
  &lt;span class="c1"&gt;# No cluster_name provided!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Missing required argument
The argument "cluster_name" is required, but no definition was found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform catches it. The caller knows exactly what they missed. This is why required variables are so important — they prevent silent failures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 4 Learnings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Root Module vs Child Module:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The configuration you run is the &lt;strong&gt;root module&lt;/strong&gt;. Any module you call is a &lt;strong&gt;child module&lt;/strong&gt;. There's no technical difference — just who's calling who.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What &lt;code&gt;terraform init&lt;/code&gt; Does:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you add a new module source, &lt;code&gt;terraform init&lt;/code&gt; downloads the module code into &lt;code&gt;.terraform/modules/&lt;/code&gt;. It doesn't apply anything — just makes the code available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Module Outputs in State:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Module outputs are stored in the state file under the module's name. If you look in &lt;code&gt;terraform.tfstate&lt;/code&gt;, you'll see:&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="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alb_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://webservers-dev-alb-xxxxx.elb.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how other configurations can read outputs from your module.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenges I Hit (And How I Fixed Them)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Challenge 1: Relative Paths&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I kept getting &lt;code&gt;source path does not exist&lt;/code&gt; errors. The problem? I was counting wrong.&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;live/dev/services/webserver-cluster/&lt;/code&gt; to &lt;code&gt;modules/services/webserver-cluster/&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;../&lt;/code&gt; = back to &lt;code&gt;live/dev/services/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;../../&lt;/code&gt; = back to &lt;code&gt;live/dev/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;../../../&lt;/code&gt; = back to &lt;code&gt;live/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;../../../../&lt;/code&gt; = back to project root, then into &lt;code&gt;modules/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix: Print your current directory and count carefully!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 2: Variable Type Mismatch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I passed &lt;code&gt;min_size = "2"&lt;/code&gt; (string) but my variable expected a number. Terraform gave me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Incorrect attribute value type
Inappropriate value for attribute "min_size": number required.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix: Always use the right type — numbers without quotes, lists with brackets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 3: Missing Outputs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My module created an ALB, but I forgot to output its DNS name. The caller couldn't access anything!&lt;/p&gt;

&lt;p&gt;Fix: Ask yourself "what does someone using this module need to know?" Output those things.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Difference Between a Good Module and a Painful Module
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Good Module&lt;/th&gt;
&lt;th&gt;Painful Module&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clear, specific variable names&lt;/td&gt;
&lt;td&gt;Vague names like &lt;code&gt;var1&lt;/code&gt;, &lt;code&gt;var2&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every variable has a description&lt;/td&gt;
&lt;td&gt;No descriptions — guess what it does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensible defaults for optional values&lt;/td&gt;
&lt;td&gt;Everything is required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Useful outputs (DNS names, IDs)&lt;/td&gt;
&lt;td&gt;No outputs — caller can't get anything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has a README&lt;/td&gt;
&lt;td&gt;"Just read the code"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One clear purpose&lt;/td&gt;
&lt;td&gt;Tries to do everything&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Best Practices I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use the &lt;code&gt;source&lt;/code&gt; parameter with relative paths&lt;/strong&gt; — absolute paths break when other people clone your repo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Always add descriptions to variables&lt;/strong&gt; — future you will thank present you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Provide sensible defaults&lt;/strong&gt; — if 90% of use cases use &lt;code&gt;t3.micro&lt;/code&gt;, make that the default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Output everything a caller might need&lt;/strong&gt; — DNS names, ARNs, IDs, URLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Version your modules&lt;/strong&gt; — when you change a module, bump the version so callers upgrade intentionally.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep modules focused&lt;/strong&gt; — one module = one responsibility. Don't create a "everything" module.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;Modules are how Terraform scales from "my infrastructure" to "our infrastructure."&lt;/p&gt;

&lt;p&gt;Before modules, I had 200 lines of copy-pasted code per environment. After modules, I have one module and 20 lines of configuration per environment.&lt;/p&gt;

&lt;p&gt;When I need to update the health check path, I change one file — not three. When I need to add a new environment, I copy the calling configuration — not 200 lines of infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modules don't just save time. They save mistakes. And when you're managing production infrastructure, that's worth everything.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. I spent 5 days writing the same infrastructure over and over. Today I spent 2 hours writing it once. The math is clear: modules pay for themselves the second you need a second environment.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>beginners</category>
      <category>terraform</category>
      <category>aws</category>
      <category>ec2</category>
    </item>
    <item>
      <title># State Isolation: Workspaces vs File Layouts — When to Use Each</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Mon, 23 Mar 2026 14:47:39 +0000</pubDate>
      <link>https://dev.to/tink-origami/-state-isolation-workspaces-vs-file-layouts-when-to-use-each-4hm</link>
      <guid>https://dev.to/tink-origami/-state-isolation-workspaces-vs-file-layouts-when-to-use-each-4hm</guid>
      <description>&lt;p&gt;&lt;strong&gt;Day 7 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I faced the million-dollar question: How do you manage dev, staging, and production without accidentally nuking production?&lt;/p&gt;

&lt;p&gt;Terraform gives you two answers. One is quick and easy. One is boring and safe. Let me tell you which one I'm choosing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: One State to Rule Them All
&lt;/h2&gt;

&lt;p&gt;Yesterday I had one state file. One. For everything.&lt;/p&gt;

&lt;p&gt;That's fine for a personal project. But imagine a team of 10 people all running Terraform on the same state file. Dev changes. Staging changes. Production changes. All mixed together like a smoothie of chaos.&lt;/p&gt;

&lt;p&gt;Someone adds a tag in dev. Someone else deletes a bucket in staging. Suddenly production is down and nobody knows why.&lt;/p&gt;

&lt;p&gt;I needed isolation. Real isolation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 1: Workspaces (The Quick Fix)
&lt;/h2&gt;

&lt;p&gt;Workspaces are Terraform's built-in solution. Same code. Different state files.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform workspace new dev
terraform workspace new staging
terraform workspace new production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your code:&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;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myapp-${terraform.workspace}"&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;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&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;When you're in dev, you get a dev bucket. In production, you get a production bucket. Same code, different resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Workspaces Get Right:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dead simple&lt;/strong&gt; — one command and you're done&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No code duplication&lt;/strong&gt; — change one file, all environments get the update&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast switching&lt;/strong&gt; — &lt;code&gt;terraform workspace select dev&lt;/code&gt; and go&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where Workspaces Fall Short:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No Code Isolation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same code means same bugs everywhere. I fixed something in dev? That change applies to production too. Unless you're using Git branches carefully, one bad commit breaks everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Too Easy to Misconfigure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I did this three times today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform workspace &lt;span class="k"&gt;select &lt;/span&gt;production  &lt;span class="c"&gt;# Wait, I meant dev!&lt;/span&gt;
terraform apply  &lt;span class="c"&gt;# Oops, I just deployed to prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No warning. No confirmation. Just me and my mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team Pain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everyone works on the same codebase. Merge conflicts. Coordination headaches. Sarah's dev change blocks John's production deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workspaces are like using the same recipe for all three meals — breakfast, lunch, and dinner. Sure, it's simple, but you probably don't want pancakes for dinner.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 2: File Layouts (The Grown-Up Way)
&lt;/h2&gt;

&lt;p&gt;Different folders. Different code. Different state. Complete isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;environments/
├── dev/
│   ├── backend.tf   # points to dev state
│   └── main.tf      # dev resources
├── staging/
│   ├── backend.tf   # points to staging state
│   └── main.tf      # staging resources
└── production/
    ├── backend.tf   # points to prod state
    └── main.tf      # prod resources (with extra protections)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What File Layouts Get Right:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Complete Isolation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dev folder has dev code. Production folder has production code. They never touch. I can delete the entire dev folder and production keeps running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hard to Misconfigure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To break production, I have to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;cd environments/production&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run Terraform&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;yes&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three conscious decisions. No accidental deploys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team-Friendly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dev team works in dev folder. Prod team works in prod folder. Different branches, different approvals, no stepping on toes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment-Specific Logic&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Production can have:&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;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;prevent_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# Can't delete prod resources!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dev can be reckless. Production can be locked down. Different needs, different code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where File Layouts Add Friction:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;More Directory Management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Five folders. Ten files. Backend configs everywhere. It's organized, but it's also... a lot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repeated Backend Config&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every environment needs its own &lt;code&gt;backend.tf&lt;/code&gt;. Copy-paste the same thing three times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-state-bucket"&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"environments/dev/terraform.tfstate"&lt;/span&gt;  &lt;span class="c1"&gt;# Only this changes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line changes per environment. The rest is identical. Feels repetitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each environment needs its own &lt;code&gt;terraform init&lt;/code&gt;. More commands, more waiting, more chances to forget something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File layouts are like having separate kitchens for breakfast, lunch, and dinner. More work to set up, but you never accidentally serve breakfast food at a dinner party.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Workspaces&lt;/th&gt;
&lt;th&gt;File Layouts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 seconds&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Duplication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Some (backend.tf)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Accidental Prod Deploy Risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team Collaboration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hard (one codebase)&lt;/td&gt;
&lt;td&gt;Easy (separate folders)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Environment-Specific Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complex (if statements)&lt;/td&gt;
&lt;td&gt;Simple (different files)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning Curve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  When to Use Workspaces
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You're working alone or in a small team.&lt;/strong&gt; Two people? Workspaces are fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're testing or prototyping.&lt;/strong&gt; Spin up dev, test something, destroy it. Quick and dirty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your environments are nearly identical.&lt;/strong&gt; Same resources, just different names. Workspaces handle that perfectly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You value speed over safety.&lt;/strong&gt; Sometimes that's the right tradeoff.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use File Layouts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You work in a team.&lt;/strong&gt; I don't care if it's 3 people or 300 — separate folders save relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have production infrastructure.&lt;/strong&gt; If customers depend on it, use file layouts. Protect your sleep schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your environments differ.&lt;/strong&gt; Dev uses t3.micro, prod uses t3.large? Different monitoring? Different backups? Separate code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need approvals.&lt;/strong&gt; Prod requires code review. Dev doesn't. Separate folders make that easy.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Recommendation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Workspaces for personal projects and testing. File layouts for everything else.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I know file layouts feel like more work. But here's the thing: that "more work" is actually the friction that prevents disaster.&lt;/p&gt;

&lt;p&gt;Every time I &lt;code&gt;cd environments/production&lt;/code&gt;, I pause. I check. I think. That pause is what keeps me from running &lt;code&gt;terraform destroy&lt;/code&gt; on production by accident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workspaces are a sledgehammer. File layouts are a scalpel. Both can build infrastructure. Only one belongs in surgery.&lt;/strong&gt;&lt;/p&gt;




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

&lt;p&gt;Today I learned that infrastructure isolation isn't just about state files. It's about creating boundaries that protect you from yourself and your team.&lt;/p&gt;

&lt;p&gt;Workspaces are clever. File layouts are boring. In production, boring wins every time.&lt;/p&gt;

&lt;p&gt;Tomorrow I'll probably complain about managing all these folders. But tonight, I'll sleep knowing my dev experiments can't touch production.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What would you choose?&lt;/strong&gt; I'm genuinely curious — drop your thoughts in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. If you're using workspaces in production and it's working for you — I'm genuinely impressed. Tell me your secrets. I need them.&lt;/em&gt; 😅&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>hashicorp</category>
      <category>aws</category>
      <category>30dayschallenge</category>
    </item>
    <item>
      <title># Managing Terraform State: Best Practices for DevOps ## How to Stop Fighting State Files and Start Collaborating</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Sun, 22 Mar 2026 17:45:53 +0000</pubDate>
      <link>https://dev.to/tink-origami/-managing-terraform-state-best-practices-for-devops-how-to-stop-fighting-state-files-and-start-4jad</link>
      <guid>https://dev.to/tink-origami/-managing-terraform-state-best-practices-for-devops-how-to-stop-fighting-state-files-and-start-4jad</guid>
      <description>&lt;p&gt;&lt;strong&gt;Day 6 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned something that every DevOps engineer eventually discovers the hard way: &lt;strong&gt;Terraform state is like your infrastructure's diary, and if you don't protect it, your team will pay the price.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Remember when I thought storing &lt;code&gt;terraform.tfstate&lt;/code&gt; locally was fine? That was Day 1 me. Naive. Innocent. About to learn a valuable lesson.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: What's Actually in That State File?
&lt;/h2&gt;

&lt;p&gt;Before today, I treated &lt;code&gt;terraform.tfstate&lt;/code&gt; like a mysterious black box. "It's there, it works, don't touch it." But today, I opened it. And what I found surprised me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's what a state file actually contains:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instances"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"arn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-demo-bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-demo-bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eu-north-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Learning"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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;What I found inside:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource ARNs&lt;/strong&gt; — the unique identifiers AWS assigns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP addresses&lt;/strong&gt; — public and private IPs of every instance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tags&lt;/strong&gt; — all the metadata I thought was just for organization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies&lt;/strong&gt; — Terraform knows which resources depend on which&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensitive data&lt;/strong&gt; — secrets, keys, and credentials (in plaintext!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The scary part:&lt;/strong&gt; If I committed this to Git (which I was doing before Day 6), anyone with access to my repo would have seen everything about my infrastructure. Every IP. Every ARN. Every. Single. Detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: The Bootstrap Problem — Terraform's Chicken-and-Egg
&lt;/h2&gt;

&lt;p&gt;Here's a fun paradox: You need Terraform to create the infrastructure that stores Terraform's state. But you need state to run Terraform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Bootstrap Problem:&lt;/strong&gt; How do you create the S3 bucket and DynamoDB table for remote state without already having remote state?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; Create them manually (or with a separate, simpler Terraform configuration) first.&lt;/p&gt;

&lt;p&gt;I created a &lt;strong&gt;bootstrap configuration&lt;/strong&gt; that deployed just the S3 bucket and DynamoDB table with local state. Once those were up, I could reconfigure my main infrastructure to use them as a remote backend.&lt;/p&gt;

&lt;p&gt;It's like building a ladder to build a house. You need something to stand on while you construct the real thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: Remote State — Your Infrastructure's Safe Haven
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before (Local State):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State lived on my laptop&lt;/li&gt;
&lt;li&gt;Team members couldn't see it&lt;/li&gt;
&lt;li&gt;Concurrent runs = corruption&lt;/li&gt;
&lt;li&gt;If my laptop died, state died&lt;/li&gt;
&lt;li&gt;Secrets in plaintext on my hard drive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After (Remote State with S3 + DynamoDB):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State lives in S3 (versioned, encrypted)&lt;/li&gt;
&lt;li&gt;Team members share the same state&lt;/li&gt;
&lt;li&gt;Locking prevents concurrent runs&lt;/li&gt;
&lt;li&gt;Versioning means I can recover from mistakes&lt;/li&gt;
&lt;li&gt;Encryption keeps secrets safe&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My Remote Backend Configuration:
&lt;/h3&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;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-team-terraform-state"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform.tfstate"&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;"eu-north-1"&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-state-locks"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&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;p&gt;&lt;strong&gt;What each argument does:&lt;/strong&gt;&lt;br&gt;
| Argument | Purpose |&lt;br&gt;
|----------|---------|&lt;br&gt;
| &lt;code&gt;bucket&lt;/code&gt; | Where the state file lives (S3) |&lt;br&gt;
| &lt;code&gt;key&lt;/code&gt; | The path/filename in the bucket |&lt;br&gt;
| &lt;code&gt;region&lt;/code&gt; | Where the bucket lives |&lt;br&gt;
| &lt;code&gt;dynamodb_table&lt;/code&gt; | Locking mechanism (critical for teams) |&lt;br&gt;
| &lt;code&gt;encrypt&lt;/code&gt; | Server-side encryption at rest |&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 4: State Locking — The Team Player's Best Friend 🔒
&lt;/h2&gt;

&lt;p&gt;Remember the school project where two people edited the same Google Doc at the same time? Chaos, right?&lt;/p&gt;

&lt;p&gt;State locking prevents that exact scenario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I tested it with two terminals:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terminal 1:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;terraform apply
&lt;span class="c"&gt;# Running...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Terminal 2 (while apply was running):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

╷
│ Error: Error acquiring the state lock
│ 
│ Lock Info:
│   ID:        abc123-def456-ghi789
│   Path:      my-bucket/terraform.tfstate
│   Operation: OperationTypeApply
│   Who:       user@computer
│   Created:   2026-03-22 10:30:45 UTC
│ 
│ Terraform acquires a state lock to protect the state from being written
│ by multiple &lt;span class="nb"&gt;users &lt;/span&gt;at the same time.
╵
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this means:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform knows when someone else is already working&lt;/li&gt;
&lt;li&gt;It refuses to run until the lock is released&lt;/li&gt;
&lt;li&gt;No two people can corrupt the state simultaneously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In a team environment, this is non-negotiable.&lt;/strong&gt; Without locking, you're one simultaneous &lt;code&gt;terraform apply&lt;/code&gt; away from infrastructure chaos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Why State Files Should NEVER Go in Git 🚫
&lt;/h2&gt;

&lt;p&gt;I used to commit &lt;code&gt;terraform.tfstate&lt;/code&gt; to Git. I was wrong. Here's why:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Explanation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Secrets in Plaintext&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;State files contain passwords, access keys, and database credentials in plaintext&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Merge Conflicts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Two engineers committing state = unresolvable conflicts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No Locking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Git doesn't prevent concurrent writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Large Files&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;State files grow huge and bloat the repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audit Issues&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Git history doesn't reflect actual infrastructure changes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The correct approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store state in &lt;strong&gt;S3&lt;/strong&gt; (encrypted, versioned)&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;DynamoDB&lt;/strong&gt; for locking&lt;/li&gt;
&lt;li&gt;Commit only your &lt;strong&gt;code&lt;/strong&gt; to Git&lt;/li&gt;
&lt;li&gt;Let the state live safely in the cloud&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 6: The Migration Experience 🚀
&lt;/h2&gt;

&lt;p&gt;When I added the backend configuration and ran &lt;code&gt;terraform init&lt;/code&gt;, something magical happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "s3" backend. Enter "yes" to copy and "no" to start empty.

  Enter a value: yes

Successfully configured the backend "s3"!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform detected my local state and offered to migrate it to S3. I said yes, and seconds later, my state file was safely in the cloud, encrypted, versioned, and ready for team collaboration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No manual copying. No complex scripts. Just Terraform being Terraform.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7: What I Learned About State That Changed Everything 💡
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;State is the source of truth&lt;/strong&gt; — Not your code, not the AWS console. The state file is what Terraform believes exists.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Drift detection is automatic&lt;/strong&gt; — If someone manually changes infrastructure, &lt;code&gt;terraform plan&lt;/code&gt; will show you exactly what's different.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;S3 versioning is your safety net&lt;/strong&gt; — Accidentally corrupted your state? Roll back to the previous version. It's like &lt;code&gt;git revert&lt;/code&gt; for your infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Encryption isn't optional&lt;/strong&gt; — State files contain secrets. &lt;code&gt;encrypt = true&lt;/code&gt; should be the default, always.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The bootstrap problem is solvable&lt;/strong&gt; — Create your backend infrastructure first (manually or with a separate config), then migrate.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Best Practices Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Practice&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Store state remotely (S3)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Team access, disaster recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enable versioning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Roll back from mistakes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enable encryption&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Protect secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use DynamoDB for locking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prevent concurrent corruption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Never commit state to Git&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Avoid secrets exposure and merge conflicts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protect the bucket with &lt;code&gt;prevent_destroy&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Accidental bucket deletion = lost state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Block public access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;State should never be publicly readable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;Day 6 taught me that Terraform isn't just about writing code — it's about managing the &lt;strong&gt;state&lt;/strong&gt; that code creates. &lt;/p&gt;

&lt;p&gt;Local state is fine for learning. But the moment you work with a team (or even just a second laptop), you need remote state with locking.&lt;/p&gt;

&lt;p&gt;I started today thinking "state is just a file." I'm ending today with a full S3 + DynamoDB backend that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Stores state securely&lt;/li&gt;
&lt;li&gt;✅ Prevents concurrent corruption&lt;/li&gt;
&lt;li&gt;✅ Encrypts everything&lt;/li&gt;
&lt;li&gt;✅ Keeps version history&lt;/li&gt;
&lt;li&gt;✅ Never touches Git&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you're still storing state locally in a team environment, you are one concurrent run away from disaster. Fix it today.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. If you're wondering why I didn't just use &lt;code&gt;terraform apply&lt;/code&gt; for the bootstrap — that's the bootstrap problem! You can't use Terraform to create the infrastructure that Terraform itself needs. I had to create the bucket and table first (manually), then migrate. Mind-bending, but it works. 🧠&lt;/em&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>terraform</category>
      <category>tutorial</category>
      <category>tfstate</category>
    </item>
  </channel>
</rss>
