terraform plan
terraform plan computes the difference between your configuration and the current infrastructure state, then presents a human-readable diff of what Terraform will do on the next apply. It makes zero changes to real infrastructure — it’s purely informational.
What plan Actually Does
When you run terraform plan, Terraform:
- Reads configuration — loads all
.tffiles in the directory - Refreshes state — queries the cloud provider to get the current real state of managed resources
- Computes the diff — compares desired config vs. current state
- Generates the plan — lists resources to create, update, destroy, or leave unchanged
terraform planReading 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.app will be created + resource "aws_instance" "app" { + ami = "ami-0abcdef1234567890" + instance_type = "t3.micro" + id = (known after apply) + public_ip = (known after apply) + tags = { + "Environment" = "production" + "Name" = "app-server" } }
# aws_instance.old will be updated in-place ~ resource "aws_instance" "old" { ~ instance_type = "t3.small" -> "t3.medium" # Change id = "i-1234567890abcdef0" # Unchanged }
# aws_security_group.web must be replaced -/+ resource "aws_security_group" "web" { ~ name = "old-sg" -> "new-sg" # Forces replacement - id = "sg-abc123" -> (known after apply) }
Plan: 1 to add, 1 to change, 0 to destroy. (1 to replace)Output symbols:
| Symbol | Meaning |
|---|---|
+ | Resource will be created |
- | Resource will be destroyed |
~ | Resource will be updated in-place |
-/+ | Resource will be destroyed and recreated |
<= | Data source will be read |
Saving a Plan File
The most important production pattern — save the plan and apply exactly that plan:
# Save plan to a binary fileterraform plan -out=tfplan
# Apply the saved plan (no re-plan, no surprises)terraform apply tfplanWhy this matters: between plan and apply, someone else could apply a change. Applying a saved plan file guarantees that what you reviewed is exactly what gets executed — critical for compliance and change management.
Key Flags
# Override a variableterraform plan -var="environment=production"terraform plan -var="instance_count=3" -var="region=us-west-2"
# Use a variable fileterraform plan -var-file="environments/production.tfvars"
# Plan only specific resource(s)terraform plan -target=aws_instance.appterraform plan -target=module.networking
# Show a destroy planterraform plan -destroy
# Skip state refresh (faster; use only when you know state is accurate)terraform plan -refresh=false
# JSON output for toolingterraform plan -json -out=tfplan.json
# Compact output for large plansterraform plan -compact-warnings
# Non-interactive (CI/CD)terraform plan -input=false -out=tfplanPlan Output Sections
A complete plan shows five sections:
1. Changes to Outputs: # Output values that will change2. Terraform will perform: # Resource-level changes3. Plan summary: # X to add, Y to change, Z to destroy4. Note about sensitive: # (if sensitive values are involved)5. Saved plan path: # (if -out was used)Understanding “Forces Replacement”
Some attribute changes require destroying and recreating the resource instead of updating in-place. Terraform marks these with # forces replacement:
# aws_instance.app must be replaced -/+ resource "aws_instance" "app" { ~ ami = "ami-old" -> "ami-new" # forces replacement }Common attributes that force replacement on EC2:
ami— changing the AMI requires a new instanceavailability_zone— can’t move an instance between AZssubnet_id— moving an instance between subnets
For stateless resources, this is usually fine. For databases, a forced replacement is catastrophic — protect with prevent_destroy = true in the lifecycle block.
Plan in CI/CD Pipelines
name: Terraform Plan
on: pull_request: paths: ['infrastructure/**']
jobs: plan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3 with: terraform_version: "1.8.0"
- name: Init run: terraform init -input=false working-directory: infrastructure/
- name: Validate run: terraform validate
- name: Plan id: plan run: terraform plan -input=false -no-color -out=tfplan working-directory: infrastructure/
- name: Post plan to PR uses: actions/github-script@v7 with: script: | const output = `${{ steps.plan.outputs.stdout }}`; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `## Terraform Plan\n\`\`\`\n${output}\n\`\`\`` });Using Infracost with Plan
# Install infracost, then:terraform plan -out=tfplaninfracost breakdown --path tfplan
# Output:# Monthly estimate:# aws_instance.app: $8.47/mo (t3.micro, us-east-1)# aws_db_instance.main: $26.28/mo (db.t3.micro, 20GB gp2)# Total: $34.75/moCost estimation from plan output before applying is a 2025 best practice for catching expensive mistakes early.