Reading plans and refactoring safely
Small code changes can produce destructive plans. Learn why addresses, `count`, and renames cause replacements, and how to refactor without them.
The plan is the change
A code diff can look harmless while its plan destroys production. Reviewing infrastructure changes means reading the plan, not just the code. Read it in this order:
- The summary line:
Plan: 3 to add, 1 to change, 2 to destroy.Any destroy deserves an explanation. - Every
-/+(replace) and-(destroy) on a stateful resource, such as databases, volumes, buckets, and DNS zones. - The reason for each replacement. The plan marks the attribute with
# forces replacement, or shows that the address itself changed.
The rule: no destroy or replace of stateful resources is approved unless the PR description says why and how data is preserved.
Identity is the address
The tool identifies each resource by its address in the code: aws_db_instance.ledger, module.ledger.aws_db_instance.main, aws_instance.scanner[2], or aws_instance.scanner["depot-3"]. State maps addresses to real objects. Consequences:
- Renaming a resource changes its address.
- Moving it into a module changes its address.
- Reordering a
count-based list changes the addresses of everything after the change.
To the tool, a new address with no state entry is a new object to create, and an old address in state that no longer appears in the code is an object to destroy. Without help, every "tidy" refactor is a destroy and re-create.
count versus for_each
Terraform: Up & Running's loops chapter shows the trap. count creates resource[0], resource[1], and so on, indexed by position. Remove depot-2 from the middle of a list and depot-3 slides from index 2 to index 1. The plan then updates or replaces the depots after it to match their new positions, and destroys the last index. Three depots restart because one was decommissioned.
for_each over a set or map addresses by key: scanner["depot-3"] stays scanner["depot-3"] whatever else changes. Use count only for identical, interchangeable copies (or as an on/off switch with 0 or 1), and for_each for anything with a name. Converting an existing count resource to for_each itself changes addresses, which brings us to moved.
Refactoring with moved blocks
A moved block tells the tool that the object at one address now lives at another:
moved {
from = aws_db_instance.ledger
to = module.ledger.aws_db_instance.main
}
moved {
from = aws_instance.scanner[2]
to = aws_instance.scanner["depot-3"]
}
The next plan shows the object as moved and updates state, with no destroy and no create. Re-plan after adding them. A correct refactor plans as zero changes apart from the intended ones. Older workflows did the same with state mv commands run by hand. moved blocks are better because they are reviewed in the PR and applied consistently by every environment's pipeline.
Guardrails
lifecycle { prevent_destroy = true }on resources whose loss is unacceptable. Any plan that would destroy them fails.- Provider-side deletion protection (most databases and buckets offer it), which also stops deletion from consoles and scripts.
create_before_destroyfor resources that can briefly coexist, to avoid downtime on replacement.- Apply the reviewed plan:
plan -out=tfplan, review, thenapply tfplan. If state changed in between, the apply refuses instead of doing something nobody reviewed.
Key terms
- Resource address
- The identity of a resource in code, such as
module.ledger.aws_db_instance.mainoraws_instance.scanner["depot-3"]. - Replacement
- Destroy and re-create (
-/+), either because an attribute cannot change in place or because the address changed. - moved block
- A declaration telling the tool that an object at an old address now lives at a new one, so it updates state instead of replacing.
- prevent_destroy
- A lifecycle setting that makes any plan that would destroy the resource fail.
Read further
- Terraform Up & Running, 3rd edition, Ch. 4, "How to Create Reusable Infrastructure with Terraform Modules" (Purchase)
Module inputs, outputs, and versioning. Note how moving a resource into a module changes its address. - Terraform Up & Running, 3rd edition, Ch. 5, "Terraform Tips and Tricks — Loops, If-Statements, Deployment, and Gotchas" (Purchase)
countversusfor_each(especially the limitation when removing an item from the middle of a list), zero-downtime deployment, and the gotchas section on refactoring and on valid plans that still fail.