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.
| Symbol | Action | Meaning |
|---|---|---|
+ | create | A new resource (or attribute) will be added |
~ | update in-place | An existing resource will be modified without replacement |
- | destroy | The resource will be deleted |
-/+ | replace | The 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 destroyis the headline — ifdestroyis 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
tfplanas 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 code | Meaning |
|---|---|
0 | Success, no changes (infrastructure matches configuration) |
1 | Error |
2 | Success, 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
planbefore everyapply, and read the full diff — never skim straight to the summary. - Treat any unexpected
destroyor-/+ replaceas a hard stop until you understand the cause. - In CI, use
plan -out=tfplanwith a manual approval gate, thenapply tfplanto apply exactly what was reviewed. - Use
-detailed-exitcodeto drive conditional applies and scheduled drift detection. - Avoid
-targetin normal workflows; reserve it for recovering from partial failures. - Run
terraform validateandterraform fmtbefore planning so the plan reflects clean, correct configuration. - Never commit saved plan files — they can embed secrets in plaintext.