Skip to content
Infrastructure as Code iac concepts 4 min read

The Dependency Graph

Terraform never executes your configuration top-to-bottom in file order. Instead, it parses every resource, data source, and module, works out how they relate to one another, and assembles a dependency graph — a directed acyclic graph (DAG) that encodes the exact order in which things must be created, updated, or destroyed. Understanding this graph is the key to predicting Terraform’s behavior, maximizing parallelism, and debugging “this resource was created before that one” surprises. OpenTofu uses the same graph model, so everything here applies equally to it.

Why a graph?

Cloud resources have ordering constraints. A subnet cannot exist before its VPC, and an EC2 instance cannot reference a security group that has not been created yet. Rather than asking you to declare ordering manually, Terraform derives it from the relationships between resources and builds a graph where each node is a resource and each edge means “must happen first.” Because the graph is acyclic, Terraform can walk it deterministically and run independent branches concurrently.

Implicit dependencies

The most common and idiomatic way to create a dependency is simply to reference one resource’s attribute from another. When Terraform sees an interpolated reference, it automatically adds an edge to the graph.

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

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

resource "aws_instance" "web" {
  ami           = "ami-0c7217cdde317cfec"
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.app.id   # implicit dependency on aws_subnet.app
}

Here Terraform learns that aws_subnet.app depends on aws_vpc.main, and aws_instance.web depends on aws_subnet.app. The VPC is created first, then the subnet, then the instance — with no depends_on anywhere. Implicit dependencies are always preferred because they are derived from real data flow and stay correct as your configuration evolves.

Explicit dependencies with depends_on

Sometimes a resource depends on another without referencing any of its attributes — for example, an application needs an IAM policy to be attached before it boots, even though the boot logic does not consume the policy’s ID. For these hidden, behavioral dependencies, use depends_on.

resource "aws_iam_role_policy" "s3_access" {
  name   = "s3-read"
  role   = aws_iam_role.app.id
  policy = data.aws_iam_policy_document.s3.json
}

resource "aws_instance" "app" {
  ami                  = "ami-0c7217cdde317cfec"
  instance_type        = "t3.micro"
  iam_instance_profile = aws_iam_instance_profile.app.name

  # The instance's user-data reads from S3 at boot, but that
  # dependency is invisible to Terraform — make it explicit.
  depends_on = [aws_iam_role_policy.s3_access]
}

Reach for depends_on only when an implicit reference is impossible. Overusing it serializes resources that could otherwise run in parallel and makes the graph harder to reason about.

Parallel creation of independent resources

Any two resources that are not connected by a path in the graph are independent, and Terraform creates them concurrently. By default it walks up to 10 nodes at once, controlled by the -parallelism=n flag.

resource "aws_s3_bucket" "logs"    { bucket = "devcraftly-logs-9f2a" }
resource "aws_s3_bucket" "backups" { bucket = "devcraftly-backups-9f2a" }
resource "aws_sns_topic" "alerts"  { name   = "alerts" }

None of these reference each other, so Terraform provisions all three simultaneously.

Output:

aws_sns_topic.alerts: Creating...
aws_s3_bucket.logs: Creating...
aws_s3_bucket.backups: Creating...
aws_s3_bucket.backups: Creation complete after 2s [id=devcraftly-backups-9f2a]
aws_sns_topic.alerts: Creation complete after 2s [id=arn:aws:sns:us-east-1:...:alerts]
aws_s3_bucket.logs: Creation complete after 3s [id=devcraftly-logs-9f2a]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Destroy ordering

For destroys, Terraform walks the graph in reverse. If a subnet depends on a VPC during creation, then the subnet must be destroyed before the VPC, because you cannot delete a VPC that still contains a subnet. Terraform handles this automatically by inverting every edge.

aws_instance.web: Destroying... [id=i-0abc123]
aws_instance.web: Destruction complete after 31s
aws_subnet.app: Destroying... [id=subnet-0def456]
aws_subnet.app: Destruction complete after 1s
aws_vpc.main: Destroying... [id=vpc-0ghi789]
aws_vpc.main: Destruction complete after 1s

This reversal is also why a depends_on edge protects you on the way out: the dependent resource is always torn down first.

Inspecting the graph with terraform graph

You can render the actual graph Terraform computes using terraform graph, which emits the DOT description language. Pipe it through Graphviz to produce an image.

terraform graph | dot -Tsvg > graph.svg
# Focus on just the apply (create/update) operations:
terraform graph -type=plan | dot -Tpng > plan.png

The -type flag lets you pick which graph to render. The most useful values:

-type valueGraph shown
planOperations for a normal plan/apply (default in modern versions)
plan-destroyOperations for a terraform destroy
applyThe fully expanded apply graph including provider nodes

Real graphs include extra nodes for providers, variables, and the special root node. Filter visual noise with terraform graph | grep -v "provider\[" before piping to dot, or just read the SVG zoomed in.

Best Practices

  • Prefer implicit dependencies via attribute references — they are self-documenting and survive refactors.
  • Reserve depends_on for genuine hidden ordering that no reference can express, and add a comment explaining why.
  • Keep modules loosely coupled: pass outputs as inputs so the graph crosses module boundaries cleanly rather than via blanket depends_on on whole modules.
  • Avoid creating accidental cycles; if terraform plan reports a cycle, run terraform graph to locate the loop and break it by removing a reference.
  • Tune -parallelism down when a provider’s API is rate-limited, and leave it at the default otherwise to keep applies fast.
  • Visualize the graph during reviews of large changes so reviewers can see what runs before what.
Last updated June 14, 2026
Was this helpful?