Skip to main content

What is the Resource Graph?

The resource graph is a directed acyclic graph (DAG) that Terraform constructs to represent the relationships between all the objects in your configuration. The graph determines the order in which operations are performed and enables parallel execution when possible.
Every Terraform operation (plan, apply, destroy, etc.) builds its own graph tailored to that specific operation. The graph structure varies based on what needs to be accomplished.Source: docs/architecture.md

Graph Components

Vertices (Nodes)

Vertices represent objects or actions in your infrastructure:
  • Resource Instances - Individual resource blocks or their instances (from count/for_each)
  • Provider Configurations - Provider initialization and configuration
  • Module Instances - Module boundaries and expansion points
  • Output Values - Output value evaluation
  • Variables - Input variable evaluation
  • Data Sources - Data source reads
Implementation: Each vertex type implements different interfaces in internal/terraform/

Edges

Edges represent “must happen after” relationships:
A ──→ B    means    "B must happen after A"
Edges ensure:
  • Resources are created before their dependents
  • Providers are initialized before being used
  • Dependencies are destroyed after their dependents
Terraform uses dependency order, not execution order. The graph walker respects edges but may execute multiple vertices concurrently when they have no dependencies between them.

DAG Implementation

Terraform’s DAG is implemented in internal/dag:
// AcyclicGraph is the core graph type
type AcyclicGraph struct {
    Graph
}

// Graph contains vertices and edges
type Graph struct {
    vertices Set
    edges    Set
}
Source: dag/dag.go and dag/graph.go

Why Acyclic?

Graphs must be acyclic (no cycles) because cycles would create impossible dependency situations:
❌ INVALID (Cycle):
A → B → C → A

Resource A needs B, which needs C, which needs A!
Terraform validates graphs and returns an error if cycles are detected.

Graph Building Process

Graphs are built using the Transform pattern, where a series of graph transformers progressively build up the graph.
1

Start with Empty Graph

Begin with an empty graph for the target module path.
2

Apply Transforms

Run a sequence of GraphTransformer implementations that each modify the graph.
3

Validate Graph

Ensure the resulting graph is a valid DAG (acyclic, connected properly).
4

Return Graph

Return the completed graph ready for walking.
Implementation: graph_builder.go

Key Graph Transforms

Different transforms build different parts of the graph:
TransformPurposeImplementation
ConfigTransformerAdd vertices for resource blockstransform_config.go
StateTransformerAdd vertices for resources in statetransform_state.go
ReferenceTransformerCreate dependency edges from referencestransform_reference.go
ProviderTransformerAssociate resources with providerstransform_provider.go
ModuleVariableTransformerHandle module input variablestransform_module_variable.go
OutputTransformerAdd output value verticestransform_output.go
TransitiveReductionTransformerOptimize by removing redundant edgestransform_transitive_reduction.go
Source: docs/architecture.md

Example: Plan Graph Building

The plan graph builder applies transforms in this order:
type PlanGraphBuilder struct {
    Steps: []GraphTransformer{
        // Add configuration resources
        &ConfigTransformer{},
        
        // Add state resources
        &StateTransformer{},
        
        // Add providers
        &ProviderTransformer{},
        
        // Create edges from references
        &ReferenceTransformer{},
        
        // Handle module expansion
        &ModuleExpansionTransformer{},
        
        // Optimize edges
        &TransitiveReductionTransformer{},
        
        // ... more transforms
    }
}
Source: graph_builder_plan.go

Dependency Detection

Terraform automatically detects dependencies through reference analysis.

Implicit Dependencies

Dependencies are inferred from resource references:
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "app" {
  vpc_id = aws_vpc.main.id  # ← Creates dependency edge
  cidr_block = "10.0.1.0/24"
}
Resulting Graph:
aws_vpc.main ──→ aws_subnet.app
The ReferenceTransformer analyzes expressions to find references:
1

Parse Expressions

Extract all references from each resource’s configuration using lang.References.
2

Match to Vertices

Find which graph vertices correspond to each reference.
3

Create Edges

Add “happens after” edges from the vertex to its dependencies.

Explicit Dependencies

Use depends_on for dependencies that can’t be inferred:
resource "aws_iam_role_policy" "example" {
  name = "example"
  role = aws_iam_role.example.id
  
  # Ensure role is created before policy
  depends_on = [aws_iam_role.example]
}

resource "null_resource" "example" {
  # Wait for EC2 instance even though not referenced
  depends_on = [aws_instance.web]
}
Only use depends_on when necessary. Terraform automatically handles most dependencies through references. Overuse of depends_on reduces parallelism and increases apply time.

Graph Walking

Once built, the graph is “walked” to execute operations:
// Walker executes vertices in parallel
type Walker struct {
    Callback WalkFunc     // Function to call for each vertex
    Reverse  bool         // Walk direction
}

// Walk the graph
func (g *AcyclicGraph) Walk(walker *Walker) error {
    // Execute vertices in dependency order
    // Multiple vertices can execute concurrently
}
Source: dag/walk.go

Walk Algorithm

The walk algorithm from dag.AcyclicGraph.Walk:
1

Find Ready Vertices

Identify vertices with no unmet dependencies (all incoming edges satisfied).
2

Execute Concurrently

Execute all ready vertices in parallel (up to parallelism limit).
3

Mark Complete

When a vertex completes, mark it done and update dependent vertices.
4

Repeat

Find newly ready vertices and repeat until all vertices are complete.

Concurrency Control

The graph walker manages concurrency:
type ContextGraphWalker struct {
    // Semaphore limits concurrent operations
    parallelSem Semaphore
    
    // Thread-safe state access
    syncState *states.SyncState
}
Default parallelism: 10 concurrent operations Source: graph_walk.go and context.go
You can adjust parallelism with the -parallelism flag:
terraform apply -parallelism=20
Higher values increase speed but may hit provider API rate limits.

Vertex Execution

Each vertex type has its own execution logic:
// Vertices implement Execute to perform their work
type GraphNodeExecutable interface {
    Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics
}
Key vertex execution implementations: Source: docs/architecture.md

Execution Steps Example

During plan operation, resource instance execution:
1

Get Provider

Retrieve the provider from EvalContext (already initialized via dependency edge).
2

Get Prior State

Retrieve current state for this specific resource instance.
3

Evaluate Configuration

Evaluate attribute expressions, fetching values from dependencies via EvalContext.
4

Call Provider

Call provider’s PlanResourceChange to produce the planned changes.
5

Save Diff

Store the resulting diff in the plan being constructed.
Source: docs/architecture.md

Dynamic Graph Expansion

Some vertices dynamically expand into subgraphs during execution.

Count and For-Each

Resources with count or for_each expand dynamically:
resource "aws_instance" "web" {
  count = 3  # Unknown until evaluation
  # ...
}
Graph Evolution:
1. Initial graph:
   aws_instance.web (unexpanded)

2. After expansion:
   aws_instance.web[0]
   aws_instance.web[1]
   aws_instance.web[2]
This is necessary because count may reference other resources whose values aren’t known when the main graph is built:
resource "aws_instance" "web" {
  # Count depends on another resource!
  count = length(aws_subnet.app[*].id)
  # ...
}
Implementation: Vertices implement GraphNodeDynamicExpandable to create subgraphs. Source: docs/architecture.md

Graph Types by Operation

Different operations use different graph builders:
Purpose: Calculate proposed changesKey vertices:
  • Configuration resources (from .tf files)
  • State resources (from state file)
  • Providers
Builder: PlanGraphBuilder

Visualizing Graphs

Terraform can output graph visualizations:
# Generate DOT format graph
terraform graph > graph.dot

# Render with Graphviz
dot -Tpng graph.dot > graph.png

Graph DOT Output

Implementation in graph_dot.go generates Graphviz DOT format:
digraph {
  "aws_vpc.main" -> "aws_subnet.app"
  "aws_subnet.app" -> "aws_instance.web"
}
Use graph visualization to debug dependency issues or understand complex configurations:
terraform graph | dot -Tsvg > graph.svg
open graph.svg

Graph Optimization

Transitive Reduction

The TransitiveReductionTransformer removes redundant edges: Before optimization:
A ──→ B ──→ C
A ──────────→ C  (redundant!)
After optimization:
A ──→ B ──→ C
This improves:
  • Graph visualization clarity
  • Walk performance (fewer edges to check)
  • Memory usage
Implementation uses the Tarjan algorithm for strongly connected components.

Error Handling

Graph walking stops on errors:
// If a vertex execution fails
func (v *SomeVertex) Execute(ctx EvalContext) tfdiags.Diagnostics {
    if err := doSomething(); err != nil {
        return diags.Append(err)
    }
}

// Walk halts and returns diagnostics
Dependent vertices are skipped when upstream vertices fail. Their diagnostics are excluded from the final error report since they’re caused by upstream failures.Source: dag/walk.go

Real-World Example

Given this configuration:
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "app" {
  vpc_id = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

resource "aws_instance" "web" {
  count = 2
  subnet_id = aws_subnet.app.id
  ami = "ami-123456"
}
Resulting Plan Graph: Execution Order:
  1. Initialize provider.aws
  2. Create aws_vpc.main
  3. Create aws_subnet.app
  4. Create aws_instance.web[0] and aws_instance.web[1] in parallel

Best Practices

Use resource references instead of depends_on whenever possible:
# Good - automatic dependency
subnet_id = aws_subnet.app.id

# Avoid unless necessary
depends_on = [aws_subnet.app]
Design your infrastructure to avoid cycles:
# ❌ This creates a cycle!
resource "aws_security_group" "a" {
  egress {
    security_groups = [aws_security_group.b.id]
  }
}

resource "aws_security_group" "b" {
  egress {
    security_groups = [aws_security_group.a.id]
  }
}
Use security group rules instead to break the cycle.
When dependency issues occur, visualize the graph:
terraform graph | dot -Tsvg > graph.svg
Higher parallelism = faster execution but:
  • May hit API rate limits
  • Uses more memory
  • Harder to debug with concurrent errors

Common Graph Issues

Cycle Errors

Error: Cycle: aws_security_group.a, aws_security_group.b
Solution: Redesign to break the cycle, often by using separate rule resources.

Missing Dependencies

Error: ... object has been deleted
Solution: Add explicit depends_on or reference the resource attribute.

Over-parallelization

Error: rate limit exceeded
Solution: Reduce parallelism: terraform apply -parallelism=5

Next Steps

State Management

Learn how state is accessed and updated during graph walks

References

Build docs developers (and LLMs) love