Skip to content
Infrastructure as Code iac resources 4 min read

Resource Dependencies

Real infrastructure is rarely a flat list of independent objects: a subnet lives inside a VPC, a route table attaches to that subnet, and an instance launches into it. Terraform models these relationships as a directed graph, and the most common way edges get added to that graph is implicitly — simply by referencing one resource’s attribute from another. Understanding how these implicit dependencies form is the key to predictable plans, correct create/destroy ordering, and avoiding the dreaded dependency cycle. This applies identically to OpenTofu, which uses the same graph engine and HCL2 syntax.

How references create dependencies

Whenever you interpolate an attribute of one resource into the configuration of another, Terraform records an edge between them. You do not declare the relationship — it is inferred from the expression. The referenced resource must be created (and have its attributes known) before the referencing resource can be planned and applied.

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

  tags = {
    Name = "main"
  }
}

resource "aws_subnet" "app" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "us-east-1a"

  tags = {
    Name = "app"
  }
}

Because aws_subnet.app reads aws_vpc.main.id, Terraform knows the VPC must exist first. The id is a computed attribute — its value is only known after the VPC is created — so Terraform schedules the VPC create, then feeds the resulting id into the subnet create. No depends_on is needed; the reference is the dependency.

Prefer implicit dependencies over depends_on. References are precise (Terraform knows exactly which attribute flows where) and self-documenting. Reach for depends_on only when a real ordering requirement exists that Terraform cannot see from the configuration.

The dependency graph

Terraform walks your configuration, builds a graph of every resource and data source, and inserts edges for each reference. It then performs a topological sort to determine a valid execution order, parallelizing nodes that have no relationship between them (up to -parallelism, default 10).

You can inspect the graph directly:

terraform graph | dot -Tsvg > graph.svg

For the configuration above, an apply produces ordered, dependency-aware output.

Output:

aws_vpc.main: Creating...
aws_vpc.main: Creation complete after 2s [id=vpc-0a1b2c3d4e5f60718]
aws_subnet.app: Creating...
aws_subnet.app: Creation complete after 1s [id=subnet-09f8e7d6c5b4a3210]

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

On destroy, Terraform walks the graph in reverse: the subnet is destroyed before the VPC, because you cannot delete a VPC that still contains a subnet.

Implicit vs. explicit dependencies

AspectImplicit (reference)Explicit (depends_on)
How it’s declaredvpc_id = aws_vpc.main.iddepends_on = [aws_vpc.main]
GranularityPer-attribute, preciseWhole-resource, coarse
Self-documentingYes — value flows visiblyNo — intent is implicit
When to useAlmost alwaysHidden ordering (e.g. IAM policy must exist before a service uses it)
RiskLowCan mask design issues; overuse hurts parallelism

A larger worked example

Dependencies chain transitively. Here an internet gateway, a route table, and an association all hang off the VPC and subnet, forming a small graph.

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.app.id
  route_table_id = aws_route_table.public.id
}

Terraform derives the order from the references alone: the VPC and subnet first, then the gateway, then the route table (which needs the gateway id), and finally the association (which needs both the subnet and route table). The association depends on five resources transitively without a single depends_on.

Avoiding cycles

A cycle occurs when resource A references B and B (directly or through a chain) references A. The graph then has no valid topological order, and Terraform refuses to proceed.

Output:


│ Error: Cycle: aws_security_group.app, aws_security_group.db

This commonly happens when two security groups reference each other in their inline rules. The fix is to break the inline rule out into a standalone resource so the groups can be created first and wired together afterward:

resource "aws_security_group" "app" {
  name   = "app"
  vpc_id = aws_vpc.main.id
}

resource "aws_security_group" "db" {
  name   = "db"
  vpc_id = aws_vpc.main.id
}

resource "aws_security_group_rule" "app_to_db" {
  type                     = "egress"
  from_port                = 5432
  to_port                  = 5432
  protocol                 = "tcp"
  security_group_id        = aws_security_group.app.id
  source_security_group_id = aws_security_group.db.id
}

Now both groups are created independently, and the rule — created last — references both. The cycle is gone because the dependency only flows one direction at each node.

If you hit a cycle, run terraform graph | dot -Tsvg > graph.svg and trace the loop visually. The cause is almost always two resources that each reference an attribute of the other.

Best practices

  • Let references do the work — model ordering through attribute interpolation rather than depends_on wherever a value actually flows between resources.
  • Reference computed attributes (like id or arn) instead of hardcoding values, so Terraform sees the true dependency and recreates downstream resources when an upstream one changes.
  • Reserve depends_on for genuinely hidden ordering, such as side effects an API performs that aren’t visible in any returned attribute.
  • Break mutual references (especially between security groups) into standalone rule resources to keep the graph acyclic.
  • Visualize the graph with terraform graph when ordering surprises you or a cycle appears.
  • Keep dependency chains shallow; deep transitive chains serialize your apply and slow it down by defeating parallelism.
Last updated June 14, 2026
Was this helpful?