The Plan/Apply Lifecycle
Every change Terraform makes goes through the same three phases: refresh reads the current real-world state, plan diffs your configuration against that state, and apply executes the diff. The single fact that makes this safe is that plan is read-only and apply is the only phase that touches anything. That separation is what lets you review an infrastructure change the way you review a pull request — look at the diff first, then approve it.
Most production incidents that trace back to Terraform are not bugs in the tool. They are someone applying a plan they did not read, where a buried -/+ line quietly destroyed and recreated a database. Reading the plan is the skill; the rest is mechanics.
Refresh
Before computing anything, Terraform queries the provider for the current state of every resource it tracks, so the plan reflects reality rather than a stale snapshot in the state file. If someone resized an instance in the console, refresh is where Terraform notices. You can run this phase alone with terraform plan -refresh-only, which shows what drifted in the real world without proposing any configuration-driven changes — the clean way to reconcile state with reality when you suspect drift.
Plan
Plan compares the refreshed state to your configuration and computes one of four actions for every resource: create, update in place, destroy, or destroy-and-recreate. It prints each with a symbol — + create, ~ update in place, - destroy, and -/+ replace. Nothing changes during a plan; it is a proposal. The example below changes a tag (an in-place update) but also changes the AMI, which AWS cannot alter on a running instance, forcing a replacement.
# aws_instance.web must be replaced -/+ resource "aws_instance" "web" { ~ ami = "ami-0aaaaa" -> "ami-0bbbbb" # forces replacement ~ instance_type = "t3.micro" -> "t3.small" ~ tags = { ~ "Name" = "web" -> "web-1" } } Plan: 1 to add, 0 to change, 1 to destroy.
The # forces replacement annotation on the ami line is the most important text in that output. It tells you exactly which argument triggered the destroy-and-recreate. The summary line — 1 to add, 0 to change, 1 to destroy — is the count, but the annotation is the explanation, and on a stateful resource it is the difference between a no-downtime tweak and an outage.
Apply
Apply walks the dependency graph and executes the planned actions in order, calling the AWS APIs and updating state after each resource succeeds. Independent resources run in parallel; dependent ones wait. If apply is interrupted, the state file still records everything that completed, so the next run picks up from a consistent point rather than redoing finished work. Run without a saved plan, apply re-plans first and asks for confirmation, showing you the same diff one more time before it commits.
Saved Plans
In any pipeline, you freeze the plan and apply that exact file. terraform plan -out=tfplan writes the computed diff to disk; terraform apply tfplan executes it without re-planning. This is what makes a reviewer's approval meaningful: apply does exactly what was reviewed, even if config or reality changed in the meantime.
# in CI: produce a reviewable artifact terraform plan -out=tfplan # after approval: apply the frozen diff, no re-plan terraform apply tfplan
The flip side is the part that surprises people: a saved plan applies the diff it was built from, not a fresh diff against current reality. If the world changed between plan and apply, apply still executes the old plan. That is the intended behavior — you reviewed that diff — but it means you keep the window between plan and apply short so the frozen diff has not gone stale.
Reading Plan Output
The one distinction worth internalizing is ~ versus -/+. A ~ changes a mutable attribute in place with no disruption — an instance tag, an autoscaling group's desired count. A -/+ destroys the resource and builds a new one because the changed argument is immutable on the cloud side, and for a database or an EBS volume that means data loss or downtime unless you have planned for it. Scan every plan for -/+ on stateful resources before you approve, and read the # forces replacement annotation to know which argument is responsible.
In-place update (~) — changes a mutable attribute with no destroy step and no downtime, like a tag or a description. Safe to apply without ceremony; the resource keeps its ID and its data.
Replacement (-/+) — destroys and recreates because the changed argument is immutable on the cloud side, such as an EC2 AMI or a subnet's CIDR. The new resource gets a new ID, and any data or in-flight traffic on the old one is gone. Always scan for this before applying a stateful resource.
- Reading only the final summary line and missing a
-/+replacement of a stateful resource buried higher in the plan — the count says "1 to destroy" and you scrolled past the one that mattered. - Assuming
applyon a saved plan re-plans against current reality — it executes the frozen plan even if the world has since changed, which is the point but catches people off guard. - Ignoring the
# forces replacementannotation, the one piece of text that names exactly which argument triggers the destroy-and-recreate. - Letting a long gap pass between plan and apply in CI, so the applied diff no longer matches the reality a reviewer approved against.
- Treating
-refresh-onlyas a no-op — it can write detected drift back into state, which is what you want, but only if you meant to reconcile.
- In automation, always
plan -out=tfplanandapply tfplanso apply executes exactly the diff that was reviewed. - Scan every plan for
-/+replacements on stateful resources — databases, volumes, anything holding data — before approving. - Use
terraform plan -refresh-onlyto reconcile state with reality when you suspect drift, without making configuration-driven changes. - Keep the window between plan and apply short in pipelines so a saved plan never applies against meaningfully changed reality.
- Read the
# forces replacementannotation, not just the summary count, to know why a resource is being recreated.
Knowledge Check
A plan shows -/+ next to your RDS instance. What is actually about to happen?
- Terraform will destroy the database and create a new one, because an immutable argument changed
- Terraform will update the existing database in place with zero downtime and no data loss
- Terraform will refresh the database's recorded state but make no real change to the instance
- Terraform will spin up a second database and leave the first one entirely untouched and running
You run plan -out=tfplan, wait an hour while someone changes a resource in the console, then run apply tfplan. What does apply do?
- Executes the frozen diff from the saved plan, not a fresh diff against the now-changed reality
- Re-plans against the current real-world reality and then applies the freshly recomputed diff
- Refuses to run and errors out because the saved plan is now older than the latest recorded state
- Discards the saved plan and prompts you to plan again
Why pair plan -out with apply <file> in a pipeline instead of just running apply?
- So the applied changes are exactly the ones a reviewer approved, with no chance of a recomputed diff
- It is the single only available way to run an apply without ever showing an interactive confirmation prompt
- It encrypts the entire state file at rest during the apply step for added security
- It runs the apply in parallel across far more resources than a normal apply would
What does the refresh phase do?
- Reads the current real-world state of tracked resources so the plan reflects reality, not a stale snapshot
- Downloads and installs the latest available provider plugin versions from the registry before the plan begins
- Applies any pending changes that were queued up in state since the previous run
- Deletes the live resources that are no longer present in the configuration files
You got correct