skip to content

Dependencies and the Resource Graph

Terraform builds a dependency graph from attribute references and walks it in parallel. Knowing when an implicit reference is enough and when depends_on is genuinely required is a classic senior-level probe.

part ofTerraformoverview, primer and where to startread it →
on this pageshow

questions

5

In Terraform, how is the resource dependency graph built from a configuration, and when is an explicit depends_on genuinely required?

level: middleimportance: must knowfreq 78%

answer

  1. order comes from references, not files
  2. attribute reference creates the edge
  3. only for hidden, unexpressed ordering
  4. IAM policy before the instance boots
  5. static list of addresses, no expressions

basics

~20 s

Terraform infers dependency edges from attribute references — writing aws_vpc.main.id inside another resource creates the edge automatically. depends_on is only for hidden ordering that exists at the provider but is never expressed as a reference in the configuration.

solid answer

~50 s

Terraform does not run your files top to bottom. It builds a directed acyclic graph of every resource, data source, provider and module, then walks it. Almost all edges are **implicit**: any expression that references another object's address — `subnet_id = aws_subnet.app.id` — tells Terraform that the subnet must be created first, because the value literally flows from one to the other. That is the edge you want, because it is precise, self-documenting, and lets Terraform propagate unknown values correctly. `depends_on` exists for the residue: an ordering that is real in the cloud but invisible in the code. The canonical case is an EC2 instance whose startup script calls S3 — it references the instance profile but never the `aws_iam_role_policy`, so without `depends_on = [aws_iam_role_policy.app]` the instance can boot before its permissions exist. If you already reference the object, adding `depends_on` for it is redundant noise.

code

hcl · 37 lines
hcl
variable "ami_id" {
  type = string
}

resource "aws_iam_role" "app" {
  name = "app"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Action    = "sts:AssumeRole"
      Principal = { Service = "ec2.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy" "app" {
  role = aws_iam_role.app.id
  policy = jsonencode({
    Version   = "2012-10-17"
    Statement = [{ Effect = "Allow", Action = "s3:GetObject", Resource = "*" }]
  })
}

resource "aws_iam_instance_profile" "app" {
  name = "app"
  role = aws_iam_role.app.name
}

resource "aws_instance" "app" {
  ami                  = var.ami_id
  instance_type        = "t3.micro"
  iam_instance_profile = aws_iam_instance_profile.app.name

  # Nothing above references the policy, but the boot script needs it.
  depends_on = [aws_iam_role_policy.app]
}

go deeper

for a junior

Know that Terraform works out ordering itself from the references you write, and that the order of blocks in a file means nothing. Be able to point at a line like vpc_id = aws_vpc.main.id and say that is the dependency.

for a middle

Explain that Terraform builds a DAG before acting, that references become edges, and that depends_on adds an edge for ordering the configuration never expresses. Give the IAM-policy-before-instance example and state that depends_on must be a static list of addresses.

for a senior

Show judgment about which dependencies to declare. Talk about hardcoded IDs silently deleting edges, about redundant depends_on next to an existing reference, and about diagnosing a first-run-only failure as a missing edge rather than a flaky provider.

for a principal

Own the convention: implicit references are the default, explicit edges require a comment explaining the invisible constraint, and every added edge narrows the graph and lengthens the critical path. Decide where that ordering belongs at all — some of it is better solved by module boundaries or by the application retrying.

## Terraform does not execute your files in order HCL files have no execution order. Terraform parses every `.tf` file in the root module (and every module it calls), then constructs a **directed acyclic graph** — a DAG — whose nodes are the things it can act on (managed resources, data sources, provider configurations, module boundaries, variables, outputs, local values) and whose edges mean "this must happen after that". Only once the graph exists does Terraform decide what to create, read, update or destroy, and in what order. The file a resource happens to live in, and its position within that file, are irrelevant. This matters practically: you can split resources across files however you like, and rearranging blocks never changes behaviour. What changes behaviour is the *references* between them. ## Implicit dependencies: references are the edges Any expression that names another object's address creates an edge from the referencing node to the referenced one: ```hcl resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" } resource "aws_subnet" "app" { vpc_id = aws_vpc.main.id # edge: subnet depends on vpc cidr_block = "10.0.1.0/24" } ``` Terraform now knows the VPC must be created before the subnet, and — just as importantly — *why*: the subnet needs one specific attribute of the VPC. During `terraform plan`, if `aws_vpc.main` does not exist yet, its `id` is not knowable, so the plan renders the subnet's `vpc_id` as `(known after apply)`. That unknown propagates only along the values that actually flow, so the rest of the subnet's plan stays concrete. This is the kind of dependency you should aim for. It costs nothing to maintain, it survives refactoring, and a reader can see the relationship on the line where it matters. ## Explicit dependencies: depends_on Some ordering exists in the provider's world but is never expressed as a value. The classic example is an instance whose software needs an IAM policy attached to its role before it boots. The instance references `aws_iam_instance_profile.app.name`, and the profile references the role — but nothing references the `aws_iam_role_policy`. Terraform is therefore free to create the instance in parallel with the policy, and the application fails on first boot with an access-denied error that disappears when you re-run. `depends_on` adds that missing edge by hand: ```hcl resource "aws_instance" "app" { ami = var.ami_id instance_type = "t3.micro" iam_instance_profile = aws_iam_instance_profile.app.name depends_on = [aws_iam_role_policy.app] } ``` The argument is a **static list of addresses** — `aws_iam_role_policy.app`, `data.aws_ami.base`, `module.network` — with no attribute suffix and no expressions. You cannot compute it from a variable or make it conditional; it must be resolvable before Terraform builds the graph, because the graph is what everything else depends on. Other genuine uses: a resource that only works once a service-linked role or an API activation exists; an application-level ordering constraint the provider's schema does not model; a race where two resources touch the same underlying object (two records with the same name, a bucket policy and an object written by something else). ## Where candidates go wrong The two failure modes are opposite and both common. The first is **omitting** an edge by hardcoding a value — pasting `"subnet-0a1b2c3d"` instead of referencing `aws_subnet.app.id` removes the edge entirely, and the ordering that used to work becomes a coin flip, most visibly on destroy. The second is **decorating** every block with `depends_on` "to be safe", which adds coarse edges Terraform then has to plan conservatively around, and hides which relationships are real. A good rule: reach for `depends_on` only after you have asked "is there an attribute I could reference instead?" and the honest answer is no. When you do add one, leave a comment saying what the hidden dependency is — the whole problem with an explicit edge is that the code no longer explains itself. ## The walk Once built, Terraform walks the DAG, starting every node whose predecessors have completed and running up to ten of them concurrently by default. Graph shape therefore sets the floor on wall-clock time: a chain of ten mutually dependent resources runs serially no matter how much concurrency is available, while a hundred independent resources saturate it immediately. Every unnecessary edge you add pushes the configuration toward the first shape.

  • If a resource already references another resource's attribute, is there ever a reason to also list it in depends_on?
    No. The reference has already created the edge, and `depends_on` cannot make it stronger — the two produce the same ordering. Duplicating it only adds noise and misleads the next reader into thinking there is a second, hidden reason for the ordering. Remove it.
  • Can you make a depends_on conditional, for example only when a feature flag variable is true?
    No. `depends_on` takes a static list of addresses and is evaluated while the graph is being constructed, before variables and expressions are resolved, so it cannot contain a conditional or a computed value. If you need conditional ordering, express it as a real attribute reference — often via a value that is only produced when the flag is on.
  • What happens to the plan if you hardcode an ID string instead of referencing the resource that produces it?
    You lose the graph edge. Terraform no longer knows the two objects are related, so it may create or destroy them in the wrong order and the provider API rejects the call. You also lose refactor safety: replacing the upstream resource leaves the hardcoded string pointing at something that no longer exists, and Terraform sees no change to plan.

saying these in an interview costs you the question

  • Terraform runs resources in the order written in the file
  • Add depends_on everywhere to be safe
  • depends_on can take a variable or conditional expression
  • Referencing an attribute is not enough, you still need depends_on
  • Splitting resources into separate files changes execution order

context

open as a page

When Terraform destroys a set of resources, what order does it use, and how does it work that order out?

level: middleimportance: should knowfreq 45%

basics

~20 s

Terraform destroys in reverse dependency order, walking the same graph with its edges inverted: nothing is deleted until everything that depends on it is gone. Subnets go before the VPC, because the subnet referenced the VPC's id.

open as a page

A terraform apply fails with an error beginning "Error: Cycle:" that names two aws_security_group resources. What has gone wrong, and how do you break it?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Two objects reference each other, so Terraform's dependency graph is no longer acyclic and no valid order exists. Break the loop by moving one direction of the relationship into a separate resource that both groups depend on, rather than referencing each other inline.

open as a page

Terraform's documentation calls depends_on a last resort. What does adding one actually cost you compared with an ordinary attribute reference?

level: seniorimportance: should knowfreq 42%

basics

~20 s

An explicit edge is whole-object and untyped: Terraform cannot see which value matters, so it must plan conservatively, marking more attributes as "(known after apply)". It also serialises work that could have run concurrently and hides the real reason for the ordering.

open as a page

What does the `terraform graph` command produce, and how do you actually make use of its output?

level: juniorimportance: nice to knowfreq 26%

basics

~20 s

terraform graph prints Terraform's internal dependency graph to stdout in Graphviz DOT format. You pipe it into the dot tool to render a picture, typically to confirm why an unexpected edge exists or, with -draw-cycles, to see a loop.

open as a page