Skip to content

Terraform

Core · prereq 03 (Containers/Podman) · ~1.5–2 days (4 submodules: state, modules, providers/lifecycle, CI)

A shell script that provisions a server works today and fails next month — it assumed a world (no resource yet, no name clash) that's no longer true once you've already run it. Terraform is declarative instead: you write the infrastructure you want, and terraform plan computes exactly what would change against a state file that remembers what's actually out there — every time, before anything touches the cloud. Nothing applies until you've read that diff.

TL;DR — in 30 seconds:

  • Terraform is declarative: plan always computes a diff against state — a record of what's actually out there — before anything touches the cloud; nothing applies until you've read that diff.
  • State is the third file that closes the loop between what your config says you want and what the cloud actually has; without it, plan can't tell "this already exists" from "this is brand new."
  • A module is just a directory of .tf files with its own inputs (variable) and outputs — the same shape (e.g. a VPC) can be reused across environments instead of copy-pasted and left to drift.

Learning objectives

By the end, the junior can:

  • Write HCL with resources, variables, outputs, and run plan → apply.
  • Explain what state is and why it matters.
  • Factor a small module and read a plan diff.

Do it with Claude

Pair, Do and Prove run in Claude Code, with the onboarding-tutor skill. Click a button to copy the exact prompt, then paste it into Claude Code in this repo.

1. Learn · acquire the concept

Read: the terraform coursefocus §4 (HCL essentials), §5 (first resource), §6 (variables/outputs/locals), §9 (modules), §11 (state). Skip for now: §10 (providers in depth) and the S3 backend specifics — you'll use those on real infra later. You've met IaC before: module 05 (AWS) introduced it as CloudFormation/CDK — this is the cloud-agnostic, state-centric take. Glossary (beat 5): the module’s Anki deck Read next: Terraform documentation (official docs — full configuration-language and CLI reference).

Check yourself — you run terraform plan on a config you haven't touched in months. What is it actually comparing to decide there's nothing to do?

Config against state (refreshed against the real cloud), not against your last apply's output — plan has no memory of "last time" beyond what's recorded in state.

Check yourself — why does a shell script that provisions a server 'work today, fail next month,' while a second terraform apply on an unchanged config reports no changes?

The shell script has no record of what it already created, so it re-runs its create logic blind to the current world. Terraform's plan diffs config against state first, so a second apply against unchanged infrastructure computes zero changes instead of trying to recreate anything.

2. Pair · passive → active

Drive the onboarding-tutor skill:

  • "Explain declarative vs imperative with a Terraform example."
  • "3-line mental model of what terraform.tfstate is and why it's dangerous to lose."
  • "Quiz me on plan vs apply before I run anything."

Common gotchas

  • Running apply without reading the plan output first. Fix: always read what +/~/-/-/+ mean for each line — a -/+ under a stateful resource is the single most common "oh no" in review; catch it before approving, not after.
  • Treating terraform.tfstate as disposable. Fix: it's the only record mapping your config to the real-world resources — losing it (or letting two applies race without a lock) means Terraform re-guesses "already exists" vs "brand new," usually by trying to recreate everything.
  • Copy-pasting the same HCL into a second environment instead of factoring a module. Fix: the moment one copy gets a fix the other doesn't, they drift — a module's variable inputs are the difference between one reusable shape and two configs slowly diverging.
  • Assuming count and for_each are interchangeable ways to make N copies. Fix: for_each keys each instance by identity so removing one doesn't shift or recreate the rest; count shifts every index above a removed element.
Check yourself — a plan shows -/+ on a resource you only meant to rename in code, not change. What went wrong?

The rename likely changed the resource's config address (its resource label or module path), not just a cloud attribute — Terraform doesn't recognize the new address as "the same resource," so it defaults to destroy-and-recreate. A moved block would tell it this is the same resource at a new address instead.

Check yourself — you factor a working root config into a module. What can the module see from the caller, and what can the caller see from the module, after the split?

Only what crosses the interface: the module sees whatever the caller passes as variable inputs, and the caller sees only what the module exposes as outputs. A local or a bare resource attribute on either side never leaks across — encapsulation, not convention.

Done when: you can explain plan/apply and state's role, unprompted.

3. Do · produce an artifact

Exercise — a safe, cloud-free first module:

Use a local/safe provider so a junior can't break anything real — e.g. local_file, random, or a null_resource. (No cloud credentials.)

  1. Install Terraform first (course §3): brew install terraform.
  2. Write a config with a variable, a resource, and an output.
  3. terraform initplanapply. Read the plan carefully before applying.
  4. Inspect terraform.tfstate and find your resource in it.
  5. Change the variable, plan again, and read the diff (what will change and why).
  6. Factor the resource into a small module and call it from root.

Done when: apply succeeds, you can point to your resource in state, and you've read a change-plan diff.

4. Prove · understanding gate

Mandatory. Pass bar in the Socratic gate.

  1. Walkthrough: Explain your config and the plan output line by line — what does +, ~, - mean?
  2. Alternative: count vs for_each for multiple instances — when each?
  3. Failure & debug: Someone deletes the resource by hand (drift). What does the next plan show, and how does Terraform reconcile it? What happens if you lose state?
  4. Provenance: Which HCL did the agent write? How did you verify the plan does what you intended (not just that it applied)?

Pass bar: you can read a plan, explain state, and reason about drift. "It applied" without reading the plan = back to Pair.

5. Retain · fight forgetting

Study the module deck SE Onboarding::07 Terraform — its 14 theme subdecks let you drill one area at a time. It syncs to your Anki automatically on pull (anki-sync).

Done when: you can recall — from the deck — resource, provider, state, plan/apply, variable/output, module, and drift.

Key takeaways

  • Terraform is declarative: plan always diffs config against state before touching the cloud — nothing applies until you've read that diff.
  • State is what lets plan tell "already exists" from "brand new." Lose it, or race two applies without a lock, and Terraform re-guesses — usually by trying to recreate everything.
  • A module is a directory of .tf files with its own variable inputs and outputs. Encapsulation is real: the module only sees what the caller passes in, the caller only sees what the module exposes.
  • A rename without a moved block reads as a new resource address, not "the same resource renamed." Terraform defaults to destroy-and-recreate (-/+) instead of an in-place rename.
  • for_each keys each instance by identity, so removing one leaves the rest untouched; count shifts every index above a removed element — which can recreate resources you never meant to touch.
  • Reading the plan symbols (+ ~ - -/+) before approving is the whole safety model. "It applied" without reading the diff means back to Pair, not Prove.

Go deeper — curated resources

Hand-picked external material to take this topic further — the best books, courses, talks, and writing. Cost is tagged Free, Free online (full text/course free on the web), or Paid.


Gate log (mentor fills on pass)

  • Date passed: / Passed by: / checked by: / Weak spots: