Modern Terraform's signature move: state surgery became code. The imperative commands you learned as escape hatches each grew a declarative, reviewable twin:
moved { from = aws_s3_bucket.logs, to = module.storage.aws_s3_bucket.logs } # 1.1 — rename (lesson 4) import { to = aws_s3_bucket.legacy, id = "acme-legacy-data" } # 1.5 — adopt existing # + terraform plan -generate-config-out=generated.tf drafts the resource block from reality removed { from = aws_db_instance.old lifecycle { destroy = false } } # 1.7 — forget (lesson 2)
Same surgeries as state mv / import / state rm — but they
ship in a PR, show up in a plan before touching state, and replay identically for every
teammate and pipeline. The imperative commands remain for emergencies.
Pre-commit: fmt → validate → terraform test (unit layer).
PR: CI runs plan -out=tfplan; humans review the plan, not just the diff.
Merge: CI runs apply tfplan — executing exactly the reviewed plan. If the
world changed in between, the apply errors instead of improvising. Plan-as-artifact is the
three-pictures model from lesson 0001 turned into a governance mechanism.
On a schedule: plan -detailed-exitcode — exit 0 clean, 2 means "the pictures
disagree" → alert; triage with plan -refresh-only to split drift from
pending work (lesson 2).
Two ecosystem facts complete the picture. Pin everything — required_version,
provider ~> constraints, module versions — and commit
.terraform.lock.hcl (exact provider versions; the one state-adjacent file that
belongs in git). And know OpenTofu: when Terraform's license went BUSL in 2023, the
community forked the last open version into OpenTofu (Linux Foundation) — drop-in compatible
today, diverging slowly. Feature floors worth memorizing: moved 1.1 ·
import block 1.5 · test 1.6 · removed/mocks 1.7 ·
use_lockfile 1.10.
tests/ suite to the module (lesson 7).import block + -generate-config-out.moved block; verify the plan shows a move,
zero changes.removed block (destroy = false); verify it
survives while leaving state.fmt -check && validate && test && plan -detailed-exitcode;
echo $? and read the exit code like CI would.