Skip to content
Infrastructure as Code iac workflow 5 min read

terraform plan

terraform plan is Terraform’s dry-run command. It refreshes the state, compares your configuration against the real infrastructure, and prints a diff of exactly what it would create, change, or destroy — without touching anything. Reviewing this diff before every apply is the single most important habit in safe infrastructure work: it is your last chance to catch a typo that would delete a production database. OpenTofu exposes the identical tofu plan command with the same flags and output format.

How plan works

When you run plan, Terraform performs three steps. First it reads the current state and refreshes it by querying the providers for the live attributes of every managed resource. Then it evaluates your configuration to build the desired state. Finally it computes the difference between the two and renders it as a proposed set of actions.

terraform plan

Output:

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
  - destroy
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami                          = "ami-0c7217cdde317cfec"
      + instance_type                = "t3.micro"
      + id                           = (known after apply)
      + private_ip                   = (known after apply)
      + tags                         = {
          + "Name" = "web-server"
        }
    }

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

The (known after apply) markers are values the provider cannot compute until the resource actually exists, such as a generated ID or assigned IP address.

Reading the diff

Every line in a plan begins with a symbol that tells you what Terraform intends to do. Memorizing these four symbols makes plans readable at a glance.

SymbolActionMeaning
+createA new resource (or attribute) will be added
~update in-placeAn existing resource will be modified without replacement
-destroyThe resource will be deleted
-/+replaceThe resource will be destroyed and recreated (forces new)

A replacement (-/+) is the most dangerous outcome, because it implies downtime or data loss. Terraform always annotates why with a # forces replacement comment next to the offending attribute.

  # aws_instance.web must be replaced
-/+ resource "aws_instance" "web" {
      ~ ami           = "ami-0c7217cdde317cfec" -> "ami-0e731c8a588258d0d" # forces replacement
      ~ id            = "i-0abc123" -> (known after apply)
        instance_type = "t3.micro"
        # (8 unchanged attributes hidden)
    }

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

Always read the summary line. Plan: X to add, Y to change, Z to destroy is the headline — if destroy is non-zero and you did not expect it, stop and investigate before applying.

Saving a plan with -out

By default a plan is advisory: a later apply recomputes its own plan, so what you reviewed and what gets applied could differ if the world changed in between. To guarantee that exactly the reviewed changes are applied, save the plan to a file and feed it back to apply.

terraform plan -out=tfplan
terraform apply tfplan

The saved plan is a binary artifact bound to the current state. When you apply it, Terraform performs no fresh refresh or re-evaluation — it executes the recorded actions verbatim. This is the recommended pattern for CI/CD pipelines, where the plan stage and the apply stage often run as separate jobs with a manual approval gate between them.

A saved plan file can contain sensitive values in plaintext. Treat tfplan as a secret: never commit it to version control and scope CI artifact access tightly.

Narrowing scope with -target and -var

The -var and -var-file flags supply input variable values for the run, exactly as they would for apply.

terraform plan -var="instance_type=t3.large" -var-file="prod.tfvars"

The -target flag restricts planning to a specific resource address and its dependencies. It is an emergency escape hatch — useful for recovering from a partial failure — not a routine workflow, because it produces a plan that ignores the rest of your configuration.

terraform plan -target=aws_instance.web -target=aws_security_group.web

When you target resources, Terraform prints an explicit warning that the resulting state may not match your full configuration. Prefer fixing the underlying dependency issue over reaching for -target.

Exit codes for automation

For scripting, the -detailed-exitcode flag changes plan’s return value into a three-way signal instead of the usual success/failure. This lets a pipeline decide whether an apply step needs to run at all.

Exit codeMeaning
0Success, no changes (infrastructure matches configuration)
1Error
2Success, changes are present
terraform plan -detailed-exitcode -out=tfplan
case $? in
  0) echo "No changes — skipping apply." ;;
  1) echo "Plan failed." ; exit 1 ;;
  2) echo "Changes detected — applying." ; terraform apply tfplan ;;
esac

A common use is drift detection on a schedule: run plan -detailed-exitcode, and if it returns 2, alert that the live infrastructure no longer matches the committed configuration.

Useful supporting flags

terraform plan -no-color            # strip ANSI codes for logs and PRs
terraform plan -json                # machine-readable streaming output
terraform plan -refresh=false       # skip the refresh step (faster, riskier)
terraform plan -compact-warnings    # condense warning summaries

The -json output is the basis for tooling that posts plan summaries as pull-request comments, since each event is a self-contained JSON object on its own line.

Best practices

  • Run plan before every apply, and read the full diff — never skim straight to the summary.
  • Treat any unexpected destroy or -/+ replace as a hard stop until you understand the cause.
  • In CI, use plan -out=tfplan with a manual approval gate, then apply tfplan to apply exactly what was reviewed.
  • Use -detailed-exitcode to drive conditional applies and scheduled drift detection.
  • Avoid -target in normal workflows; reserve it for recovering from partial failures.
  • Run terraform validate and terraform fmt before planning so the plan reflects clean, correct configuration.
  • Never commit saved plan files — they can embed secrets in plaintext.
Last updated June 14, 2026
Was this helpful?