terraform plan & terraform apply — Reading the Diff Like a Pro

terraform plan & terraform apply — Reading the Diff Like a Pro
terraform plan — The Safety Net You Should Never Skip

Never run terraform apply without understanding what it’s about to do. terraform plan is the dry run — it computes the full diff between your config and the current state, then shows you exactly what Terraform intends to change. Nothing is actually created, modified, or destroyed during plan.

terraform plan

Why is this so important? Because infrastructure changes are often irreversible or have side effects that aren’t obvious from the code. Changing an RDS instance class might cause a reboot. Modifying a VPC CIDR might force-replace the entire VPC and everything inside it. The plan output tells you exactly what’s happening before it happens.

Reading Plan Output — The Symbols
Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami                          = "ami-0c55b159cbfafe1f0"
      + instance_type                = "t2.micro"
      + id                           = (known after apply)
      + public_ip                    = (known after apply)
    }

  # aws_security_group.web_sg will be updated in-place
  ~ resource "aws_security_group" "web_sg" {
        id   = "sg-0abc123"
      ~ name = "old-name" -> "new-name"
    }

  # aws_instance.old will be destroyed
  - resource "aws_instance" "old" {
      - id            = "i-0def456"
      - instance_type = "t2.micro"
    }

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

The symbols communicate everything:

SymbolMeaningImpact
+Will be createdNew resource, no disruption
-Will be destroyedResource deleted — potentially breaking
~Will be updated in-placeModified without replacement, usually safe
-/+Will be destroyed and recreatedReplacement — brief downtime while old is removed and new is created
<=Will be read (data source)No infrastructure change — just a lookup

The -/+ symbol deserves special attention. It means the resource will be destroyed first, then recreated. This happens when you change an attribute that the provider marks as ForceNew — meaning the cloud doesn’t support modifying it in place. For example, changing an EC2 instance’s ami triggers a -/+ because AWS can’t swap an AMI on a running instance. The instance is terminated and a new one is launched.

Why this matters: if your plan shows -/+ for a database, that means the database will be deleted and recreated. Data loss. If it shows -/+ for a VM behind a load balancer, that means brief downtime. Always look for -/+ symbols before approving a plan.

(known after apply) — Understanding Computed Values

Some values don’t exist until after the resource is created. An EC2 instance’s public IP, an S3 bucket’s ARN, a resource’s auto-generated ID — these are all assigned by the cloud provider at creation time.

+ public_ip = (known after apply)
+ id        = (known after apply)
+ arn       = (known after apply)

This is normal and expected. Terraform knows it needs to create the resource; it just can’t predict what the cloud will assign. Once apply runs, these values are populated in state and available for reference by other resources.

The implication: if resource B depends on resource A’s computed value (like using A’s ID), Terraform must create A first and wait for the value before creating B. This is how the dependency graph works — some ordering constraints come from data flow, not just explicit depends_on.

Saving a Plan: The CI/CD Essential
terraform plan -out=myplan.tfplan

This saves the plan to a binary file. You can then apply exactly that plan:

terraform apply myplan.tfplan

Why is saving plans critical in CI/CD? Without a saved plan, terraform apply re-runs the plan internally before applying. If something changes in the cloud between your plan review and the apply — someone modifies a resource manually, an auto-scaling event occurs, a new AMI is published — you’d apply something nobody reviewed.

The saved plan is a contract: “This is exactly what was reviewed. This is exactly what gets applied.” No re-planning, no drift between review and execution.

In a production pipeline:

# Stage 1: Plan and save
terraform init
terraform plan -out=plan.tfplan

# Stage 2: Human reviews plan output in PR
# Reviewer approves

# Stage 3: Apply the exact saved plan
terraform init
terraform apply plan.tfplan  # No confirmation prompt needed — it's a saved plan

This pattern is the gold standard. The approved plan is exactly what gets applied. No surprises.

terraform apply — Actually Making Changes
terraform apply                   # Shows plan, asks for confirmation
terraform apply -auto-approve     # Skips the confirmation prompt
terraform apply myplan.tfplan     # Applies a saved plan

The interactive apply flow:
1. Runs a fresh plan (unless you passed a saved plan file)
2. Shows the plan output
3. Asks: Do you want to perform these actions? Enter a value: yes
4. Executes changes following the dependency graph — parallel where safe, sequential where dependencies require it
5. Writes results to state

The -auto-approve flag skips step 3. You’d use this in CI/CD when the plan was already reviewed and approved. Never use it interactively against production unless you’ve just reviewed the plan and are confident.

One thing to understand about apply execution: Terraform doesn’t apply changes one by one. It parallelizes independent operations. If you’re creating 10 security groups that don’t depend on each other, Terraform creates them simultaneously (up to the parallelism limit, which defaults to 10). This is why a 50-resource environment deploys in minutes, not in the hour it would take sequentially.

The Resource Dependency Graph

Terraform builds a directed acyclic graph (DAG) of all resources based on their dependencies. This graph determines execution order — both for creation and destruction.

resource "aws_vpc" "main" { ... }

resource "aws_subnet" "public" {
  vpc_id = aws_vpc.main.id   # Implicit dependency on aws_vpc.main
  ...
}

resource "aws_instance" "web" {
  subnet_id = aws_subnet.public.id  # Implicit dependency on aws_subnet.public
  ...
}

Terraform derives the order automatically: create VPC first → then subnet → then instance. On destroy, the order reverses: instance first → then subnet → then VPC.

Dependencies are usually implicit — Terraform detects them when one resource references another’s attributes. Sometimes you need explicit dependencies using depends_on — when there’s a relationship the code doesn’t express (like needing an IAM policy attachment to exist before an EC2 instance boots, even though there’s no direct attribute reference).

You can visualize the graph:

terraform graph | dot -Tsvg > graph.svg

This produces a visual DAG. Useful for debugging ordering issues in complex configs.

Targeting: Use Sparingly
terraform plan -target=aws_instance.web
terraform apply -target=aws_instance.web

This limits the plan/apply to a single resource (and its dependencies). Terraform ignores everything else.

When it seems useful: quick-fixing a single broken resource during an incident. Iterating on one resource during development without waiting for 50 others to be evaluated.

Why it’s dangerous: it creates state inconsistency. Other resources that depend on the targeted one may be out of sync. Resources that the targeted one depends on might not be updated. -target is a tactical escape hatch, not a workflow. Using it routinely means your state is perpetually partial.

HashiCorp explicitly recommends against routine use of -target. If you find yourself using it frequently, your Terraform configs are probably too large and should be split into smaller, independent workspaces.

Design Scenario: Reading a Surprising Plan

You change a tag on an EC2 instance and run terraform plan. Expected: 1 to change. Actual output:

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

Wait — what’s being destroyed? You scroll up and see a -/+ for the instance’s security group. The security group has name_prefix set, and the AWS provider marks it as ForceNew. Changing the name triggers a replacement of the security group, which in turn might affect instances referring to it.

This is why you read every line of the plan, not just the summary. The summary says “1 to destroy.” The details explain what and why. If you’d applied without reading, you’d have caused downtime.

Key Takeaways

Always read the plan summary line: Plan: 2 to add, 1 to change, 3 to destroy. If you’re expecting 1 add and 0 destroys but see 3 destroys — stop. Investigate before applying. The plan is your last line of defense.

The -/+ symbol is the most dangerous plan output. It means a resource will be destroyed and recreated. For stateless resources like security groups, this might be harmless. For stateful resources like databases or instances with local data, it means data loss. Train yourself to spot it.

Saved plan files (-out=) are the mechanism that makes CI/CD pipelines trustworthy. Without them, there’s a gap between “what was reviewed” and “what was applied.” In production, that gap is where outages hide.