Skip to main content
RunBook Academy

TerraformXVI · Plan Review and Saved PlansPlan review

terraform plan in Depth

Intermediate⏱ ~30 min🧪 Lab requiredbashterraform

What you'll learn

  • Read every symbol and action in a Terraform plan
  • Identify replacement vs in-place update vs destroy
  • Recognise known-after-apply values and their implications
  • Apply a production plan review workflow

Prerequisites

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12

Not yet marked complete on this device.

The plan is the operational artefact. Every other artifact in Terraform — the configuration, the state, the apply log — is in service of producing a plan you can read and trust. This lesson teaches the plan in depth.

What the plan output means

A typical plan output:

Terraform will perform the following actions:

  # aws_instance.web must be replaced
-/+ resource "aws_instance" "web" {
      ~ ami                          = "ami-0e1bed4f" -> "ami-9ad034sd" # forces replacement
      ~ instance_type                = "t3.medium" -> "t3.small" # forces replacement
        id                           = "i-0abc123def456789"
      ~ tags                         = {
          - "Environment" = "dev" -> null
          + "Environment" = "production"
        }
      - (14 unchanged attributes hidden)
    }

  # aws_security_group.alb will be created
  + resource "aws_security_group" "alb" {
      + arn                    = (known after apply)
      + description            = "ALB SG"
      + id                     = (known after apply)
      + name                   = "alb-sg"
      + vpc_id                 = "vpc-12345"
      + (6 other attributes omitted)
    }

  # aws_lb.public will be updated in-place
  ~ resource "aws_lb" "public" {
      ~ idle_timeout               = 60 -> 90
        id                         = "arn:aws:elasticloadbalancing:..."
        (15 unchanged attributes hidden)
    }

  # aws_s3_bucket.legacy will be destroyed
- resource "aws_s3_bucket" "legacy" {
      - bucket           = "legacy-bucket-2024-01" -> null
      - id               = "legacy-bucket-2024-01" -> null
      - (12 other attributes hidden)
    }

Plan: 1 to add, 1 to change, 1 to destroy, 1 to replace.

The plan is a proposal. It is the core saying “given the configuration, the state, and the refresh, here is what I believe I need to do”.

The action symbols

SymbolMeaningOperational implication
+ createResource does not exist yetCosts money; creates new attack surface
~ update in-placeResource exists, attributes changeUsually safe; verify the change
-/+ replaceResource will be destroyed and recreatedDestructive — data may be lost
- destroyResource exists but is no longer in configurationOften associated with data loss
<= readData source is being readNo real-world effect
~ taintResource is marked for replacement on next applyPre-1.6 mechanism; replaced by replace_triggered_by

The summary line at the bottom is the plan summary:

Plan: 1 to add, 1 to change, 1 to destroy, 1 to replace.

Add up the destroys and the replaces. The number tells you the theoretical destruction cost of the plan. If a plan says “21 to add, 0 to change, 0 to destroy,” the operational impact is clear. If a plan says “0 to add, 0 to change, 18 to destroy,” the operational impact is also clear, and probably not what you intended.

Replacement vs in-place update

Most resources have a notion of which attribute changes are in-place and which force replacement. The provider documentation is the authoritative source. The plan summary tells you what the provider decided.

For example, the AWS providers aws_instance resource:

  • Changes to instance_type force replacement (you cannot change the instance type of a running EC2 instance in place).
  • Changes to tags are in-place.
  • Changes to user_data are in-place.

The plan shows the replacement-triggering attributes with a comment:

~ instance_type = "t3.medium" -> "t3.small" # forces replacement

The comment is the providers signal. The plan output is the operational opportunity to see the replacement before it happens.

Known-after-apply values

Some attributes are not knowable until the resource is created. The plan shows them as (known after apply):

+ resource "aws_security_group" "alb" {
    + arn                    = (known after apply)
    + id                     = (known after apply)
  }

A (known after apply) value tells you:

  • The attribute will be assigned by the provider during the create operation.
  • The plan cannot show you the final value.
  • The attribute will be populated in state after the apply.

A common pitfall: a downstream resource that references the known-after-apply attribute will fail because the reference is undefined at plan time. The fix is either to use a different attribute that is known at plan time, or to define the downstream with a depends_on so the apply order is correct.

How to read the plan

A production plan review workflow:

  1. Read the summary line first. Plan: 1 to add, 0 to change, 0 to destroy, 0 to replace. Annotate expectation. If the summary doesn’t match your expectation, stop and investigate.
  2. For each replaced resource, find the # forces replacement comment. Identify the attribute that triggered the replacement. Ask: should this have been in-place?
  3. For each destroyed resource, find it in the configuration. Is the resource still supposed to exist? If not, why is it being destroyed?
  4. For each created resource, find it in the configuration. Is this expected? Where in the dependency graph does it appear?
  5. For each in-place update, scan the diff. Is the change expected?
  6. For each known-after-apply value, identify downstream references. Will the apply succeed?

The plan is the operational artefact. Reading it is not a formality; it is the production review.

Saved plans

A saved plan is a plan file written to disk and applied later:

terraform plan -out=production.tfplan
terraform show production.tfplan       # review the saved plan
terraform apply production.tfplan      # apply exactly what was reviewed

The -out flag:

  • Records the configuration at plan time. The apply runs against the saved configuration, not against whatever the configuration is now.
  • Records the state at plan time. The apply executes against the state as the plan saw it, not against the current state.
  • Records the plan decisions at plan time. The apply does not re-run plan against the current state.

A saved plan is the operational version of a code review. It says “this is what we agreed to apply; nothing else is acceptable”.

If the configuration or state changes between plan -out and apply <plan>, the apply will fail with a state-mismatch error. This is by design: the saved plan is the contract, and the apply must match the contract.

Targeting

The -target flag is a recovery tool, not a workflow.

terraform apply -target=aws_instance.web

Targeting limits the apply to a subset of resources. It is useful in three recovery scenarios:

  • A specific resource is in a bad state and needs to be re-applied.
  • The state and reality disagree on one resource and need to be reconciled.
  • A previous apply failed partway through, and the affected resources need to be re-applied.

It is not a workflow tool. Using -target to avoid planning carefully is a recipe for drift: the resources you did not target will accumulate diverging state, and the next non-targeted plan will propose changes against them.

The course returns to targeting in Part LXXIV.

The plan output is JSON

terraform show -json <plan> produces a structured representation of the plan. The JSON is useful for:

  • Programmatic review (CI pipelines that parse the plan).
  • Comparing plans for the same configuration across branches.
  • Generating a human-readable summary in CI logs.

A reduced example:

{
  "format_version": "1.2",
  "terraform_version": "1.9.8",
  "resource_changes": [
    {
      "address": "aws_instance.web",
      "type": "aws_instance",
      "name": "web",
      "change": {
        "actions": ["update", "delete", "create"],
        "before": { "instance_type": "t3.medium" },
        "after":  { "instance_type": "t3.small" }
      }
    }
  ]
}

The actions array is ["update", "delete", "create"] for a replace. Parsing the JSON is the most reliable way to extract “plan X wants to replace N resources” in a CI pipeline.

Plan review in CI

A CI pipeline that runs terraform plan against a merged pull request is the cheapest production control. The plan output is the artefact; the engineer reviews the artefact in the PR.

Recommended CI workflow:

Pull request opened

ci: terraform init

ci: terraform validate

ci: terraform plan -out=tfplan

ci: terraform show -json tfplan | jq ... > plan-summary.json

ci: comments PR with plan-summary.json

Engineer reviews the plan summary in the PR

Engineer approves or requests changes

The plan is the unit of review. State is the unit of trust. Apply is the unit of execution.

Common plan surprises

A few patterns that surprise the unprepared reader:

  • The plan says “0 to add, 0 to change, 0 to destroy” but the provider is opening an API call. Read the section headers; refresh operations are not counted in the summary.
  • The provider name changed. A required_providers change causes every resource to be replaced, even if the schema is identical.
  • The state was imported. A terraform import from outside the tracked configuration can cause the plan to propose unwanted changes.
  • The variable changed. A tfvars change between two plans can cause the configuration to have different values without the configuration file changing.

The plan is the unification artefact. If the plan surprises you, the cause is in one of the three sources of truth.

What comes next

The next lesson is terraform apply: how the plan becomes reality, what happens during the apply, and what to do when the apply fails partway through.

Knowledge check · 7 questions

  1. Q1. What is a saved plan?

  2. Q2. What is the role of plan review?

  3. Q3. The plan is the only honest record of what Terraform is about to do.

  4. Q4. What is the role of the summary line?

  5. Q5. Which of the following are good plan-review practices? (Select all that apply.)

  6. Q6. What is the role of saved-plan apply?

  7. Q7. A plan shows 18 to destroy and 0 to add. The team expected 0 changes. What is the right action?

Passing score: 75%. Answers are checked in this browser.