Terraform plan
A Terraform plan is the set of changes Terraform proposes to make real
infrastructure match your configuration. terraform plan refreshes what
exists, compares it with the configuration and the state file, and gives each
resource an action. Nothing changes until terraform apply runs that plan,
which is why the plan is the thing a reviewer actually approves.
The actions
| Action | Human output | Plan JSON actions |
Means |
|---|---|---|---|
| Create | + |
["create"] |
A new object will be made |
| Update | ~ |
["update"] |
Changed in place; the object keeps its identity |
| Replace | -/+ |
["delete","create"] |
Destroyed, then a new one created |
| Replace (CBD) | +/- |
["create","delete"] |
New one created first (create_before_destroy), then old removed |
| Destroy | - |
["delete"] |
The object will be deleted |
| Read | <= |
["read"] |
A data source is read during apply, not during plan |
| Import | — | ["no-op"] + importing |
An existing object is brought under management (import block) |
| Move | — | ["no-op"] + previous_address |
Same object, new address (moved block) |
| Forget | — | ["forget"] |
Removed from state, real object left alone (removed block) |
Moves and imports can also carry an update. The JSON then says ["update"]
with a previous_address or importing alongside.
Why something gets replaced
Replacement is the action reviewers care most about. A replaced database or
load balancer is an outage, not a tweak. Terraform records why in
action_reason:
action_reason |
Cause |
|---|---|
replace_because_cannot_update |
A changed attribute is immutable for this resource type |
replace_because_tainted |
The object was marked tainted (often a failed create) |
replace_by_request |
terraform plan -replace=ADDRESS |
replace_by_triggers |
lifecycle { replace_triggered_by = [...] } fired |
delete_because_no_resource_config |
The resource block was deleted from the configuration |
delete_because_each_key |
The for_each key no longer exists |
delete_because_count_index |
The count shrank |
For cannot_update, the attribute responsible is listed in replace_paths.
The human output marks it # forces replacement. A changed AMI on an
instance or a renamed RDS identifier are the classic cases.
Known after apply and sensitive values
Some values don't exist until the change is made: an id the cloud assigns,
an IP, anything computed from a resource that isn't created yet. The plan
shows these as (known after apply). In JSON they are true at the same path
in after_unknown. The after value is null or missing there.
Sensitive values (a sensitive = true variable or output, or an attribute the
provider marks sensitive) show as (sensitive value). The JSON flags them in
before_sensitive / after_sensitive, but the real values are still in the
file. A plan file is a secret. Don't attach plan.json to a ticket. Share a
rendering that masks them.
The plan JSON
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
The top-level fields you'll use:
resource_changes: one entry per resource instance, withaddress,type,provider_name,change.actions,change.before,change.after, the unknown and sensitive masks,replace_paths, andaction_reason.output_changes: the same before/after shape for each root output.resource_drift: objects that changed outside Terraform since the last apply. These are refreshed into state, not reverted, unless the config also changes them.planned_values/prior_state: full snapshots, useful for policy tools like OPA or Sentinel.errored: true if planning failed partway, so the plan is incomplete.
terraform plan -json is a different thing: a stream of one JSON object per
line describing UI events (planned_change, change_summary, diagnostic).
It names each resource's action but carries no attribute values.
Reviewing a plan
The plan text works for whoever wrote the change. It works badly for everyone else who has to approve it. It's thousands of lines in a CI log. It gets cut off in a PR comment past GitHub's 65,536-character limit. And it mixes the one destroy that matters in with two hundred tag updates. A review-ready plan puts destroys and replaces first, says why each one happens, shows only the attributes that change, and masks secrets. It needs to sit somewhere a reviewer without cloud credentials can open it, comment on one resource, and sign off.
Try it
Comma is free: unlimited reports, unlimited commenters, unlimited revision history.
Open the Terraform plan viewer →