Skip to content

Plan & Apply

Terraform’s execution model is built around a core data structure - the Directed Acyclic Graph (DAG) - that maps every dependency relationship in your configuration and drives the correct ordering of every create, update, and destroy operation.


A graph is a collection of nodes (resources) and edges (connections between them). Terraform’s version is:

  • Directed - edges have a direction that defines who depends on whom
  • Acyclic - no circular loops are allowed

This structure is called the resource graph internally and is Terraform’s central data structure for all planning and apply operations.

Terraform’s resource graph contains three types of nodes:

Node TypeDescription
Resource nodeMaps to a specific resource, data source, or individual iteration from count/for_each
Provider configuration nodeOne node per provider configuration (e.g. two AWS provider configs → two nodes)
Resource meta nodeGroups resources when count > 1 - exists mainly to keep graph diagrams clean

The terraform graph command outputs a text representation of your dependency graph in GraphViz .dot format. Pipe it through the dot CLI to produce images:

Terminal window
# Generate a PNG diagram
terraform graph | dot -Tpng > graph.png
# Generate an SVG
terraform graph | dot -Tsvg > graph.svg
# Save the raw .dot file
terraform graph > graph.dot

The -type flag selects which phase to visualise:

ModeWhat it shows
planSimplified dependency view
plan-refresh-onlyRefresh-only plan dependencies
plan-destroyDestroy plan order
applyFull resource-level apply graph (more detail than plan)

Running terraform plan compares your configuration against the current state and produces an execution plan. Terraform offers four planning modes:

ModeCommandWhat it does
Defaultterraform planCompares code + state → generates a change plan
Destroyterraform plan -destroyPlans to tear down all managed infrastructure. Destruction happens in the reverse of creation order.
Refresh-onlyterraform plan -refresh-onlyUpdates local state to reflect real-world infrastructure - no infrastructure changes are made
Targetedterraform plan -target=<address>Plans only the specified resource(s). ⚠️ Anti-pattern - use only for exceptional debugging.

In standard plan output, resources are shown bottom-to-top:

  • Bottom - resources with no dependencies (created first)
  • Top - resources that depend on those below them

-replace supersedes the deprecated terraform taint command. It flags a specific resource for forced destroy + re-create without immediately modifying state, so you can review the plan before committing:

Terminal window
terraform plan -replace="aws_instance.web"

Save a plan to a binary file for later review and guaranteed-unchanged apply:

Terminal window
terraform plan -out=plan.tfplan
# Make the binary human-readable
terraform show plan.tfplan
# Convert to JSON for programmatic processing
terraform show -json plan.tfplan

Input variables let you use the same root module configuration to generate different environments (staging vs. production, multi-tenant, etc.).

MethodHowNotes
-var flagterraform plan -var 'count=3'Highest precedence. Struggles with complex types (objects, maps) and has cross-shell escaping issues.
-var-file flagterraform plan -var-file=staging.tfvarsExplicit file load. Best for complex types.
Auto-loaded .tfvarsNamed terraform.tfvars, terraform.tfvars.json, or *.auto.tfvarsLoaded automatically without a flag.
Environment variablesexport TF_VAR_instance_count=2Case-sensitive variable names. Commonly used by CI/CD systems for auth credentials.
Interactive promptTerraform asks at runtimeOnly triggered if all other methods fail and no default is set.
  1. -var and -var-file CLI flags (last flag wins if specified multiple times)
  2. *.auto.tfvars and *.auto.tfvars.json (processed in lexical/alphabetical order)
  3. terraform.tfvars.json
  4. terraform.tfvars
  5. TF_VAR_ environment variables
  6. Interactive prompt (only if nothing else is set)

Prefer these patterns instead:

ApproachHow
CI/CD secret managersGitHub Actions, Terraform Cloud, and Spacelift all support encrypted secrets that are masked in logs
External secret managersHashiCorp Vault, AWS Secrets Manager, Azure Key Vault - pass the path to the secret rather than the plaintext value; let the orchestrator inject it directly into the application

The apply phase executes the resource graph produced during planning - creating, modifying, or deleting real-world infrastructure to match your configuration.

Section titled “Method 1: Apply from a Saved Plan (Recommended)”
Terminal window
terraform plan -out=plan.tfplan
terraform apply plan.tfplan
  • Separates planning and execution - a human or automated tool can review the exact changes before they run
  • No confirmation prompt - Terraform assumes the saved plan was already reviewed
  • File extension is convention only (.tfplan); any extension works
Terminal window
terraform apply
  • Generates a fresh plan automatically, then pauses for interactive confirmation
  • Accepts all the same flags as terraform plan (-var, -var-file, -refresh-only, etc.)
Terminal window
terraform apply -auto-approve

Skips the confirmation prompt entirely.

Terminal window
terraform destroy

terraform destroy is an alias for terraform apply -destroy. It generates a destroy plan and asks for confirmation before executing.


Terminal window
terraform apply -parallelism=5

Controls how many resource operations run simultaneously (default: 10). Execution is constrained by the dependency graph and provider API rate limits.

By default Terraform locks the state file during apply to prevent concurrent runs from corrupting data:

Terminal window
terraform apply -lock=false # Disable locking

Before generating a plan, Terraform optionally refreshes its view of real-world infrastructure by querying each provider:

Terminal window
terraform plan -refresh-only # Update state without changing infrastructure
terraform apply -refresh-only # Apply the state resync

refresh-only is particularly useful for recovering from failed state updates - situations where Terraform successfully changed infrastructure but crashed before writing the result to state.

ScenarioRecovery
Resources updated or deleted externallyRun terraform apply -refresh-only to resync state
New resources created but missing from stateManually import the resources, or delete them from the cloud and re-run apply

Terminal window
terraform plan -target="aws_instance.web"
terraform apply -target="module.vpc"

Targeting restricts the plan/apply to a specific resource or module. Terraform automatically includes all dependencies of the targeted resource.


Symptom: Terraform fails with a Cycle error listing the resources involved.

Cause: Two or more resources depend on each other in a loop, violating the acyclic rule.

Error: Cycle: aws_security_group.app, aws_instance.web

Fix: Break the chain - find the shared attribute causing the loop and pass its value through an input variable or local instead of a direct resource reference. Complex circular dependencies usually signal that the underlying architecture needs to be simplified.

Symptom: Modifying one resource causes Terraform to plan the forced replacement of many downstream resources.

Cause: Changing a resource attribute triggers a destroy + re-create, which propagates to all dependents.

Fix: Review the plan carefully for “forces replacement” warnings. If you want to apply changes to new resources without destroying existing dependents, add the triggering attribute to the ignore_changes lifecycle block:

lifecycle {
ignore_changes = [ami] # ignore AMI updates that would force replacement
}

Symptom: A resource fails to create because a prerequisite hasn’t launched yet, even though the plan appeared valid.

Cause: The dependency exists at the infrastructure level but isn’t expressed in Terraform code (no attribute cross-reference).

Fix: Add an explicit depends_on:

resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
depends_on = [aws_internet_gateway.main]
}

4 · Always-Detected Changes (State Drift / Eternal Drift)

Section titled “4 · Always-Detected Changes (State Drift / Eternal Drift)”

Symptom: Every terraform plan shows the same change even though nothing in the configuration changed.

Cause: A provider-level mismatch between how you write a value and how the provider’s API returns it - common examples: integer 45 vs float 45.0, TRUE vs true, list order differences.

Fix: Match your configuration exactly to the format the provider API returns. Check the provider docs or run a terraform show after apply to see the canonical format.

5 · Calculated Iterations (count / for_each)

Section titled “5 · Calculated Iterations (count / for_each)”

Symptom: Terraform errors with “The ‘count’/‘for_each’ value depends on resource attributes that cannot be determined until apply.”

Cause: The value passed to count or for_each is only computed after resource creation (e.g. a cloud-assigned IP or resource ID) - Terraform cannot build the graph nodes at plan time.

Fix: Refactor to base iterators on values known at plan time - use input variables or locals. If stuck with for_each, fall back to count + count.index to avoid relying on retrieved values.

Symptom: Infrastructure was successfully changed but Terraform crashed or lost connectivity before writing to the state file.

Resource typeRecovery
Updated or deleted resourcesRun terraform apply -refresh-only to detect and record the actual state
Newly created resourcesRun terraform import to bring them into state, or manually delete them from the cloud provider and re-apply