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, with address, type, provider_name, change.actions, change.before, change.after, the unknown and sensitive masks, replace_paths, and action_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 →

Related