TerraformVIII · Dependencies and the Resource GraphDependencies
The Dependency Graph
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
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_onblock adds an edge that did not exist before. - A state change can change the graph only in narrow ways. A
terraform state mvchanges the addresses in state; the graph is rebuilt with the new addresses. Aterraform importadds 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
- Graph changes between plan and apply. A data source refresh
during plan surfaces a new attribute; a
countexpression 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-onlyfirst. - 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. - Graph contains cycles.
terraform graphwill still emit output, butterraform planwill fail withError: Cycle. The graph shows the cycle as a closed loop in the rendering. - Graphviz not installed.
dot: command not foundwhen piping. Installgraphvizfrom the distro package manager. - DOT output truncated by terminal. Pipe to a file or use
less. The default output is plain text; it does not paginate. - Graph changes after a state move. A
terraform state mvchanges 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 graphon 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 validateand confirm the configuration is valid. - Run
terraform plan -out=tfplanand confirm the plan produces the expected set of changes. - Run
terraform graph -type=plan > graph.dotand inspect the file. - Render with
dot -Tpng graph.dot -o graph.pngand review the diagram. - For each module, confirm the expected patterns are present and no cycles exist.
Knowledge check · 7 questions
Q1. What data structure does Terraform build from the configuration before applying?
Q2. The resource graph is identical to the execution order of the apply.
Q3. Which command produces a DOT-format representation of the dependency graph?
Q4. Which of the following are nodes in the Terraform dependency graph? (Select all that apply.)
Q5. You want to visualise the dependency graph for a 200-resource configuration. What is the most reliable approach?
Q6. What is the difference between `terraform graph` and `terraform graph -type=apply`?
Q7. A configuration with a dependency cycle will cause `terraform graph` to error.
Passing score: 75%. Answers are checked in this browser.