Skip to main content
RunBook Academy

TerraformVIII · Dependencies and the Resource GraphDependencies

The Dependency Graph

Intermediate⏱ ~14 minbash

What you'll learn

  • Describe the directed acyclic graph Terraform builds from the configuration
  • Distinguish the resource graph from the apply execution order
  • Inspect the graph with `terraform graph` and render it with Graphviz
  • Identify common graph patterns and the failure modes that distort them

Prerequisites

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

Every Terraform apply runs against a directed acyclic graph (DAG). Nodes are addresses: resources, data sources, module outputs, providers, and a handful of internal pseudo-nodes. Edges are dependencies: implicit references, explicit depends_on, and provider-configuration edges.

The graph is the structural model. It says nothing about parallelism or order; it only says what must happen before what. Terraform then takes the graph, runs a topological sort, and slices the sorted order into waves of independent work. The waves run concurrently up to -parallelism.

This lesson is about the graph itself. The next two lessons are about the failure mode (cycles) and the operational controls (parallelism and ordering).

What the graph contains

For a configuration with one VPC, two subnets, and one security group referencing the VPC, the graph has roughly:

Nodes:
  provider["registry.terraform.io/hashicorp/aws"]
  aws_vpc.main
  aws_subnet.a
  aws_subnet.b
  aws_security_group.web

Edges:
  aws_subnet.a -> aws_vpc.main
  aws_subnet.b -> aws_vpc.main
  aws_security_group.web -> aws_vpc.main
  aws_vpc.main -> provider["registry.terraform.io/hashicorp/aws"]
  aws_subnet.a -> provider["registry.terraform.io/hashicorp/aws"]
  aws_subnet.b -> provider["registry.terraform.io/hashicorp/aws"]
  aws_security_group.web -> provider["registry.terraform.io/hashicorp/aws"]

The provider edge is implicit: every resource that uses the AWS provider depends on the provider configuration being initialised. The provider is a leaf node in the create graph; during destroy, the provider is needed by every resource, so it stays in the graph.

For a module, the graph contains the module as a subgraph with the module’s resources as nodes and the inputs as additional edges from the caller.

The graph is rebuilt every plan

The graph is not persisted. Every terraform plan rebuilds the graph from the current configuration plus the current state. State is consulted only to filter out resources that no longer exist in the configuration (and to add implicit edges to resources that are still in state but are not in the configuration, for terraform destroy operations).

The rebuild has two consequences:

  • A configuration change can change the graph. A new reference adds an edge; a removed reference removes one; a depends_on block adds an edge that did not exist before.
  • A state change can change the graph only in narrow ways. A terraform state mv changes the addresses in state; the graph is rebuilt with the new addresses. A terraform import adds a node for a resource that was previously only known to the real world.

terraform graph

The CLI command terraform graph writes the graph in DOT format to stdout. DOT is a graph description language; it can be rendered by Graphviz.

terraform graph > graph.dot

The output looks like:

digraph {
  compound = "true"
  newrank = "true"
  "aws_vpc.main" [label = "aws_vpc.main"]
  "aws_subnet.a" [label = "aws_subnet.a"]
  "aws_subnet.b" [label = "aws_subnet.b"]
  "aws_security_group.web" [label = "aws_security_group.web"]

  "aws_subnet.a" -> "aws_vpc.main"
  "aws_subnet.b" -> "aws_vpc.main"
  "aws_security_group.web" -> "aws_vpc.main"
}

The edges point from dependent to dependency (aws_subnet.a depends on aws_vpc.main, so the edge is aws_subnet.a -> aws_vpc.main). This is the convention DOT uses; for apply order, Terraform walks the graph from leaves (no incoming edges) to roots (no outgoing edges).

Filtering by graph type

terraform graph -type=plan   # default; the planned graph
terraform graph -type=apply  # the apply graph (post-refresh)
terraform graph -type=module # only the module-level nodes

-type=apply is what you usually want when debugging a real apply. It reflects the graph as it stands after the refresh step, which is when data sources are read and the real-world state is folded into the graph.

Filtering by module

terraform graph -module=root
terraform graph -module=module.network

This is useful when the configuration is large and you only want to inspect a slice.

Rendering with Graphviz

Graphviz is a separate tool that ships as the graphviz package on Debian 12 and Ubuntu 24.04. Install it:

sudo apt-get install -y graphviz

Then render the DOT file to PNG:

terraform graph -type=plan | dot -Tpng > graph.png

Open graph.png in any image viewer. The diagram shows nodes as boxes and edges as arrows. For a small configuration (under 20 resources), the diagram is readable. For a large configuration (200+ resources), the diagram is dense and you should filter to a slice first.

terraform graph -type=plan | grep -E "aws_vpc|aws_subnet" | dot -Tpng > network.png

Common graph patterns

Fan-out. A single resource creates many independent children.

   aws_vpc.main
        |
   +----+----+
   |    |    |
  sa    sb   sg

The VPC is one wave; the three children run in parallel in the next wave.

Fan-in. Many resources depend on a single root.

  aws_security_group.web
   ^   ^   ^
   |   |   |
  r1  r2  r3

The security group is one wave; the three consumers run in parallel after it.

Long chain. Each resource depends on the previous one.

  A -> B -> C -> D

The apply runs four serial waves. The chain is rarely intentional; it usually indicates that the operator forgot that one of the edges could be a data source instead of a managed resource.

Forest. Many independent subgraphs.

  Tree1   Tree2   Tree3

The apply runs the trees in parallel; each tree runs its own internal topological order.

Failure modes

  1. Graph changes between plan and apply. A data source refresh during plan surfaces a new attribute; a count expression over that attribute now returns a different value; the graph for the apply is different from the graph for the plan. The plan said “12 resources will be created”; the apply creates 14. Re-plan with -refresh-only first.
  2. Graph is too large to read. 500+ resources in a single root module is a smell. Split into modules, then inspect each module’s graph with -module=module.network.
  3. Graph contains cycles. terraform graph will still emit output, but terraform plan will fail with Error: Cycle. The graph shows the cycle as a closed loop in the rendering.
  4. Graphviz not installed. dot: command not found when piping. Install graphviz from the distro package manager.
  5. DOT output truncated by terminal. Pipe to a file or use less. The default output is plain text; it does not paginate.
  6. Graph changes after a state move. A terraform state mv changes the addresses in state. The next plan rebuilds the graph with the new addresses; edges that pointed to the old address are broken. Re-validate after any state operation.

How to validate

terraform validate
terraform plan -out=tfplan
terraform graph -type=plan > graph.dot
dot -Tpng graph.dot -o graph.png

Read the diagram. For each module, confirm:

  • The expected fan-out and fan-in patterns are present.
  • There are no unexpected long chains.
  • There are no cycles (visually obvious: a closed loop in the arrows).
  • The number of root nodes matches the number of resources with no outgoing edges.

For a 50-resource configuration, this takes five minutes. For a 500-resource configuration, this takes half a day and is a sign that the configuration should be split into modules.

Performance implications

terraform graph is cheap; the cost is reading the configuration and walking the expressions. For a configuration with 10,000 resources, the graph emission takes a few seconds.

Rendering with dot is more expensive. A 1,000-node graph takes a few seconds to render. A 10,000-node graph takes minutes and produces a PNG that is not visually useful. Filter the graph before rendering.

The graph itself does not affect apply performance. The graph is the input to the topological sort and parallelism slicing; the apply performance is bounded by -parallelism and the actual resource operation times.

What to do in production

  • Keep the root module small. Modules with more than 100 resources are hard to inspect and hard to reason about.
  • Run terraform graph on every non-trivial merge request and read the output. Reviewers should reject PRs that add edges without justification.
  • Treat cycles as bugs. The graph should be a DAG; a cycle means the configuration is wrong.
  • For large configurations, use terraform graph -module=... to inspect slices.

Security implications

terraform graph reads the configuration. It does not read state. It does not call provider APIs. It does not leak secrets. The output is a list of addresses; it does not include attribute values.

The graph can reveal the structure of the infrastructure to anyone with read access to the configuration. Treat the rendered PNG as internal documentation; do not publish it externally without review.

Verification

  • Run terraform validate and confirm the configuration is valid.
  • Run terraform plan -out=tfplan and confirm the plan produces the expected set of changes.
  • Run terraform graph -type=plan > graph.dot and inspect the file.
  • Render with dot -Tpng graph.dot -o graph.png and review the diagram.
  • For each module, confirm the expected patterns are present and no cycles exist.

Knowledge check · 7 questions

  1. Q1. What data structure does Terraform build from the configuration before applying?

  2. Q2. The resource graph is identical to the execution order of the apply.

  3. Q3. Which command produces a DOT-format representation of the dependency graph?

  4. Q4. Which of the following are nodes in the Terraform dependency graph? (Select all that apply.)

  5. Q5. You want to visualise the dependency graph for a 200-resource configuration. What is the most reliable approach?

  6. Q6. What is the difference between `terraform graph` and `terraform graph -type=apply`?

  7. Q7. A configuration with a dependency cycle will cause `terraform graph` to error.

Passing score: 75%. Answers are checked in this browser.