<?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>Forem: Mukami</title>
    <description>The latest articles on Forem by Mukami (@tink-origami).</description>
    <link>https://forem.com/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>Forem: Mukami</title>
      <link>https://forem.com/tink-origami</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tink-origami"/>
    <language>en</language>
    <item>
      <title>Deploying a Static Website on AWS S3 with Terraform: A Beginner's Guide</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Thu, 16 Apr 2026 07:57:46 +0000</pubDate>
      <link>https://forem.com/tink-origami/deploying-a-static-website-on-aws-s3-with-terraform-a-beginners-guide-1gf5</link>
      <guid>https://forem.com/tink-origami/deploying-a-static-website-on-aws-s3-with-terraform-a-beginners-guide-1gf5</guid>
      <description>&lt;h2&gt;
  
  
  From Zero to a Live, Globally Distributed Website in One Day
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 25 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I built something I can actually show people.&lt;/p&gt;

&lt;p&gt;Not just infrastructure. Not just a cluster that responds to curl. An actual website. With a URL. That works in a browser.&lt;/p&gt;

&lt;p&gt;Using nothing but Terraform code.&lt;/p&gt;




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

&lt;p&gt;A fully serverless static website hosted on AWS S3, served globally through CloudFront, with HTTPS, custom error pages, and environment isolation.&lt;/p&gt;

&lt;p&gt;All deployed with one command: &lt;code&gt;terraform apply&lt;/code&gt;&lt;/p&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → CloudFront (HTTPS) → S3 Bucket (Static Files)
                              ├── index.html
                              └── error.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 bucket&lt;/strong&gt; stores the HTML files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucket policy&lt;/strong&gt; makes the files publicly readable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; distributes content globally and adds HTTPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; manages everything as code&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;day25-static-website/
├── modules/
│   └── s3-static-website/
│       ├── main.tf          # S3 bucket + CloudFront
│       ├── variables.tf     # Configurable inputs
│       └── outputs.tf       # CloudFront URL
├── envs/
│   └── dev/
│       ├── main.tf          # Module call
│       ├── variables.tf
│       └── terraform.tfvars # Environment config
├── backend.tf               # Remote state
└── provider.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure enforces &lt;strong&gt;DRY&lt;/strong&gt; — the module is reusable, and the environment configuration is minimal.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Module (Reusable)
&lt;/h2&gt;

&lt;p&gt;The module encapsulates everything needed for a static website:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# modules/s3-static-website/main.tf&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;"website"&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;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_name&lt;/span&gt;
  &lt;span class="nx"&gt;force_destroy&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="err"&gt;!&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;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_website_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"website"&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;website&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;index_document&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;suffix&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;index_document&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;error_document&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error_document&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_cloudfront_distribution"&lt;/span&gt; &lt;span class="s2"&gt;"website"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;domain_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket_website_configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;website&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;website_endpoint&lt;/span&gt;
    &lt;span class="nx"&gt;origin_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-website"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;default_cache_behavior&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;viewer_protocol_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"redirect-to-https"&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;Key design decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;force_destroy = var.environment != "production"&lt;/code&gt; — dev buckets can be destroyed easily, production buckets are protected&lt;/li&gt;
&lt;li&gt;CloudFront adds HTTPS automatically — no certificate needed for the default domain&lt;/li&gt;
&lt;li&gt;Module outputs the CloudFront URL so callers can access it&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Environment Configuration (Clean)
&lt;/h2&gt;

&lt;p&gt;Because the module does all the heavy lifting, the dev environment config is tiny:&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;# envs/dev/main.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"static_website"&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/s3-static-website"&lt;/span&gt;

  &lt;span class="nx"&gt;bucket_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;bucket_name&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;index_document&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"index.html"&lt;/span&gt;
  &lt;span class="nx"&gt;error_document&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"error.html"&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;"cloudfront_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="s2"&gt;"https://${module.static_website.cloudfront_domain_name}"&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 hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# envs/dev/terraform.tfvars&lt;/span&gt;
&lt;span class="nx"&gt;bucket_name&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-terraform-website-day25-20260416"&lt;/span&gt;
&lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. All the complexity lives in the module.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Deployment
&lt;/h2&gt;



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

Apply &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; Resources: 6 added, 0 changed, 0 destroyed.

Outputs:
cloudfront_url &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://d123.cloudfront.net"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;After waiting 10 minutes for CloudFront to propagate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://d123.cloudfront.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;&lt;/span&gt;Terraform Static Website&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;🚀 Day 25: Deployed with Terraform!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Environment: dev&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This website was deployed using Terraform on Day 25!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A live, globally distributed website. Deployed entirely through code.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;It's real.&lt;/strong&gt; Not a demo. Not a "hello world" that only works on localhost. A real website with a real URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's complete.&lt;/strong&gt; S3 + CloudFront + HTTPS + error handling + environment isolation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's reusable.&lt;/strong&gt; The module can deploy dev, staging, and production with different variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It demonstrates everything from Days 1-24:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modules (Day 8-9)&lt;/li&gt;
&lt;li&gt;Remote state (Day 6)&lt;/li&gt;
&lt;li&gt;DRY principle (Day 4)&lt;/li&gt;
&lt;li&gt;Environment isolation (Day 7)&lt;/li&gt;
&lt;li&gt;Tags and best practices (Day 16)&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;S3 static websites are simple but have limits.&lt;/strong&gt; No HTTPS on the S3 endpoint — that's why you need CloudFront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudFront takes time.&lt;/strong&gt; 5-15 minutes to propagate globally. Be patient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The module pattern is powerful.&lt;/strong&gt; I can now deploy a static website in any environment with 5 lines of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remote state protects your work.&lt;/strong&gt; The state file is encrypted in S3, not on my laptop.&lt;/p&gt;




&lt;h2&gt;
  
  
  The DRY Principle in Practice
&lt;/h2&gt;

&lt;p&gt;Without a module, I would have written 100+ lines of S3 + CloudFront configuration for every environment. With a module, each environment needs only 5 lines.&lt;/p&gt;

&lt;p&gt;That's the difference between a one-off script and production-grade infrastructure.&lt;/p&gt;




</description>
      <category>terraform</category>
      <category>aws</category>
      <category>s3</category>
      <category>static</category>
    </item>
    <item>
      <title>My Final Preparation for the Terraform Associate Exam</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Mon, 13 Apr 2026 15:15:27 +0000</pubDate>
      <link>https://forem.com/tink-origami/my-final-preparation-for-the-terraform-associate-exam-45go</link>
      <guid>https://forem.com/tink-origami/my-final-preparation-for-the-terraform-associate-exam-45go</guid>
      <description>&lt;h2&gt;
  
  
  60 Minutes. 57 Questions. No Looking Back.
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 24 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I did something I've been dreading for weeks.&lt;/p&gt;

&lt;p&gt;I simulated the real exam.&lt;/p&gt;

&lt;p&gt;No notes. No pauses. No second chances to look something up.&lt;/p&gt;

&lt;p&gt;Just me, 57 questions, and a 60-minute timer.&lt;/p&gt;

&lt;p&gt;Here's what happened, what I learned, and my plan for exam day.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Simulation Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Score:&lt;/strong&gt; 43/57 (75%) — just above the 70% passing threshold&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time remaining:&lt;/strong&gt; 4 minutes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Questions flagged for review:&lt;/strong&gt; 12&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domains I struggled with:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform CLI (26% weight) — missed 5 questions&lt;/li&gt;
&lt;li&gt;State management (8% weight) — missed 3 questions&lt;/li&gt;
&lt;li&gt;Terraform Cloud (4% weight) — missed 2 questions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CLI section is what almost got me. The questions about &lt;code&gt;-target&lt;/code&gt;, &lt;code&gt;-refresh-only&lt;/code&gt;, and &lt;code&gt;terraform state mv&lt;/code&gt; were harder than I expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned From My Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The CLI traps:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform plan -target&lt;/code&gt; doesn't just plan the targeted resource — it also plans any resources that depend on it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform state mv&lt;/code&gt; requires the full resource address, not just the name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform apply -refresh-only&lt;/code&gt; updates state to match real infrastructure without changing anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The state traps:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A stale state file can cause &lt;code&gt;terraform plan&lt;/code&gt; to show no changes even when infrastructure has drifted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform refresh&lt;/code&gt; is deprecated — use &lt;code&gt;terraform apply -refresh-only&lt;/code&gt; instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Terraform Cloud traps:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sentinel policies run after plan, before apply&lt;/li&gt;
&lt;li&gt;Workspaces in TFC are separate state files, not separate directories&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Flash Card Answers (Without Looking)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. What file does &lt;code&gt;terraform init&lt;/code&gt; create to record provider versions?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;.terraform.lock.hcl&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Difference between &lt;code&gt;terraform.workspace&lt;/code&gt; and a Terraform Cloud workspace?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;terraform.workspace&lt;/code&gt; is an expression used in config to get current workspace name. TFC workspace is a separate state file with collaboration features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. If you run &lt;code&gt;terraform state rm aws_instance.web&lt;/code&gt;, what happens to the EC2 instance?&lt;/strong&gt;&lt;br&gt;
Nothing — it continues running. Only removed from state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. What does &lt;code&gt;depends_on&lt;/code&gt; do and when should you use it?&lt;/strong&gt;&lt;br&gt;
Creates explicit dependency when Terraform can't infer it. Use when a resource needs to wait for another that isn't referenced in its arguments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Purpose of &lt;code&gt;.terraform.lock.hcl&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
Locks provider versions so every team member uses the same version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. How does &lt;code&gt;for_each&lt;/code&gt; differ from &lt;code&gt;count&lt;/code&gt; when items are removed from middle?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;count&lt;/code&gt; reindexes and recreates subsequent resources. &lt;code&gt;for_each&lt;/code&gt; keys by value, so only removed item is affected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. What does &lt;code&gt;terraform apply -refresh-only&lt;/code&gt; do?&lt;/strong&gt;&lt;br&gt;
Updates state file to match real infrastructure without modifying resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Maximum items in a single &lt;code&gt;terraform import&lt;/code&gt; command?&lt;/strong&gt;&lt;br&gt;
One — you can only import one resource at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. What happens when you run &lt;code&gt;terraform plan&lt;/code&gt; against a workspace that has never been applied?&lt;/strong&gt;&lt;br&gt;
It shows all resources as "to be created" since state is empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. What does &lt;code&gt;prevent_destroy&lt;/code&gt; do and what does it NOT prevent?&lt;/strong&gt;&lt;br&gt;
Blocks &lt;code&gt;terraform destroy&lt;/code&gt; from deleting the resource. Does NOT prevent manual deletion in AWS Console.&lt;/p&gt;




&lt;h2&gt;
  
  
  High-Weight Domain Drill
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Terraform basics (24%):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;templatefile()&lt;/code&gt; reads a template file and renders it with variables — useful for user_data scripts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;merge()&lt;/code&gt; combines multiple maps; later keys overwrite earlier ones&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tomap()&lt;/code&gt; converts a list of objects to a map, but fails if keys aren't unique&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Terraform CLI (26%):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform init -upgrade&lt;/code&gt; forces provider version upgrades even when lock file pins them&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform apply -auto-approve&lt;/code&gt; skips interactive approval — never use in production&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform plan -out=file.tfplan&lt;/code&gt; saves plan to apply exactly later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IaC concepts (16%):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declarative = describe desired state, system figures out how to achieve it&lt;/li&gt;
&lt;li&gt;Idempotency = applying same config multiple times produces same result&lt;/li&gt;
&lt;li&gt;Drift = difference between declared state and actual state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;State management (8%):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State is the source of truth — Terraform compares config to state, not directly to AWS&lt;/li&gt;
&lt;li&gt;State locking prevents concurrent writes using DynamoDB&lt;/li&gt;
&lt;li&gt;State versioning in S3 allows recovery from corruption&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common Exam Traps (I Added 3 More)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. "Terraform plan shows no changes" doesn't mean infrastructure is correct&lt;/strong&gt; — stale state can mask drift&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;terraform destroy&lt;/code&gt; vs &lt;code&gt;terraform state rm&lt;/code&gt;&lt;/strong&gt; — destroy deletes real resources, state rm only removes from state&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Module source &lt;code&gt;?ref=main&lt;/code&gt; vs &lt;code&gt;?ref=v1.0.0&lt;/code&gt;&lt;/strong&gt; — branch is mutable, tag is immutable. Always pin to tags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;sensitive = true&lt;/code&gt; does NOT prevent secrets from being stored in state&lt;/strong&gt; — only masks terminal output&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Multi-select questions&lt;/strong&gt; — if it says "select TWO," exactly two. One or three = wrong regardless of which you picked&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. &lt;code&gt;terraform plan -target&lt;/code&gt; includes dependencies&lt;/strong&gt; — not just the targeted resource&lt;/p&gt;




&lt;h2&gt;
  
  
  Exam-Day Strategy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the question twice&lt;/strong&gt; before looking at answers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend max 90 seconds per question&lt;/strong&gt; — flag and move on if stuck&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eliminate clearly wrong answers first&lt;/strong&gt; — often gets you from 4 to 2 options&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flag any question you're unsure about&lt;/strong&gt; — don't waste time overthinking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer all flagged questions in remaining time&lt;/strong&gt; — even guessing beats leaving blank&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for "select TWO" instructions&lt;/strong&gt; — don't over-select or under-select&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust your gut on first pass&lt;/strong&gt; — your first instinct is usually right&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Remaining Red Topics
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Terraform Cloud features&lt;/strong&gt; — still red. I've used S3 backend, not TFC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I'll address this:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete TFC "Get Started" tutorials (1 hour)&lt;/li&gt;
&lt;li&gt;Create a free TFC account and run a remote plan&lt;/li&gt;
&lt;li&gt;Write 10 practice questions about TFC features&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;75% on my first simulation. Not great. Not failing.&lt;/p&gt;

&lt;p&gt;The CLI section is my biggest risk. The questions about specific flags and edge cases are harder than I expected.&lt;/p&gt;

&lt;p&gt;But I know exactly where my gaps are now. And I have six days to close them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whatever the score is on exam day, I know this material better than I did 24 days ago.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's go.&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://developer.hashicorp.com/terraform/certification/associate" rel="noopener noreferrer"&gt;Official Study Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/certification/associate/sample-questions" rel="noopener noreferrer"&gt;Official Sample Questions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devjournal</category>
      <category>devops</category>
      <category>learning</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Preparing for the Terraform Associate Exam — Key Resources and Tips</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Mon, 13 Apr 2026 06:47:25 +0000</pubDate>
      <link>https://forem.com/tink-origami/preparing-for-the-terraform-associate-exam-key-resources-and-tips-4146</link>
      <guid>https://forem.com/tink-origami/preparing-for-the-terraform-associate-exam-key-resources-and-tips-4146</guid>
      <description>&lt;h2&gt;
  
  
  How I Audited My Gaps and Built a Study Plan
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 23 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I shifted from building infrastructure to preparing for the certification.&lt;/p&gt;

&lt;p&gt;After 22 days of hands-on work, I thought I was ready. Then I looked at the official exam objectives.&lt;/p&gt;

&lt;p&gt;I wasn't as ready as I thought.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Domain Audit
&lt;/h2&gt;

&lt;p&gt;The exam covers 9 domains with different weights. Here's my honest self-assessment:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;My Rating&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IaC concepts&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;🟢 Green&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terraform's purpose&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;🟢 Green&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terraform basics&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;td&gt;🟢 Green&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terraform CLI&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;td&gt;🟡 Yellow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modules&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;🟢 Green&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core workflow&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;🟢 Green&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State management&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;🟡 Yellow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuration&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;🟢 Green&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terraform Cloud&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;🔴 Red&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Green&lt;/strong&gt; = I can explain and have done it hands-on.&lt;br&gt;
&lt;strong&gt;Yellow&lt;/strong&gt; = I understand conceptually but need practice.&lt;br&gt;
&lt;strong&gt;Red&lt;/strong&gt; = I need to learn this.&lt;/p&gt;

&lt;p&gt;The CLI and state sections are more detailed than I expected. Terraform Cloud is almost entirely new to me.&lt;/p&gt;


&lt;h2&gt;
  
  
  CLI Commands — Know Them Cold
&lt;/h2&gt;

&lt;p&gt;The exam tests specific command flags. You need to know what each command does and when to use it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Downloads providers, initializes backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform validate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Checks syntax and consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform fmt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Formats code to canonical style&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows proposed changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates or updates infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform destroy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes all resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform output&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reads outputs from state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists resources in state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state show&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows resource attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state mv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Moves resource between states&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state rm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes from state (no destroy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Imports existing resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform workspace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Manages multiple state files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform providers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows provider versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform graph&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Outputs dependency graph&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trickiest is understanding what &lt;code&gt;terraform state rm&lt;/code&gt; does to real infrastructure — nothing. It only removes from state.&lt;/p&gt;


&lt;h2&gt;
  
  
  Non-Cloud Providers
&lt;/h2&gt;

&lt;p&gt;Terraform isn't just for AWS. The random and local providers appear frequently on the exam:&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;"suffix"&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;"random_password"&lt;/span&gt; &lt;span class="s2"&gt;"db_pass"&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="mi"&gt;16&lt;/span&gt;
  &lt;span class="nx"&gt;special&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;"local_file"&lt;/span&gt; &lt;span class="s2"&gt;"config"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Password: ${random_password.db_pass.result}"&lt;/span&gt;
  &lt;span class="nx"&gt;filename&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/config.txt"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; The exam expects you to know Terraform works with DNS, TLS, random values, and local files — not just cloud providers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Terraform Cloud — My Biggest Gap
&lt;/h2&gt;

&lt;p&gt;I used S3 + DynamoDB for remote state. Terraform Cloud has features I haven't touched:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remote runs (vs local runs)&lt;/li&gt;
&lt;li&gt;Sentinel policies (run after plan, before apply)&lt;/li&gt;
&lt;li&gt;Private module registry&lt;/li&gt;
&lt;li&gt;Workspace-specific variables&lt;/li&gt;
&lt;li&gt;Cost estimation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is only 4% of the exam, but it's 100% new to me.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Study Plan (Days 24-30)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Study Method&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;terraform state commands&lt;/td&gt;
&lt;td&gt;Run each command on test resources&lt;/td&gt;
&lt;td&gt;45 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentinel policies&lt;/td&gt;
&lt;td&gt;Read docs + write 2 policies&lt;/td&gt;
&lt;td&gt;60 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terraform Cloud&lt;/td&gt;
&lt;td&gt;Complete TFC lab exercises&lt;/td&gt;
&lt;td&gt;90 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI flags (-out, -target)&lt;/td&gt;
&lt;td&gt;Practice each flag&lt;/td&gt;
&lt;td&gt;45 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random/local providers&lt;/td&gt;
&lt;td&gt;Build example configs&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official sample questions&lt;/td&gt;
&lt;td&gt;Complete all&lt;/td&gt;
&lt;td&gt;60 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Practice Questions — Write Your Own
&lt;/h2&gt;

&lt;p&gt;Writing questions forces you to understand the material deeply. Here's one I wrote:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; You run &lt;code&gt;terraform state rm aws_instance.web&lt;/code&gt;. What happens to the actual EC2 instance?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A) Terminated immediately&lt;/li&gt;
&lt;li&gt;B) Removed from state only (continues running) ✅&lt;/li&gt;
&lt;li&gt;C) Stopped&lt;/li&gt;
&lt;li&gt;D) Removed from state and terminated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why the wrong answers are wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A, C, D describe actions that affect real infrastructure&lt;/li&gt;
&lt;li&gt;Only B correctly states that &lt;code&gt;state rm&lt;/code&gt; affects only the state file&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Official Sample Questions
&lt;/h2&gt;

&lt;p&gt;I scored 8/10 on my first attempt. The two I missed were about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sentinel policy execution timing (runs after plan, before apply)&lt;/li&gt;
&lt;li&gt;Terraform Cloud workspace vs directory structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These went straight into my study plan.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;The CLI section is more detailed than most people expect.&lt;/strong&gt; You need to know specific flags, not just commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-cloud providers are tested.&lt;/strong&gt; Don't skip the random and local providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform Cloud is different from open source.&lt;/strong&gt; If you only used S3 backend, you need to study TFC separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing practice questions is the best study technique.&lt;/strong&gt; It forces you to think like the exam writer.&lt;/p&gt;




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

&lt;p&gt;The exam tests specific knowledge, not just experience. You can build infrastructure for weeks and still miss questions about &lt;code&gt;terraform state mv&lt;/code&gt; or Sentinel policy syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My advice:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Print the official study guide&lt;/li&gt;
&lt;li&gt;Audit yourself honestly against every objective&lt;/li&gt;
&lt;li&gt;Build a concrete study plan for your gaps&lt;/li&gt;
&lt;li&gt;Write your own practice questions&lt;/li&gt;
&lt;li&gt;Don't underestimate the CLI section&lt;/li&gt;
&lt;/ol&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/certification/associate" rel="noopener noreferrer"&gt;Official Study Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/certification/associate/sample-questions" rel="noopener noreferrer"&gt;Official Sample Questions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/cli/commands" rel="noopener noreferrer"&gt;Terraform CLI Commands&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>exam</category>
      <category>terraform</category>
      <category>30daychallenge</category>
    </item>
    <item>
      <title>Putting It All Together: My 22-Day Terraform Journey</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Mon, 13 Apr 2026 06:26:35 +0000</pubDate>
      <link>https://forem.com/tink-origami/putting-it-all-together-my-22-day-terraform-journey-hlh</link>
      <guid>https://forem.com/tink-origami/putting-it-all-together-my-22-day-terraform-journey-hlh</guid>
      <description>&lt;h2&gt;
  
  
  From "What's a provider?" to a Complete CI/CD Pipeline
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 22 of the 30-Day Terraform Challenge&lt;/strong&gt; — and I can't believe how far I've come.&lt;/p&gt;

&lt;p&gt;Twenty-two days ago, I didn't know what a provider alias was. I hardcoded instance types. I thought &lt;code&gt;terraform destroy&lt;/code&gt; was the scariest command in the world.&lt;/p&gt;

&lt;p&gt;Today, I have a complete integrated pipeline. And I'm about to finish the book.&lt;/p&gt;

&lt;p&gt;Here's what I built. What I learned. And what surprised me most.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Journey in 22 Days
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Days&lt;/th&gt;
&lt;th&gt;What I Built&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Introduction to the challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Setting up AWS CLI, credentials, Terraform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;First EC2 instance with user data script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Configurable web server with variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-6&lt;/td&gt;
&lt;td&gt;Auto Scaling Group + Application Load Balancer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Remote state with S3 + DynamoDB locking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8-9&lt;/td&gt;
&lt;td&gt;Reusable modules (webserver cluster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Loops (&lt;code&gt;count&lt;/code&gt;, &lt;code&gt;for_each&lt;/code&gt;) and conditionals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Environment-aware module (dev vs prod)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Zero-downtime deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Secrets management with AWS Secrets Manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14-15&lt;/td&gt;
&lt;td&gt;Multiple providers (multi-region, EKS, Docker)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Production-grade refactor (tags, alarms, validation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;Manual testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;Automated testing (terraform test + Terratest)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;IaC adoption strategy for teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-21&lt;/td&gt;
&lt;td&gt;Application + infrastructure deployment workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;Integrated CI/CD pipeline + Sentinel policies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a complete infrastructure platform. Most engineers don't build this much in their first year.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built (The Highlights)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Week 1 (Days 1-7):&lt;/strong&gt; I went from a single hardcoded EC2 instance to a configurable, load-balanced, auto-scaling cluster with remote state. The jump from Day 3 to Day 5 was the biggest learning curve — understanding ASGs and ALBs took hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2 (Days 8-14):&lt;/strong&gt; I built reusable modules, added conditionals for multi-environment deployments, learned zero-downtime with &lt;code&gt;create_before_destroy&lt;/code&gt;, and deployed an EKS cluster with Kubernetes. The EKS section was the most complex — 68 resources, 15 minutes of waiting, and a lot of coffee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3 (Days 15-22):&lt;/strong&gt; I refactored everything to production-grade standards (tags, alarms, validation), wrote manual and automated tests, designed an adoption strategy for teams, and finally built an integrated CI/CD pipeline with Sentinel policies.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Integrated Pipeline
&lt;/h2&gt;

&lt;p&gt;My final workflow combines everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Infrastructure CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform fmt -check -recursive&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init -backend=false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform validate&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform test&lt;/span&gt;

  &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;validate&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform plan -out=ci.tfplan&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-plan&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci.tfplan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Format check on every PR&lt;/li&gt;
&lt;li&gt;Validation and unit tests&lt;/li&gt;
&lt;li&gt;Plan generation saved as immutable artifact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same plan can be promoted from dev to prod — no regeneration, no drift.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sentinel Policies
&lt;/h2&gt;

&lt;p&gt;I wrote two policies to enforce standards:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policy 1: Require ManagedBy tag&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;main&lt;/span&gt; &lt;span class="err"&gt;=&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;all&lt;/span&gt; &lt;span class="nx"&gt;tfplan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_changes&lt;/span&gt; &lt;span class="nx"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ManagedBy"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="s2"&gt;"terraform"&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;Policy 2: Allow only approved instance types&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;allowed_types&lt;/span&gt; &lt;span class="err"&gt;=&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="s2"&gt;"t3.small"&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;These caught several violations in my early code — security groups without tags, t2.micro instances — and forced me to fix them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Side-by-Side Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Application Code&lt;/th&gt;
&lt;th&gt;Infrastructure Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;Git repository&lt;/td&gt;
&lt;td&gt;Git repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local run&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact&lt;/td&gt;
&lt;td&gt;Docker image&lt;/td&gt;
&lt;td&gt;Saved &lt;code&gt;.tfplan&lt;/code&gt; file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Versioning&lt;/td&gt;
&lt;td&gt;Semantic tag&lt;/td&gt;
&lt;td&gt;Semantic tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated tests&lt;/td&gt;
&lt;td&gt;Unit + integration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;terraform test&lt;/code&gt; + Terratest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy enforcement&lt;/td&gt;
&lt;td&gt;Linting&lt;/td&gt;
&lt;td&gt;Sentinel policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;CI/CD pipeline&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform apply &amp;lt;plan&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback&lt;/td&gt;
&lt;td&gt;Redeploy previous image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform apply &amp;lt;previous plan&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The parallel is striking. Infrastructure code follows the same discipline as application code.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed in How I Think
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The single biggest mental shift:&lt;/strong&gt; Infrastructure is not a one-time setup. It's a software product.&lt;/p&gt;

&lt;p&gt;Before: "I'll set up the servers once and never touch them again."&lt;/p&gt;

&lt;p&gt;After: "Every infrastructure change goes through version control, review, testing, and CI/CD — just like application code."&lt;/p&gt;

&lt;p&gt;Infrastructure isn't a project. It's a product that needs continuous improvement.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Harder Than Expected
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;State management.&lt;/strong&gt; Not the technical part — the understanding that state is the source of truth. A corrupted state file is worse than corrupted code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing.&lt;/strong&gt; Unit tests were easy. Integration tests (Terratest) were hard — writing Go code, handling timeouts, waiting for ALBs to provision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-region deployments.&lt;/strong&gt; I thought adding a second region would be trivial. Provider aliases, module design, remote state — everything got more complicated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EKS.&lt;/strong&gt; The cluster took 15 minutes to provision, and &lt;code&gt;kubectl&lt;/code&gt; authentication was a nightmare. But it worked.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Week 1:&lt;/strong&gt; Create a &lt;code&gt;.gitignore&lt;/code&gt; before writing any code. I committed state files and provider binaries. Embarrassing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2:&lt;/strong&gt; Write unit tests earlier. I waited until Day 18. Testing from Day 1 would have caught bugs earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3:&lt;/strong&gt; Separate state buckets by environment earlier. One bucket for everything was fine for learning but dangerous for production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overall:&lt;/strong&gt; Read the book before starting the challenge, not during. But that's cheating.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Terraform Associate Certification.&lt;/strong&gt; I've been studying the exam objectives. My weak areas are Terraform Cloud features (I used S3 backend) and Sentinel policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First real project:&lt;/strong&gt; Refactor my team's development environment. Five developers, each with a manually configured AWS sandbox. I'll write a module that provisions identical sandboxes for everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Longer term:&lt;/strong&gt; Contribute to open source Terraform modules. I've learned enough to help others.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 10's Most Important Insight
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Immutable versioned artifacts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same Docker image that passes tests in CI gets promoted to staging, then production. No rebuilding. No "but it worked in dev."&lt;/p&gt;

&lt;p&gt;For infrastructure, the saved &lt;code&gt;.tfplan&lt;/code&gt; file is that immutable artifact. The plan reviewed in the PR is the exact plan applied in production. No drift. No surprises.&lt;/p&gt;

&lt;p&gt;This insight changes everything. Infrastructure deployment becomes predictable.&lt;/p&gt;




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

&lt;p&gt;Twenty-two days ago, I was afraid of &lt;code&gt;terraform destroy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Today, I trust it because I know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State is backed up in versioned S3&lt;/li&gt;
&lt;li&gt;Plans are reviewed before apply&lt;/li&gt;
&lt;li&gt;Tests run automatically&lt;/li&gt;
&lt;li&gt;Sentinel enforces rules&lt;/li&gt;
&lt;li&gt;Rollback is one command away&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure as Code isn't just a tool. It's a discipline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And it's one I'll carry into every project from now on.&lt;/p&gt;




</description>
      <category>aws</category>
      <category>beginners</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>A Workflow for Deploying Infrastructure Code with Terraform</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Sun, 12 Apr 2026 15:08:49 +0000</pubDate>
      <link>https://forem.com/tink-origami/a-workflow-for-deploying-infrastructure-code-with-terraform-40p6</link>
      <guid>https://forem.com/tink-origami/a-workflow-for-deploying-infrastructure-code-with-terraform-40p6</guid>
      <description>&lt;h2&gt;
  
  
  Seven Steps. One Plan File. Zero Surprises.
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 21 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned that deploying infrastructure code safely requires discipline that application deployments don't need.&lt;/p&gt;

&lt;p&gt;Yesterday I mapped the seven-step application workflow. Today I applied it to infrastructure — and discovered where the two workflows diverge and why those differences matter.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Seven-Step Infrastructure Workflow
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Version control (Git)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Run locally (&lt;code&gt;terraform plan -out&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Make code changes (feature branch)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Submit for review (PR with plan output)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Run automated tests (CI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Merge and release (tag)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Deploy (apply saved plan file)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 1: Version Control
&lt;/h2&gt;

&lt;p&gt;Every infrastructure change starts in Git. Not in the AWS Console.&lt;br&gt;
&lt;/p&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;"Day 21: Initial web server"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Every change has an author, a timestamp, and a reason. No more mystery infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Run Locally — The Infrastructure Difference
&lt;/h2&gt;

&lt;p&gt;For application code, running locally means executing the binary. For infrastructure, it means running &lt;code&gt;terraform plan&lt;/code&gt;.&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;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;day21.tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The critical difference:&lt;/strong&gt; You're not running the code. You're generating a diff against your state file. The plan shows exactly what will change in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Make Code Changes
&lt;/h2&gt;

&lt;p&gt;Create a feature branch and make your change. I added a CloudWatch alarm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; add-cloudwatch-alarm
&lt;span class="c"&gt;# Edit main.tf to add the alarm resource&lt;/span&gt;
git add main.tf
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add CloudWatch CPU alarm"&lt;/span&gt;
git push origin add-cloudwatch-alarm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Submit for Review — The Infrastructure Difference
&lt;/h2&gt;

&lt;p&gt;For application code, you review the code diff. For infrastructure, you review the plan output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My PR description:&lt;/strong&gt;&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="gu"&gt;## What this changes&lt;/span&gt;
Adds a CloudWatch metric alarm for CPU utilization

&lt;span class="gu"&gt;## Terraform plan output&lt;/span&gt;
Plan: 1 to add, 0 to change, 0 to destroy.

&lt;span class="gu"&gt;## Resources affected&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Created: 1 (CloudWatch alarm)

&lt;span class="gu"&gt;## Blast radius&lt;/span&gt;
Low. Only adds monitoring. No impact on existing resources.

&lt;span class="gu"&gt;## Rollback plan&lt;/span&gt;
Remove the alarm resource and re-apply.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; The reviewer sees exactly what will change in production without running Terraform themselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Run Automated Tests
&lt;/h2&gt;

&lt;p&gt;GitHub Actions runs &lt;code&gt;terraform validate&lt;/code&gt;, &lt;code&gt;terraform fmt --check&lt;/code&gt;, and unit tests automatically on every PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Catch syntax and formatting errors before a human ever reviews the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Merge and Release
&lt;/h2&gt;

&lt;p&gt;After approval, merge and tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
git pull origin main
git tag &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"v1.1.0"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add CloudWatch CPU alarm"&lt;/span&gt;
git push origin v1.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Tags create rollback points. Every release is versioned.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Deploy — The Infrastructure Difference
&lt;/h2&gt;

&lt;p&gt;For application code, you deploy from CI. For infrastructure, you apply the saved plan file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply day21.tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The critical difference:&lt;/strong&gt; Using the saved plan guarantees that exactly what was reviewed is what gets applied. No surprises. No drift between plan and apply.&lt;/p&gt;




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

&lt;p&gt;A web server with a CloudWatch alarm:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EC2 instance&lt;/td&gt;
&lt;td&gt;Running, serving HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security group&lt;/td&gt;
&lt;td&gt;Allow port 80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch alarm&lt;/td&gt;
&lt;td&gt;Monitoring CPU &amp;gt; 80%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Verification:&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;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://56.228.18.125
&amp;lt;h1&amp;gt;Day 21: Infrastructure Deployment Workflow&amp;lt;/h1&amp;gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;aws cloudwatch describe-alarms &lt;span class="nt"&gt;--alarm-names&lt;/span&gt; day21-high-cpu
&lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"AlarmName"&lt;/span&gt;: &lt;span class="s2"&gt;"day21-high-cpu"&lt;/span&gt;,
    &lt;span class="s2"&gt;"StateValue"&lt;/span&gt;: &lt;span class="s2"&gt;"OK"&lt;/span&gt;,
    &lt;span class="s2"&gt;"Threshold"&lt;/span&gt;: 80.0
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Infrastructure-Specific Safeguards
&lt;/h2&gt;

&lt;p&gt;These have no equivalent in application code deployment:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Plan File Pinning
&lt;/h3&gt;

&lt;p&gt;Always apply from a saved plan, never from a fresh plan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Correct — apply exactly what was reviewed&lt;/span&gt;
terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reviewed.tfplan
terraform apply reviewed.tfplan

&lt;span class="c"&gt;# Risky — the plan may differ&lt;/span&gt;
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Blast Radius Documentation
&lt;/h3&gt;

&lt;p&gt;Every PR must document what breaks if the apply fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. State Backup
&lt;/h3&gt;

&lt;p&gt;S3 bucket versioning must be enabled. Know how to restore a previous state.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Approval Gates for Destructive Changes
&lt;/h3&gt;

&lt;p&gt;Any plan showing resource destruction requires explicit approval beyond PR review.&lt;/p&gt;




&lt;h2&gt;
  
  
  Infrastructure vs Application: Key Differences
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;th&gt;Why It Exists&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plan files, not binaries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Infrastructure changes affect real resources; plan must be reviewed before apply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Blast radius&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A bad infrastructure deploy can destroy production data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Terraform tracks real resources; state corruption breaks everything&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Most Dangerous Step
&lt;/h2&gt;

&lt;p&gt;The gap between &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt; is where things go wrong. If infrastructure changes between plan and apply, the plan becomes invalid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The safeguard:&lt;/strong&gt; Save the plan file and apply it directly. Never run &lt;code&gt;terraform apply&lt;/code&gt; without a plan 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="c"&gt;# Safe&lt;/span&gt;
terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;day21.tfplan
terraform apply day21.tfplan

&lt;span class="c"&gt;# Risky&lt;/span&gt;
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;&lt;strong&gt;Plan output is the most important review artifact.&lt;/strong&gt; Include it in every PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius matters.&lt;/strong&gt; A bad infrastructure deploy can break more than a bad code deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan file pinning is non-negotiable.&lt;/strong&gt; Without it, you're applying something that might not match what was reviewed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State is the source of truth.&lt;/strong&gt; Protect it with versioning and backups.&lt;/p&gt;




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

&lt;p&gt;Deploying infrastructure code requires the same seven steps as application code — plus infrastructure-specific safeguards.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&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;Console clicks&lt;/td&gt;
&lt;td&gt;Pull requests with plan output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Who changed this?"&lt;/td&gt;
&lt;td&gt;Git blame&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hope it works&lt;/td&gt;
&lt;td&gt;Plan file proves it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can't roll back&lt;/td&gt;
&lt;td&gt;Tags and state versioning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Seven steps. Plan files. Blast radius documentation. State backups.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Infrastructure deployment shouldn't be risky. It should be reviewed, tested, and applied exactly as planned.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. The moment I applied a saved plan file and saw exactly what was reviewed happen in production, I understood why teams adopt this workflow. It's not slower. It's safer.&lt;/em&gt; 🔧&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>terraform</category>
      <category>30daychallenge</category>
      <category>aws</category>
    </item>
    <item>
      <title>A Workflow for Deploying Application Code with Terraform</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Sun, 12 Apr 2026 14:12:01 +0000</pubDate>
      <link>https://forem.com/tink-origami/a-workflow-for-deploying-application-code-with-terraform-59f9</link>
      <guid>https://forem.com/tink-origami/a-workflow-for-deploying-application-code-with-terraform-59f9</guid>
      <description>&lt;h2&gt;
  
  
  Seven Steps from Local Change to Production
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 20 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned that infrastructure deployment should look exactly like application deployment.&lt;/p&gt;

&lt;p&gt;The same seven steps developers trust to ship code safely can be used to ship infrastructure. No more "clicking around in the console." No more "who changed that security group?" No more "it works on my machine."&lt;/p&gt;

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




&lt;h2&gt;
  
  
  The Seven-Step Workflow
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Use version control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Run the code locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Make code changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Submit changes for review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Run automated tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Merge and release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 1: Version Control
&lt;/h2&gt;

&lt;p&gt;Every infrastructure change starts in Git. Not in the AWS Console. Not in a script. Not in a Slack message.&lt;br&gt;
&lt;/p&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 web server - Version 1"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Every change has an author, a timestamp, and a reason. No more mystery changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Run Locally
&lt;/h2&gt;

&lt;p&gt;Before proposing changes, run them locally to see what will happen.&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;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;day20.tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows exactly what will change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which resources will be created&lt;/li&gt;
&lt;li&gt;Which will be modified&lt;/li&gt;
&lt;li&gt;Which will be destroyed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; You catch mistakes before they reach production. Not after.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Make Code Changes
&lt;/h2&gt;

&lt;p&gt;Create a feature branch and make your change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; update-to-v3
&lt;span class="c"&gt;# Edit main.tf&lt;/span&gt;
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;"Update web server to Version 3"&lt;/span&gt;
git push origin update-to-v3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Changes are isolated. Main branch stays deployable. Multiple engineers can work simultaneously.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Submit for Review
&lt;/h2&gt;

&lt;p&gt;Open a pull request on GitHub. Include the &lt;code&gt;terraform plan&lt;/code&gt; output in the description.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Code review catches mistakes. The plan output shows the reviewer exactly what will happen in production — without them running Terraform themselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Run Automated Tests
&lt;/h2&gt;

&lt;p&gt;Your CI pipeline runs &lt;code&gt;terraform validate&lt;/code&gt;, &lt;code&gt;terraform fmt&lt;/code&gt;, and &lt;code&gt;terraform test&lt;/code&gt; automatically on every PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Catch syntax errors, formatting issues, and logic mistakes before a human ever looks at them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Merge and Release
&lt;/h2&gt;

&lt;p&gt;After approval, merge the PR and tag the release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
git pull origin main
git tag &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"v3.0.0"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Version 3 release"&lt;/span&gt;
git push origin v3.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Tags give you a history of what changed when. Rollback is as simple as checking out an old tag.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Deploy
&lt;/h2&gt;

&lt;p&gt;Apply the saved plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply day20.tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify:&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-server.com
&amp;lt;h1&amp;gt;Version 3: Day 20 workflow &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; The plan was reviewed, tested, and approved before apply. No surprises.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Deployed
&lt;/h2&gt;

&lt;p&gt;A simple web server that shows its version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Command&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;v1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Version 1: Hello from Day 20!"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform plan &amp;amp;&amp;amp; apply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tag changed, but user_data didn't run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;user_data_replace_on_change = true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;"Version 3: Day 20 workflow complete!"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third attempt worked because I learned that EC2 instances don't re-run user_data unless you force replacement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Workflow Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Application Code&lt;/th&gt;
&lt;th&gt;Infrastructure Code&lt;/th&gt;
&lt;th&gt;Key Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Version control&lt;/td&gt;
&lt;td&gt;Git for source code&lt;/td&gt;
&lt;td&gt;Git for .tf files&lt;/td&gt;
&lt;td&gt;State file is NOT in Git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run locally&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm start&lt;/code&gt; / &lt;code&gt;python app.py&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Plan shows what will change, not a running app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make changes&lt;/td&gt;
&lt;td&gt;Edit source files&lt;/td&gt;
&lt;td&gt;Edit .tf files&lt;/td&gt;
&lt;td&gt;Changes affect real cloud resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;Code diff in PR&lt;/td&gt;
&lt;td&gt;Plan output in PR&lt;/td&gt;
&lt;td&gt;Reviewer must understand cloud implications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated tests&lt;/td&gt;
&lt;td&gt;Unit tests, linting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Infra tests deploy real resources → cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge and release&lt;/td&gt;
&lt;td&gt;Merge + tag&lt;/td&gt;
&lt;td&gt;Merge + tag&lt;/td&gt;
&lt;td&gt;Module consumers must pin versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;CI/CD pipeline&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apply must be run from trusted, locked environment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Biggest Difference
&lt;/h2&gt;

&lt;p&gt;Application code: You run it and see if it works.&lt;/p&gt;

&lt;p&gt;Infrastructure code: You run &lt;code&gt;plan&lt;/code&gt; and predict what will happen. Then you trust the prediction.&lt;/p&gt;

&lt;p&gt;That trust comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code review&lt;/li&gt;
&lt;li&gt;Automated tests&lt;/li&gt;
&lt;li&gt;A history of successful deploys&lt;/li&gt;
&lt;li&gt;The ability to roll back&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;Plan output is the most powerful review tool.&lt;/strong&gt; Include it in every PR. It shows exactly what will change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User_data doesn't re-run on existing instances.&lt;/strong&gt; You need &lt;code&gt;user_data_replace_on_change = true&lt;/code&gt; or a launch template with &lt;code&gt;create_before_destroy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git workflow works for infrastructure.&lt;/strong&gt; The same seven steps developers trust can be used to deploy infrastructure safely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags matter.&lt;/strong&gt; Every release gets a tag. Every tag is a rollback point.&lt;/p&gt;




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

&lt;p&gt;Infrastructure deployment doesn't need to be risky or mysterious.&lt;/p&gt;

&lt;p&gt;The same seven-step workflow that engineers trust to ship application code works perfectly for Terraform.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&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;Console clicks&lt;/td&gt;
&lt;td&gt;Pull requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Who changed this?"&lt;/td&gt;
&lt;td&gt;Git blame&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual testing&lt;/td&gt;
&lt;td&gt;Automated tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hope it works&lt;/td&gt;
&lt;td&gt;Plan output proves it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can't roll back&lt;/td&gt;
&lt;td&gt;Tags = rollback points&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Version control. Plan. Review. Test. Merge. Tag. Deploy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Seven steps. Every time. No exceptions.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. The moment I saw &lt;code&gt;terraform plan&lt;/code&gt; show exactly what would change, and then &lt;code&gt;curl&lt;/code&gt; confirm it worked, I finally understood why teams adopt this workflow. It's not slower. It's safer.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>30daychallenge</category>
      <category>iac</category>
    </item>
    <item>
      <title>How to Convince Your Team to Adopt Infrastructure as Code</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Sun, 12 Apr 2026 13:28:56 +0000</pubDate>
      <link>https://forem.com/tink-origami/how-to-convince-your-team-to-adopt-infrastructure-as-code-13fj</link>
      <guid>https://forem.com/tink-origami/how-to-convince-your-team-to-adopt-infrastructure-as-code-13fj</guid>
      <description>&lt;h2&gt;
  
  
  The Technical Part Is Easy. The People Part Is Hard.
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 19 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I learned something uncomfortable.&lt;/p&gt;

&lt;p&gt;The technical part of Terraform is the easy part. Writing configurations, managing state, setting up modules — that's straightforward compared to what's actually hard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting a team to change how they work.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Engineers Don't Resist Technology
&lt;/h2&gt;

&lt;p&gt;Engineers don't resist technology. They resist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changing habits they've had for years&lt;/li&gt;
&lt;li&gt;Learning new tools when the old ones "work"&lt;/li&gt;
&lt;li&gt;Slowing down to do things the "right" way&lt;/li&gt;
&lt;li&gt;Trusting automation over their own judgment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're trying to introduce IaC to a team, you're not solving a technical problem. You're solving a people problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Business Case (What Leadership Cares About)
&lt;/h2&gt;

&lt;p&gt;Leadership doesn't care about "better infrastructure." They care about outcomes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Business Problem&lt;/th&gt;
&lt;th&gt;IaC Solution&lt;/th&gt;
&lt;th&gt;Measurable Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure incidents from manual errors&lt;/td&gt;
&lt;td&gt;Code review catches mistakes before apply&lt;/td&gt;
&lt;td&gt;Fewer production outages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hours spent on repetitive environment setup&lt;/td&gt;
&lt;td&gt;Reusable modules provision in minutes&lt;/td&gt;
&lt;td&gt;Engineering time freed for product work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No audit trail for compliance&lt;/td&gt;
&lt;td&gt;Every change is a git commit with author and timestamp&lt;/td&gt;
&lt;td&gt;Full audit trail for auditors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev environments differ from production&lt;/td&gt;
&lt;td&gt;Same config for all environments&lt;/td&gt;
&lt;td&gt;Fewer "works on my machine" incidents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow onboarding for new engineers&lt;/td&gt;
&lt;td&gt;Documented, version-controlled configs&lt;/td&gt;
&lt;td&gt;Faster onboarding time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Frame the conversation around outcomes they already care about.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Most IaC Adoptions Fail
&lt;/h2&gt;

&lt;p&gt;According to the author, the most common reason IaC adoption fails is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trying to do too much at once.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Teams attempt to migrate all their existing infrastructure to Terraform in one big project. It takes months. People get frustrated. Things break. Management loses confidence. The project dies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Start small. Win early. Build momentum.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Incremental Adoption Strategy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1: Start with Something New (2-4 weeks)
&lt;/h3&gt;

&lt;p&gt;Do not migrate existing infrastructure first. Pick one new piece of infrastructure — a new S3 bucket, a new IAM role, a monitoring dashboard — and provision it entirely with Terraform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero migration risk (it's new, not replacing anything)&lt;/li&gt;
&lt;li&gt;Quick win (days, not months)&lt;/li&gt;
&lt;li&gt;Team learns without pressure&lt;/li&gt;
&lt;li&gt;Creates a success story&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Success criteria:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuration is code-reviewed and merged&lt;/li&gt;
&lt;li&gt;Remote state is configured in S3&lt;/li&gt;
&lt;li&gt;Team members can run &lt;code&gt;terraform plan&lt;/code&gt; and understand the output&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Phase 2: Import Existing Infrastructure (4-6 weeks)
&lt;/h3&gt;

&lt;p&gt;Once the team is comfortable with the workflow, begin importing critical existing resources.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: import an existing S3 bucket&lt;/span&gt;
terraform import aws_s3_bucket.existing_logs my-existing-logs-bucket

&lt;span class="c"&gt;# Example: import an existing security group&lt;/span&gt;
terraform import aws_security_group.existing sg-0abc123def456789
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prioritise resources that:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change frequently&lt;/li&gt;
&lt;li&gt;Have caused incidents&lt;/li&gt;
&lt;li&gt;Are well-understood&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do not try to import everything at once.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 3: Establish Team Practices (Ongoing)
&lt;/h3&gt;

&lt;p&gt;Once multiple engineers are writing Terraform, establish the practices that prevent chaos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Module versioning&lt;/li&gt;
&lt;li&gt;Code review requirements for all infrastructure changes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform plan&lt;/code&gt; output as a required part of every PR&lt;/li&gt;
&lt;li&gt;Automated &lt;code&gt;terraform validate&lt;/code&gt; and &lt;code&gt;terraform fmt&lt;/code&gt; in CI&lt;/li&gt;
&lt;li&gt;State locking enforced via DynamoDB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No manual console changes to Terraform-managed resources&lt;/strong&gt; — ever&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Phase 4: Automate Deployments (6-8 weeks)
&lt;/h3&gt;

&lt;p&gt;Connect Terraform to your CI/CD pipeline so that merges to main trigger &lt;code&gt;terraform apply&lt;/code&gt; automatically.&lt;/p&gt;

&lt;p&gt;At this stage, infrastructure changes go through the same review and deployment process as application code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cultural Shift
&lt;/h2&gt;

&lt;p&gt;Technical changes require cultural changes. Here's what needs to shift:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"It works on my machine"&lt;/td&gt;
&lt;td&gt;"It works in the code"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual console changes&lt;/td&gt;
&lt;td&gt;Pull requests for everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blaming the tool&lt;/td&gt;
&lt;td&gt;Blaming the process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heroic fixes&lt;/td&gt;
&lt;td&gt;Reliable rollbacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"I know what changed"&lt;/td&gt;
&lt;td&gt;"The git log knows"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Building Trust in Automation
&lt;/h2&gt;

&lt;p&gt;Teams don't trust automation because they've been burned before. Build trust through:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Visibility&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;terraform plan&lt;/code&gt; output is the most transparent change preview possible. Use it in every PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Safety&lt;/strong&gt;&lt;br&gt;
Start with read-only changes. Let the team run &lt;code&gt;terraform plan&lt;/code&gt; for weeks before anyone runs &lt;code&gt;apply&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Rollback capability&lt;/strong&gt;&lt;br&gt;
Show the team how to revert a change in minutes. Trust comes from knowing you can recover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Gradual rollout&lt;/strong&gt;&lt;br&gt;
Start with low-risk resources. Work up to critical infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I've Observed
&lt;/h2&gt;

&lt;p&gt;In my experience, the hardest part of IaC adoption isn't technical. It's getting experienced engineers to trust code over console.&lt;/p&gt;

&lt;p&gt;Engineers who have been burned by bad automation resist. Engineers who have fixed things manually for years don't see the problem.&lt;/p&gt;

&lt;p&gt;The solution isn't better tooling. It's small wins that build confidence over time.&lt;/p&gt;




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

&lt;p&gt;The technical part of Terraform is straightforward. The real challenge is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convincing leadership to invest in IaC&lt;/li&gt;
&lt;li&gt;Getting engineers to change their workflow&lt;/li&gt;
&lt;li&gt;Building trust in automation&lt;/li&gt;
&lt;li&gt;Moving incrementally when everyone wants to move fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Start small. Win early. Build momentum. The rest follows.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. The next time someone says "we should just do it manually this once," you'll know that's how drift starts. One manual change becomes ten. Ten becomes a hundred. The only way to win is to never start.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>30daychallenge</category>
      <category>iac</category>
    </item>
    <item>
      <title>Automating Terraform Testing: From Unit Tests to End-to-End Validation</title>
      <dc:creator>Mukami</dc:creator>
      <pubDate>Fri, 10 Apr 2026 06:53:52 +0000</pubDate>
      <link>https://forem.com/tink-origami/automating-terraform-testing-from-unit-tests-to-end-to-end-validation-3o24</link>
      <guid>https://forem.com/tink-origami/automating-terraform-testing-from-unit-tests-to-end-to-end-validation-3o24</guid>
      <description>&lt;h2&gt;
  
  
  How to Stop Wondering If Your Infrastructure Works and Start Knowing It Does
&lt;/h2&gt;




&lt;p&gt;&lt;strong&gt;Day 18 of the 30-Day Terraform Challenge&lt;/strong&gt; — and today I finally solved the problem that's been bothering me since Day 1.&lt;/p&gt;

&lt;p&gt;How do you know your infrastructure actually works?&lt;/p&gt;

&lt;p&gt;Manual testing gave me confidence, but it didn't scale. Every change meant re-running the same checks. Every environment meant more time. Every team member meant more coordination.&lt;/p&gt;

&lt;p&gt;Today I automated everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Layers of Testing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Type&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Deploys Real Infra&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Terratest&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;End-to-End&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Terratest&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;15-30 min&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each layer catches different failures. Together, they create confidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1: Unit Tests (Fast, Free, No AWS)
&lt;/h2&gt;

&lt;p&gt;Terraform 1.6+ includes a native testing framework. No external dependencies. No real infrastructure deployed. Just plan-time assertions.&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;# webserver_cluster_test.tftest.hcl&lt;/span&gt;

&lt;span class="nx"&gt;variables&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="s2"&gt;"test-cluster"&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;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;run&lt;/span&gt; &lt;span class="s2"&gt;"validate_asg_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;

  &lt;span class="nx"&gt;assert&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;"^test-cluster-asg-"&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_prefix&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;"ASG name prefix must start with cluster_name"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="s2"&gt;"validate_instance_type"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;

  &lt;span class="nx"&gt;assert&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;aws_launch_template&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;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;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Instance type must match variable"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="s2"&gt;"validate_tags"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;

  &lt;span class="nx"&gt;assert&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;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;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Environment"&lt;/span&gt;&lt;span class="p"&gt;]&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;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ALB must have Environment tag = dev"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with: &lt;code&gt;terraform test&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it catches:&lt;/strong&gt; Syntax errors, naming conventions, tag consistency, logic mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't catch:&lt;/strong&gt; DNS propagation, health check failures, actual HTTP responses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 2: Integration Tests (Real Infra, Real Assertions)
&lt;/h2&gt;

&lt;p&gt;Integration tests deploy real infrastructure, run assertions against it, then destroy it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// test/webserver_cluster_test.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestWebserverClusterIntegration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parallel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;uniqueID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UniqueId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;clusterName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test-cluster-%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uniqueID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;terraformOptions&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TerraformDir&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"../manual-test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Vars&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
      &lt;span class="s"&gt;"cluster_name"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;clusterName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;"instance_type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"t3.micro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;"min_size"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;"max_size"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;"environment"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;// CRITICAL: Always destroy, even if test fails&lt;/span&gt;
  &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;terraformOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

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

  &lt;span class="c"&gt;// Retry for 5 minutes (ALB takes time)&lt;/span&gt;
  &lt;span class="n"&gt;http_helper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpGetWithRetryWithCustomValidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;200&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;Run with: &lt;code&gt;go test -v -timeout 30m ./...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it catches:&lt;/strong&gt; ALB DNS resolution, health check passing, actual HTTP responses, deployment ordering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The critical piece:&lt;/strong&gt; &lt;code&gt;defer terraform.Destroy&lt;/code&gt; ensures cleanup even if tests fail. No orphaned resources. No surprise AWS bills.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 3: End-to-End Tests (Full Stack)
&lt;/h2&gt;

&lt;p&gt;E2E tests deploy everything — VPC, database, application — and verify the whole system works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestFullStackEndToEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parallel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;uniqueID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UniqueId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c"&gt;// Deploy VPC&lt;/span&gt;
  &lt;span class="n"&gt;vpcOptions&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TerraformDir&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"../modules/networking/vpc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Vars&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
      &lt;span class="s"&gt;"vpc_name"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test-vpc-%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uniqueID&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;defer&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vpcOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitAndApply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vpcOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;vpcID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vpcOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"vpc_id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;subnetIDs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vpcOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"private_subnet_ids"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;// Deploy app using VPC outputs&lt;/span&gt;
  &lt;span class="n"&gt;appOptions&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TerraformDir&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"../modules/services/webserver-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Vars&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
      &lt;span class="s"&gt;"cluster_name"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test-app-%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uniqueID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="s"&gt;"vpc_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;vpcID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;"subnet_ids"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;subnetIDs&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;defer&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitAndApply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;albDnsName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"alb_dns_name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;http_helper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpGetWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;albDnsName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it catches:&lt;/strong&gt; Cross-module integration issues, networking problems, full stack failures that unit and integration tests miss.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CI/CD Pipeline
&lt;/h2&gt;

&lt;p&gt;Run everything automatically on every commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;unit-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Unit Tests&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init &amp;amp;&amp;amp; terraform test&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual-test&lt;/span&gt;

  &lt;span class="na"&gt;integration-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Integration Tests&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'push'&lt;/span&gt;  &lt;span class="c1"&gt;# Only on merge to main&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unit-tests&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
      &lt;span class="na"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.21"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go test -v -timeout 30m ./...&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Job dependencies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit tests run on every PR (fast, cheap)&lt;/li&gt;
&lt;li&gt;Integration tests only run on merge to main (slower, costs money)&lt;/li&gt;
&lt;li&gt;E2E tests run on schedule (once a day)&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Before automation, every change meant:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;terraform apply&lt;/code&gt; manually&lt;/li&gt;
&lt;li&gt;Wait 5 minutes&lt;/li&gt;
&lt;li&gt;Test with &lt;code&gt;curl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remember to destroy&lt;/li&gt;
&lt;li&gt;Repeat for every environment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, every commit triggers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Unit tests (10 seconds)&lt;/li&gt;
&lt;li&gt;Integration tests (5 minutes)&lt;/li&gt;
&lt;li&gt;Confidence that it works&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure that is tested automatically is infrastructure you can trust.&lt;/strong&gt;&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Type&lt;/th&gt;
&lt;th&gt;What It Found&lt;/th&gt;
&lt;th&gt;Time&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;Unit&lt;/td&gt;
&lt;td&gt;Missing tags, wrong naming&lt;/td&gt;
&lt;td&gt;10s&lt;/td&gt;
&lt;td&gt;✅ Caught before PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;Health check failures, 502 errors&lt;/td&gt;
&lt;td&gt;5min&lt;/td&gt;
&lt;td&gt;✅ Caught before merge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2E&lt;/td&gt;
&lt;td&gt;Cross-module networking&lt;/td&gt;
&lt;td&gt;15min&lt;/td&gt;
&lt;td&gt;✅ Caught before release&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;Unit tests are your safety net.&lt;/strong&gt; Run them on every commit. They cost nothing and catch everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration tests are your confidence builder.&lt;/strong&gt; Run them before merging. They cost a little but find real issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E2E tests are your release gate.&lt;/strong&gt; Run them less frequently. They cost more but verify everything works together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;defer terraform.Destroy&lt;/code&gt; is critical.&lt;/strong&gt; Without it, failed tests leave resources running. With it, cleanup is guaranteed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets never go in code.&lt;/strong&gt; Use GitHub Secrets for AWS credentials.&lt;/p&gt;




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

&lt;p&gt;Manual testing gave me confidence for one deployment. Automated testing gives me confidence for every deployment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&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;Test once a day&lt;/td&gt;
&lt;td&gt;Test every commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual curl checks&lt;/td&gt;
&lt;td&gt;Automated HTTP assertions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hope cleanup works&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;defer&lt;/code&gt; guarantees cleanup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 minutes of manual work&lt;/td&gt;
&lt;td&gt;5 minutes of automated trust&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If you're not testing your infrastructure automatically, you're deploying with blind faith.&lt;/strong&gt;&lt;/p&gt;




</description>
      <category>aws</category>
      <category>terraform</category>
      <category>test</category>
      <category>30daychallenge</category>
    </item>
    <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://forem.com/tink-origami/the-importance-of-manual-testing-in-terraform-1812</link>
      <guid>https://forem.com/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://forem.com/tink-origami/creating-production-grade-infrastructure-with-terraform-3b1p</link>
      <guid>https://forem.com/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://forem.com/tink-origami/deploying-multi-cloud-infrastructure-with-terraform-modules-hln</link>
      <guid>https://forem.com/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://forem.com/tink-origami/getting-started-with-multiple-providers-in-terraform-5g54</link>
      <guid>https://forem.com/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>
  </channel>
</rss>
