Skip to main content

[Terraform Cheat Sheet] Reading terraform graph Output

Starting a running cheat sheet rather than a single post — commands and reference notes worth keeping close, pulled from whatever I’m actually using that week. First entry: terraform graph, using the S3 + CloudFront stack this site itself runs on as the example.

Rendering it on Windows

winget install Graphviz.Graphviz

Add to PATH:

C:\Program Files\Graphviz\bin

Generate the graph and render it:

terraform graph > C:\Temp\graph.dot

(Get-Content C:\Temp\graph.dot -Raw) |
  Set-Content C:\Temp\graph-clean.dot -Encoding ascii

& "C:\Program Files\Graphviz\bin\dot.exe" `
  -Tpng C:\Temp\graph-clean.dot `
  -o C:\Temp\graph.png

That middle step — re-encoding to ASCII — matters more than it looks. terraform graph’s output on Windows can carry characters dot.exe chokes on depending on your shell’s default encoding; re-saving as plain ASCII before rendering avoids a class of “why is Graphviz throwing a parse error on a file I didn’t touch” problems.

Three graphs, not one

terraform graph isn’t a single command — the -type flag changes what you get, and the three types answer different questions:

terraform graph                    # config-level dependencies only
terraform graph -type=apply        # full execution graph, incl. internal expand/close nodes
terraform graph -type=plan-destroy # the reverse: destroy ordering

Screenshot: terraform graph default output showing config-level dependencies for the S3+CloudFront+OIDC stack — plain resource and data source boxes only.

The plain terraform graph (no -type) is the one to reach for first — it shows only resources and data sources, no internals, which is exactly what you want when the question is “what depends on what.” The apply and plan-destroy variants add a lot more nodes — module expansion, provider open/close lifecycle, output and local evaluation — genuinely useful for debugging a specific ordering problem, genuinely noisy for just understanding a config.

How to read the chart

1. Shapes matter

Confirmed straight from Terraform’s own DOT output, not just visual guessing:

  • Box (rectangle) — a resource or data source. aws_s3_bucket.site, data.aws_route53_zone.site.
  • Diamond — a provider configuration. provider["registry.terraform.io/hashicorp/aws"].
  • Note shape (a box with a folded corner — subtle enough to miss at a glance in a low-res screenshot) — a variable. var.domain_name, var.bucket_name.
  • Ellipse (oval), no explicit shape set — everything else: (expand) and (close) internal lifecycle nodes, module boundaries, the root node itself. Only shows up in the apply and plan-destroy graphs, not the plain one.

2. Arrow direction — the part worth double-checking

This is where it’s genuinely easy to get turned around, and not just for humans — there’s an open GitHub issue against Terraform itself pointing out that even AI tools misread plan-output arrows something like 40–50% of the time. Worth being precise about it here rather than trusting intuition.

The rule: A → B means A depends on B. The arrowhead points at the thing that has to exist first. The node with the outgoing arrow — the tail — is the one waiting, and gets created or evaluated after the node it points to.

aws_cloudfront_distribution.site
              |
              v
      aws_s3_bucket.site

Reading that correctly: CloudFront depends on the S3 bucket. The bucket has to exist first — CloudFront’s origin config needs to reference it — so Terraform creates the bucket, then the distribution. The arrow points at the dependency, not at “what happens next.”

It’s worth drawing this out explicitly because the instinctive reading — “the arrow shows the order things happen in, left to right” — is exactly backwards from how Terraform’s own DOT output actually works. A → B reads as “A needs B,” not “A, then B.”

3. Top node: root

[root] root

Only shows up in the apply/plan-destroy graphs. This is Terraform’s root module — everything in a single, non-nested config belongs to it by default:

.
├── main.tf
├── outputs.tf
└── variables.tf

Screenshot: terraform graph -type=apply output showing the full execution graph with expand/close internal nodes, local and variable evaluation, and provider lifecycle nodes fanning out from the root node.

4. Locals and variables show up as their own nodes

Both get evaluated as distinct graph nodes, not inlined into whatever resource references them:

local.github_owner
local.github_repo_name
local.wildcard_sub
local.use_immutable_sub
var.github_owner_id
var.github_repo_id
var.github_branch

Worth remembering as the mental model: local.github_owner is a node computed from var.github_owner_id and whatever expression the locals block defines — Terraform tracks that as a real dependency edge, same as it would between two resources. If a local references a variable, there’s a local.x → var.y edge in the graph exactly like there’d be one between two resources.

5. Destroy inverts the direction of execution, not the arrows

terraform graph -type=plan-destroy

Screenshot: terraform graph -type=plan-destroy output, structurally similar to the apply graph but walked in reverse — dependents destroyed before their dependencies.

The graph itself looks structurally similar to the apply graph — same nodes, same edges, same “A → B means A depends on B” reading. What changes is which direction Terraform walks it: apply walks toward the arrowheads (dependencies first), destroy walks against them (dependents torn down first, dependencies last). CloudFront gets destroyed before the S3 bucket it depends on — makes sense, since you can’t safely tear down something another resource still points at.


More commands get added here as I actually use them — this isn’t meant to be a complete Terraform reference on day one, just the notes worth keeping.