🛸 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
| Thing | Number / fact |
|---|---|
| Terraform artifacts | 3: config, state, plan |
| Plan actions | 5: create / update / replace / delete / no-op |
ForceNew change | always a replace (-/+) — destroys the real resource |
| Replace default order | destroy-then-create (downtime); create_before_destroy inverts it |
| Delete order | reverse topological (dependents first) |
| Azure state backend | Azure Storage blob; lock = blob lease |
| State secrets | stored in plaintext in state → encrypt + restrict access |
| CDKTF output | cdk.tf.json (Terraform JSON) → same plan/apply engine |
| Drift | real ≠ state; plan refreshes (provider GET) then diffs |
| Lost state | Terraform 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 (aForceNewattr — a rename, a region, a SKU) and someone skimmed past it and typedyes. 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), orignore_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
- Not knowing the three artifacts — talking about
applywithout distinguishing config, state, and plan. - Treating
~and-/+as the same ("it'll just change it") — a replace destroys the resource. - Saying "Terraform handles drift automatically" — it detects drift via refresh; you decide how to reconcile.
- Not having an answer to "where does state live and how is it locked."
- Forgetting deletes go in reverse dependency order.
depends_oneverywhere — serialises parallelisable resources and hides the real graph.- Thinking CDKTF is a different engine — it synthesizes to Terraform JSON and runs the same plan/apply.
- Forgetting state holds secrets in plaintext — so it needs encryption + access control like the secret store it effectively is.
force-unlockas 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
ForceNewreplace 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.