Execution Plan #
When you run terraform plan, Terraform doesn’t just make a random list of changes. Behind the scenes, it builds a dependency graph — a data structure that determines the exact execution order, which resources can run in parallel, and which resources must wait for others to finish first. Understanding how the execution plan is built helps you write more efficient configurations and debug problems that aren’t obvious on the surface.
What Is a Dependency Graph #
A dependency graph is the data structure Terraform builds from your configuration. Every resource is a node in the graph, and every reference between resources is an edge (a dependency direction). Terraform automatically infers dependencies from references — you don’t need to specify the execution order manually.
flowchart TD
A["aws_vpc.main\n(no dependencies,\nexecuted first)"] --> B["aws_subnet.public\n(requires the VPC)"]
A --> C["aws_internet_gateway.main\n(requires the VPC)"]
A --> D["aws_route_table.public\n(requires the VPC)"]
B --> E["aws_instance.web\n(requires the subnet)"]
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#e8f5e9,stroke:#2e7d32
style C fill:#e8f5e9,stroke:#2e7d32
style D fill:#e8f5e9,stroke:#2e7d32
style E fill:#fff3e0,stroke:#e65100# The configuration producing the graph above:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # ← edge: subnet → VPC
cidr_block = "10.0.1.0/24"
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id # ← edge: IGW → VPC
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id # ← edge: route table → VPC
}
resource "aws_instance" "web" {
subnet_id = aws_subnet.public.id # ← edge: instance → subnet
ami = var.ami_id
}
| Dependency Level | Resource | Depends On | Can Run Parallel With |
|---|---|---|---|
| Level 0 (root) | aws_vpc.main | Nothing | — |
| Level 1 | aws_subnet.public | VPC | internet_gateway, route_table |
| Level 1 | aws_internet_gateway.main | VPC | subnet, route_table |
| Level 1 | aws_route_table.public | VPC | subnet, internet_gateway |
| Level 2 | aws_instance.web | Subnet | Nothing in this example |
How Terraform infers dependencies: Every time you writeaws_vpc.main.idoraws_subnet.public.idinside a resource block, Terraform automatically detects that the resource depends on the owner of that attribute. No manual dependency declaration needed.
Parallelism in the Execution Plan #
Terraform automatically executes resources in parallel as long as there are no dependencies between them. This makes Terraform far more efficient than sequential scripts.
gantt
title Sequential vs Parallel Execution Comparison
dateFormat X
axisFormat %s
section Sequential (Bash)
VPC :a1, 0, 2s
Subnet :a2, 2, 4s
IGW :a3, 4, 6s
Route Table :a4, 6, 8s
Instance :a5, 8, 10s
section Parallel (Terraform)
VPC :b1, 0, 2s
Subnet :b2, 2, 4s
IGW :b3, 2, 4s
Route Table :b4, 2, 4s
Instance :b5, 4, 6sSEQUENTIAL (bash script):
VPC: ████ (2s)
Subnet: ░░░░████ (4s after VPC)
IGW: ░░░░░░░░████ (6s after subnet)
RT: ░░░░░░░░░░░░████ (8s after IGW)
Instance:░░░░░░░░░░░░░░░░████ (10s after RT)
Total: ~14 seconds (everything sequential)
PARALLEL (Terraform):
VPC: ████ (2s)
Subnet: ░░░░████ (starts after VPC)
IGW: ░░░░████ (starts together with the subnet)
RT: ░░░░████ (starts together with the subnet and IGW)
Instance:░░░░░░░░████ (waits for the subnet to finish)
Total: ~6 seconds (VPC → parallel → instance)
| Aspect | Sequential (Script) | Parallel (Terraform) |
|---|---|---|
| Total time (example) | ~14 seconds | ~6 seconds |
| Independent resources | Run one after another | Run simultaneously |
| Default concurrency | 1 by 1 | 10 at once |
| Efficiency | Low | High |
# Adjust parallelism as needed
terraform plan # Default: 10 parallel
terraform apply -parallelism=5 # Reduce if rate-limited
terraform apply -parallelism=20 # Increase for large infra
Reading the Execution Plan in Detail #
The terraform plan output shows the order Terraform will follow, but doesn’t explicitly show parallelism. To see the dependency graph visually, use terraform graph.
flowchart TD
A["terraform graph"] --> B["DOT format (Graphviz)"]
B --> C["Render to SVG/PNG\n(dot -Tsvg / dot -Tpng)"]
B --> D["Render to JSON\n(terraform show -json)"]
E["terraform graph -plan=tfplan"] --> F["Graph specific\nto a particular\nplan"]
style A fill:#e3f2fd,stroke:#1565c0
style E fill:#e3f2fd,stroke:#1565c0# Generate the dependency graph in DOT format (Graphviz)
terraform graph
# Output (snippet):
# digraph {
# compound = "true"
# newrank = "true"
# "aws_vpc.main" -> "aws_subnet.public"
# "aws_vpc.main" -> "aws_internet_gateway.main"
# "aws_subnet.public" -> "aws_instance.web"
# }
# Render to an image (requires Graphviz installed: brew install graphviz)
terraform graph | dot -Tsvg > graph.svg
terraform graph | dot -Tpng > graph.png
# For a specific plan (not the current configuration)
terraform graph -plan=tfplan
Explicit Dependencies with depends_on
#
Terraform infers dependencies from direct references (aws_vpc.main.id). But there are cases where a dependency isn’t visible from references — for example, a dependency on a side effect.
flowchart TD
A["Dependency between resources"] --> B{"Can it be inferred\nfrom a reference?"}
B -->|"Yes: vpc_id = aws_vpc.main.id"| C["✅ Implicit dependency\nAutomatically detected\nNo depends_on needed"]
B -->|"No: side effect\ne.g. IAM policy\nalready attached"| D["⚠️ Explicit dependency\nNeeds depends_on"]
D --> E[""depends_on = [\n aws_iam_role_policy_attachment.node_policy\n""]
C --> F["Maximum parallelism\nEfficient"]
E --> G["Reduces parallelism\nResources wait\nunnecessarily"]
style C fill:#e8f5e9,stroke:#2e7d32
style D fill:#fff3e0,stroke:#e65100
style F fill:#e8f5e9,stroke:#2e7d32
style G fill:#ffebee,stroke:#c62828When depends_on Is Needed
#
# CASE: An EKS Node Group needs an IAM policy already attached to the role
# This dependency CANNOT be inferred from resource references alone
# because the node_group only references the role ARN, not the policy attachment
resource "aws_iam_role" "node" {
name = "eks-node-role"
# ...
}
resource "aws_iam_role_policy_attachment" "node_policy" {
role = aws_iam_role.node.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}
resource "aws_eks_node_group" "workers" {
cluster_name = aws_eks_cluster.main.name
node_role_arn = aws_iam_role.node.arn
# ✅ depends_on is needed here because:
# the node_group references the role ARN (not the policy attachment)
# Terraform doesn't know the node_group needs the policy already attached
depends_on = [
aws_iam_role_policy_attachment.node_policy
]
}
| Case | depends_on? | Explanation |
|---|---|---|
Resource B references resource_a.id | ❌ Not needed | The dependency is already implicit from the reference |
| Resource B needs a side effect from A | ✅ Needed | E.g. an IAM policy must be attached first |
| Resource B needs A to finish first | ❌ Check first | It may be implicit from a reference |
Anti-Pattern: Excessive depends_on
#
# ❌ ANTI-PATTERN: Unnecessary depends_on
resource "aws_subnet" "public" {
depends_on = [aws_vpc.main] # ✗ Not needed!
vpc_id = aws_vpc.main.id # This reference is already enough
cidr_block = "10.0.1.0/24"
}
# ✅ CORRECT: Let the reference determine the dependency automatically
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # ✓ The dependency is already implied
cidr_block = "10.0.1.0/24"
}
flowchart TD
A["Excessive depends_on"] --> B["Resources wait for\nresources that could\nactually run in parallel"]
B --> C["Reduced parallelism\nSlower applies"]
B --> D["Dependency graph\nmore complex\nthan needed"]
E["Best practices"] --> F["Use direct references\nfor implicit dependencies"]
E --> G["Use depends_on\nonly for side effects"]
E --> H["Document the reason\nfor every depends_on"]
style A fill:#ffebee,stroke:#c62828
style C fill:#ffebee,stroke:#c62828
style E fill:#e8f5e9,stroke:#2e7d32The Execution Plan in JSON Format #
For integration with external tooling (CI/CD, policy checkers, notifications), the execution plan can be exported in JSON format. This enables programmatic inspection of the planned changes.
flowchart TD
A["terraform plan\n-out=tfplan"] --> B["terraform show\n-json tfplan > plan.json"]
B --> C["Analyze with jq\nor custom scripts"]
B --> D["Policy checks\n(OPA, Sentinel)"]
B --> E["Notifications\nSlack, Teams, etc."]
B --> F["Approval gates\nCI/CD pipeline"]
C --> G["Check the destroy count\nCheck resource types\nCheck blast radius"]
D --> H["Validate compliance\nbefore applying"]
E --> I["The team knows\nwhat will change"]
F --> J["Manual approval\nbefore applying"]
style B fill:#e3f2fd,stroke:#1565c0
style G fill:#e8f5e9,stroke:#2e7d32
style H fill:#e8f5e9,stroke:#2e7d32# Generate a plan and export to JSON
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
# Plan JSON structure (snippet):
# {
# "format_version": "1.2",
# "resource_changes": [
# {
# "address": "aws_instance.web",
# "change": {
# "actions": ["create"],
# "before": null,
# "after": {
# "ami": "ami-0abcdef1234567890",
# "instance_type": "t3.micro"
# }
# }
# }
# ]
# }
# Example: Check if any resources will be destroyed
terraform show -json tfplan | \
jq '[.resource_changes[] | select(.change.actions[] == "delete")] | length'
# Output: 0 (nothing will be deleted)
# Example: Count the resources that will change
terraform show -json tfplan | \
jq '[.resource_changes[] | select(.change.actions[] != "no-op")] | length'
# Output: 5 (5 resources will change)
# Example: List all resources that will be created
terraform show -json tfplan | \
jq -r '.resource_changes[] | select(.change.actions[] == "create") | .address'
# Output:
# aws_vpc.main
# aws_subnet.public
# aws_instance.web
| jq Query | Use Case | Best For |
|---|---|---|
select(.actions[] == "delete") | Detect resources to be deleted | Safety check before apply |
select(.actions[] != "no-op") | Count total changes | Blast radius assessment |
select(.actions[] == "create") | List new resources | Review new resources |
select(.actions[] == "update") | List changed resources | Review changes |
Summary #
- The dependency graph is built automatically from references between resources — you don’t need to specify the execution order manually.
- Independent resources execute in parallel — Terraform runs up to 10 operations simultaneously by default for maximum efficiency.
terraform graphproduces a visualization of the dependency graph — useful for understanding complex configurations. Render it to SVG/PNG with Graphviz.depends_onis for side-effect dependencies — use it only when a dependency can’t be inferred from a direct reference (e.g. IAM policy attachments).- Don’t overuse
depends_on— every explicitdepends_onreduces parallelism and can slow down applies.- Export the plan to JSON (
terraform show -json tfplan) for CI/CD integration — it enables programmatic checks like “no resources will be destroyed” or “blast radius assessment”.