Infrastructure as code describes resources and their relationships in versioned files. A planning step compares the declared target with provider state and proposes changes. That turns infrastructure into something reviewable — a diff, a pull request, an approval — and makes rollback a commit away. The plan is only as trustworthy as its inputs and state handling, which is where the real failure modes live.
State is the identity of what you built
The tool needs to know that this security group in the plan is that security group in the cloud, not a new one. That mapping lives in a state file, keyed by resource addresses:
# declared target — versioned, reviewed, applied
resource "aws_db_instance" "orders" {
identifier = "orders"
instance_class = "db.t4g.medium" # 2 vCPU, 4 GiB — was db.t4g.small
multi_az = true
deletion_protection = true
}
resource "aws_db_replica" "orders_read" {
count = 1 # added in this change
source_identifier = aws_db_instance.orders.identifier
}plan is the diff between this file and the provider's real resources. A change that adds a
read replica can be reviewed for cost (~$0.06/hr on a t4g medium → ~$44/month), networking
(security groups, subnets), and failover consequences before anything is created. State
must therefore be remote and locked: shared in durable storage so it survives laptops, and
locked so two concurrent applies don't both decide to create a replica and fight over the
address.
Drift: the thing that makes plans lie
Drift is real infrastructure that changed outside the declared workflow — a hotfixed security group, a manually scaled instance, an auto-created snapshot. The plan refreshes its view of reality first, so drift shows up as a diff — but only if someone runs a plan.
day 1 : terraform apply → 0 added, 3 changed, 0 destroyed ✓ clean
day 40 : on-call adds port 2222 to the SG by hand (break-glass)
day 41 : next plan → 1 to change (egress rule reverted) ← detected
if nobody plans for 6 months, the change is invisible
and the next apply silently reverts the break-glass ruleRun a scheduled plan in CI (daily, or on a timer) purely for drift detection, and treat unexplained diffs as findings. The nasty case is not drift that gets reverted — it is automation that depends on drifted state (an IP allowlist someone added manually) being corrected without anyone connecting the outage to the apply.
Reviewing a plan that wants to destroy something
The check question is the everyday case: a plan proposes replacing a production database because of a naming change. Most providers force replacement when an immutable attribute changes — name, AZ, engine version — and the plan's destroy/create pair for a database means data loss unless a snapshot-and-restore path exists.
Before approving, reviewers should look at:
terraform plan -json | jq '.resource_changes[] | select(.change.actions | index("delete"))'
does any action = ["delete","create"] ? ← replacement, not update
is deletion_protection on the resource? ← would block it at apply time
is there a from_snapshot / snapshot on destroy?
does the change touch a state address that ← renaming an address looks
other resources reference? like delete+create tooTwo tools preserve identity when it's only the reference that changed:
# terraform ≥ 1.1 — move the address without replacing the resource
moved {
from = aws_db_instance.main
to = aws_db_instance.orders
}terraform state mv does the same interactively (destructive to state if mistyped — take a
backup first: terraform state pull > state.backup.json). The rule generalizes: an
address change is a data-destroying change until proven otherwise.
Costs and failure cases
- Secrets in state. State files record resource attributes verbatim — database passwords, generated keys, connection strings. Encrypt state at rest, restrict who can read it, and keep real secrets in a secrets manager referenced by data source rather than written into the configuration. Plain-text secrets in version control are a one-commit compromise.
- Broad plans. A plan showing
24 to change, 0 to destroyis not automatically safer than one showing a replacement — a bulk IAM or network change can have a wider blast radius than a single replacement. Review by resource type, not by count. - Plan ≠ apply. Drift between plan and apply (or a concurrent change) is why apply runs from the same pipeline that produced the plan, with no manual edits in between.
- IaC doesn't fix bad design — it makes a bad architecture reproducible across every environment you have.
Key takeaways
- State is identity. Remote, locked, versioned state is what lets a plan say "change" instead of "create another one" — and what makes concurrent teams safe.
- Review the actions, not the summary. Any
delete+createpair on stateful resources is a data-loss candidate; usemovedblocks orstate mvto preserve identity. - Drift detection needs a schedule, or your declared state quietly stops describing reality — and the next apply reverts somebody's break-glass change.
- State files are secret-bearing artifacts. Encrypt, restrict, and keep real credentials out of both state and configuration.
Terraform Documentation: Infrastructure as Code covers state, plans, and moved blocks.