Skip to content

How Terraform Actually Figures Out What to Build First

Published: at 05:45 PM

How Terraform Actually Figures Out What to Build First

You write a VPC, an ALB, and an ECS Fargate service in the same main.tf. You run terraform apply, and somehow the VPC always gets created first, the ALB second, and Fargate last. You never told Terraform to do that. So how does it know?

The short answer: it doesn’t read your file top to bottom. It builds a graph.

Terraform doesn’t execute files, it executes a graph

On every plan or apply, Terraform does roughly this:

  1. Parses all your .tf files and creates one node per resource.
  2. Looks at every attribute reference inside each resource block.
  3. Turns those references into edges between nodes.
  4. Assembles everything into a DAG — a Directed Acyclic Graph.
  5. Walks the graph, running anything with no unresolved dependencies, in parallel where possible. That’s it. The order of your resource blocks in the file is irrelevant. You could write Fargate first, ALB second, VPC last, and the outcome would be identical.

Where the edges come from

Take this:

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}
 
resource "aws_lb" "alb" {
  subnets = [aws_subnet.public.id]
  # references aws_vpc.main indirectly through the subnet
}
 
resource "aws_ecs_service" "fargate" {
  load_balancer {
    target_group_arn = aws_lb_target_group.tg.arn
  }
}

When Terraform parses aws_lb.alb, it sees aws_subnet.public.id used inside it. That’s not just interpolation — it’s a signal. Terraform records: this resource can’t be created until that one exists and has an ID. Same story for Fargate referencing the ALB’s target group ARN.

This is called an implicit dependency, and it’s how most ordering happens in practice. You never declare “build order,” you just reference outputs, and Terraform infers the graph from that.

Sometimes there’s no attribute to reference — like waiting on IAM eventual consistency — so you fall back to an explicit dependency:

resource "aws_ecs_service" "fargate" {
  depends_on = [aws_iam_role_policy.ecs_exec]
}

The graph itself

Resources become nodes. Dependencies become directed edges. Terraform requires this to be acyclic — if A depends on B and B depends on A, terraform plan fails immediately rather than trying to guess.

For the VPC → ALB → Fargate example, the graph looks like:

aws_vpc.main
   └── aws_lb.alb
          └── aws_ecs_service.fargate

A graph walker traverses this. Any node whose dependencies are already satisfied can start immediately — including in parallel with unrelated resources. That’s why Terraform can spin up your VPC and, say, an unrelated S3 bucket at the same time, but will always block Fargate until the ALB is done.

Two more things worth knowing:

See it yourself

You don’t have to take this on faith:

terraform graph | dot -Tsvg > graph.svg

This renders the actual dependency graph Terraform built for your configuration. Genuinely useful the first time you’re debugging a resource that’s applying in an order you didn’t expect — it usually means there’s a reference (or missing one) you didn’t notice.

Why this matters day to day

Once this clicks, a few Terraform behaviors stop being mysterious:

References