🛸 Hitchhiker's Guide — Phase 02: Terraform, CDKTF & IaC State Internals

Read this if: you can write a Terraform module and run apply, but a -/+ in the plan doesn't make your stomach drop yet, and "where does your state live and how is it locked" isn't a question you can answer in one sentence. This is the compressed tour the WARMUP derives slowly. Skim it, then read the WARMUP, then build the lab.


0. The 30-second mental model

Terraform is the diff between the world you wrote and the world it remembers. Three artifacts: config (desired), state (recorded reality, terraform.tfstate), plan (the diff). apply executes the plan and writes new state. ARM (P01) is stateless — it re-derives reality every deploy. Terraform is stateful — it trusts a file. Everything good (a portable, reviewable, multi-cloud plan) and everything scary (state loss, drift, the -/+ that deletes your database) follows from that one decision.

One sentence to tattoo: read the plan for -/+ — that's a real resource about to be destroyed.

1. The plan symbols, one breath

 +    create        in config, not in state
 ~    update         differs, only MUTABLE attrs changed     (in place, safe-ish)
-/+   replace        differs, a ForceNew (IMMUTABLE) attr     (DESTROY then create ← danger)
 -    delete         in state, not in config
(none) no-op         identical                                (= converged = good)

Plan: N to add, M to change, K to destroy is just these counted. The whole job of reading a plan is hunting the -/+ and the - you didn't intend.

2. The numbers and facts to tattoo on your arm

ThingNumber / fact
Terraform artifacts3: config, state, plan
Plan actions5: create / update / replace / delete / no-op
ForceNew changealways a replace (-/+) — destroys the real resource
Replace default orderdestroy-then-create (downtime); create_before_destroy inverts it
Delete orderreverse topological (dependents first)
Azure state backendAzure Storage blob; lock = blob lease
State secretsstored in plaintext in state → encrypt + restrict access
CDKTF outputcdk.tf.json (Terraform JSON) → same plan/apply engine
Driftreal ≠ state; plan refreshes (provider GET) then diffs
Lost stateTerraform forgets it owns your infra → re-creates / collides

3. The dials you'll turn forever

update ⟷ replace            (mutable attr = ~ ; immutable = -/+ destroys real infra)
destroy-then-create ⟷ create-before-destroy   (downtime vs name collisions)
implicit deps ⟷ depends_on  (prefer references; depends_on serialises & hides the graph)
config wins ⟷ reality wins ⟷ ignore_changes   (the three drift reconciliations)
local state ⟷ remote+locked  (laptop = solo & fragile; blob+lease = team-safe)
HCL ⟷ CDKTF                 (static & simple vs generated config in a real language)
Terraform ⟷ Bicep           (multi-cloud + reviewable plan vs no state file to lose)

The interview signal: when they say "I want to rename this storage account," can you say "that's a ForceNew — it'll be a -/+, which destroys it; is there data behind it?"

4. The terraform / az one-liners you'll actually type

# bootstrap remote state (the first thing a real team does)
az group create -n rg-tfstate -l eastus
az storage account create -n sttfstate -g rg-tfstate --sku Standard_LRS --encryption-services blob
az storage container create -n tfstate --account-name sttfstate

# the core loop
terraform init                              # download providers, configure the backend
terraform plan -out=tfplan                  # compute the diff; SAVE it so apply runs exactly it
terraform apply tfplan                       # execute the saved plan (takes the blob-lease lock)

# read the plan for danger
terraform plan | grep -E '(-/+|# .* will be (destroyed|replaced))'   # find the replaces/deletes

# drift
terraform plan -refresh-only                # show drift WITHOUT proposing config changes
terraform apply -refresh-only               # accept reality into state (no infra change)

# state surgery (know these exist; use with care)
terraform state list                         # what state thinks it owns
terraform state show azurerm_storage_account.main
terraform import azurerm_resource_group.main /subscriptions/…/resourceGroups/rg-app  # adopt an existing resource
terraform state mv  <old> <new>             # rename without destroy/create
terraform state rm  <addr>                  # forget a resource (does NOT delete it)
terraform force-unlock <LOCK_ID>            # ⚠️ only for an ORPHANED lock, never a live apply

# CDKTF
cdktf synth                                  # run your program → cdk.tf.json
cdktf plan ; cdktf deploy                    # same engine, programmatic front-end

5. War story shapes you'll relive

  • "The apply deleted our database." → the plan had a -/+ on it (a ForceNew attr — a rename, a region, a SKU) and someone skimmed past it and typed yes. Fix is reading the plan: every -/+ is a destroy. Often the cause is "tightened the name to satisfy a new naming policy" — and the name is immutable.
  • "Two of us applied at once and state is garbage." → no locking, or someone force-unlocked a live run. Fix is remote state + blob-lease locking, and never force-unlocking a running apply.
  • "The plan wants to change things I never touched." → drift. Someone edited the resource in the portal; refresh caught it. Decide: revert (re-apply), keep (update config / import), or ignore_changes.
  • "We lost the state file." → state on a laptop, no remote backend, no versioning. Now Terraform thinks it owns nothing and wants to create resources that already exist. Hours of terraform import. This is why remote + versioned state is non-negotiable.
  • "create_before_destroy still caused an outage." → the resource had a hard-coded unique name, so the new one collided with the old. Use name_prefix / let it be generated.

6. Vocabulary that signals you've held the pager

  • State — Terraform's recorded reality; an address→attributes map it trusts.
  • ForceNew / replace (-/+) — an immutable attribute changed → destroy-then-create.
  • create_before_destroy — the lifecycle flag that makes a replace zero-downtime.
  • Drift — real ≠ state; the out-of-band change plan's refresh surfaces.
  • Backend — where state lives (AzureRM = a storage blob) and how it's locked.
  • Blob lease — the Azure lock that prevents concurrent state corruption.
  • import / state mv / state rm — adopt / rename / forget without touching infra.
  • Refresh — the read-real-resources phase before the diff.
  • Synthesize (cdktf synth) — turn a CDKTF program into Terraform JSON.
  • prevent_destroy — the lifecycle guardrail that fails the plan on a delete/replace of a protected resource — the seatbelt for stateful resources.

7. Beginner mistakes that mark you in interviews

  1. Not knowing the three artifacts — talking about apply without distinguishing config, state, and plan.
  2. Treating ~ and -/+ as the same ("it'll just change it") — a replace destroys the resource.
  3. Saying "Terraform handles drift automatically" — it detects drift via refresh; you decide how to reconcile.
  4. Not having an answer to "where does state live and how is it locked."
  5. Forgetting deletes go in reverse dependency order.
  6. depends_on everywhere — serialises parallelisable resources and hides the real graph.
  7. Thinking CDKTF is a different engine — it synthesizes to Terraform JSON and runs the same plan/apply.
  8. Forgetting state holds secrets in plaintext — so it needs encryption + access control like the secret store it effectively is.
  9. force-unlock as a routine fix — it re-creates the corruption locking prevents.

8. How this phase pays off later

  • The plan/apply engine (the lab) is the engine under every IaC tool — CDKTF, and the same shape as ARM/Bicep (P01) and CloudFormation/Pulumi.
  • The ForceNew replace instinct protects every stateful resource you'll ever manage — databases (P00 data layer), public IPs and subnets (P06), Key Vaults (P12).
  • Remote state + locking is the same shared-mutable-state-with-a-lock pattern you'll see in Service Bus sessions (P10) and distributed coordination everywhere.
  • Drift reconciliation is the daily reality of governed estates (P04 RBAC, P05 landing zones) where humans and policy can also mutate resources.
  • The stateful vs stateless tradeoff is a Round-5 architecture ADR (P15).

Now read the WARMUP slowly, build the state engine, and never type yes on a plan you haven't read for -/+ again.