terraform graph
Terraform builds an internal dependency graph from your configuration and state, then walks it to decide the order in which resources are created, updated, or destroyed. The terraform graph command exposes that graph as DOT — a plain-text description you can render into a diagram. This is invaluable for understanding implicit dependencies, debugging unexpected ordering, and communicating architecture to your team. OpenTofu ships the same command (tofu graph) with identical behavior.
How the dependency graph works
Terraform never relies on the order resources appear in your .tf files. Instead, it infers edges from references: when one resource interpolates an attribute of another, an edge is added so the referenced resource is processed first. This is why explicit depends_on is only needed when a dependency exists outside of Terraform’s visibility.
Consider a small VPC stack:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "main"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
}
resource "aws_instance" "web" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
}
Because aws_subnet.public references aws_vpc.main.id, and aws_instance.web references aws_subnet.public.id, Terraform knows it must create the VPC, then the subnet, then the instance — and destroy them in reverse.
Generating the DOT output
Run the command from your initialized working directory. It prints the graph to standard output in DOT format.
terraform graph
Output:
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] aws_instance.web (expand)" [label = "aws_instance.web", shape = "box"]
"[root] aws_subnet.public (expand)" [label = "aws_subnet.public", shape = "box"]
"[root] aws_vpc.main (expand)" [label = "aws_vpc.main", shape = "box"]
"[root] aws_instance.web (expand)" -> "[root] aws_subnet.public (expand)"
"[root] aws_subnet.public (expand)" -> "[root] aws_vpc.main (expand)"
"[root] provider[\"registry.terraform.io/hashicorp/aws\"]" -> "[root] aws_vpc.main (expand)"
}
}
Each arrow A -> B means “A depends on B,” so B is created before A.
Rendering the graph with Graphviz
The raw DOT text is hard to read, so pipe it through Graphviz’s dot tool to produce an image. Install Graphviz first (brew install graphviz, apt install graphviz, or choco install graphviz), then:
terraform graph | dot -Tsvg > graph.svg
You can target other formats just as easily:
terraform graph | dot -Tpng -o graph.png
terraform graph | dot -Tpdf -o graph.pdf
Tip: For large configurations the default graph can be dense and unreadable. Render to SVG (vector, zoomable) rather than PNG, and consider scoping the graph with
-targeton the underlying plan, or splitting the configuration into smaller modules.
Choosing what the graph shows
By default terraform graph reflects the plan-time graph. Flags let you select a different operation or a more detailed view.
| Flag | Description |
|---|---|
-type=plan | Graph for a plan (the default when state or no plan is present). |
-type=apply | Graph for an apply operation. |
-type=plan-destroy | Graph for a destroy plan, showing teardown ordering. |
-type=apply with -plan=FILE | Graph from a saved plan file. |
-draw-cycles | Highlight dependency cycles in red, which is useful when an apply fails with a cycle error. |
-module-depth=n | Limit how deeply module internals are expanded (-1 for unlimited). |
To inspect destroy ordering — handy before running terraform destroy on a production stack:
terraform graph -type=plan-destroy | dot -Tsvg > destroy.svg
To diagnose a Cycle: error, add -draw-cycles so the offending edges stand out:
terraform graph -draw-cycles | dot -Tsvg > cycle.svg
Graphing from a saved plan
If you want the exact graph for a specific planned change, save the plan first and feed it back in. This guarantees the graph matches what terraform apply will execute.
terraform plan -out=tfplan
terraform graph -plan=tfplan | dot -Tsvg > planned.svg
Output:
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] aws_instance.web" [label = "aws_instance.web", shape = "box"]
"[root] aws_subnet.public" [label = "aws_subnet.public", shape = "box"]
"[root] aws_instance.web" -> "[root] aws_subnet.public"
}
}
Best practices
- Render to SVG and commit a generated diagram to your repo’s
docs/folder so reviewers can see architecture changes alongside code changes. - Use
terraform graph -type=plan-destroybefore destroying shared infrastructure to confirm nothing critical is torn down early. - Reach for
-draw-cyclesthe moment Terraform reports a cycle error — it pinpoints the loop far faster than reading raw DOT. - Prefer implicit dependencies (attribute references) over
depends_on; the graph is most accurate when Terraform infers edges itself. - Keep modules small and focused so individual graphs stay legible; a single giant graph signals a configuration that should be split.
- Remember the graph reflects the current configuration and state, so run
terraform initand ensure state is fresh before trusting the output.