In Terraform, how is the resource dependency graph built from a configuration, and when is an explicit depends_on genuinely required?
answer
- order comes from references, not files
- attribute reference creates the edge
- only for hidden, unexpressed ordering
- IAM policy before the instance boots
- static list of addresses, no expressions
basics
~20 sTerraform 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 sTerraform 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 linesvariable "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
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.
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.
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.
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