Quick answer: Terraform works out the order in which to create, update and destroy resources by building a dependency graph. Most dependencies are implicit β created automatically whenever one resource references another’s attribute, such as aws_vpc.main.id. When no attribute reference exists but ordering still matters, you add an explicit dependency with depends_on. Everything with no dependency between it runs in parallel.
This guide explains how the graph is built, how to read it, when depends_on is genuinely required and when it is a code smell, how destruction order works, and how to debug the classic “resource not found” errors that come from getting ordering wrong.
Why ordering matters and how the graph is built
A subnet cannot exist before its VPC. An EC2 instance cannot attach a security group that has not been created. In a shell script you would sequence these by hand and sprinkle sleep commands in between. Terraform computes the order for you β but only if you give it the information it needs.
During plan, Terraform parses every block and looks at every expression. Each time an expression references another object β a resource, data source, module output or variable β it draws an edge in a directed acyclic graph (DAG). It then performs a topological walk: nodes with no incoming edges start immediately, each node starts as soon as all of its dependencies finish, and up to 10 nodes run concurrently (adjustable with -parallelism).
You can see the graph yourself with terraform graph | dot -Tpng > graph.png (requires Graphviz). For small projects this is a surprisingly useful teaching tool.
Implicit dependencies: the default you should rely on
Whenever you use one resource’s attribute inside another, Terraform records the dependency automatically:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "app" {
vpc_id = aws_vpc.main.id # depends on the VPC
cidr_block = "10.0.1.0/24"
}
resource "aws_security_group" "web" {
name = "tkh-web"
vpc_id = aws_vpc.main.id # also depends on the VPC
}
resource "aws_vpc_security_group_ingress_rule" "http" {
security_group_id = aws_security_group.web.id # depends on the SG
from_port = 80
to_port = 80
ip_protocol = "tcp"
cidr_ipv4 = "0.0.0.0/0"
}
resource "aws_instance" "web" {
ami = "ami-0f58b397bc5c1f2e8"
instance_type = "t3.micro"
subnet_id = aws_subnet.app.id # depends on subnet
vpc_security_group_ids = [aws_security_group.web.id] # depends on SG
}
Terraform will create the VPC first, then the subnet and security group in parallel, then the ingress rule and the instance in parallel. You wrote no ordering logic at all, and the code documents what depends on what.
Note the modern aws_vpc_security_group_ingress_rule resource. Older tutorials use aws_security_group_rule or inline ingress blocks; the separate rule resource is now recommended because each rule has its own ID and can change without replacing the whole group.
Explicit dependencies with depends_on
Sometimes a dependency exists in the real world but not in your code, because resource A never consumes an attribute of resource B. The textbook case is IAM: a Lambda function needs its role’s policy attached before the first invocation, but the function only references the role ARN, not the policy attachment.
resource "aws_iam_role" "lambda" {
name = "tkh-lambda-role"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_lambda_function" "api" {
function_name = "tkh-api"
role = aws_iam_role.lambda.arn # implicit: role
runtime = "python3.12"
handler = "app.handler"
filename = "build/api.zip"
# Explicit: the attachment is not referenced anywhere above,
# but Lambda fails at runtime without it.
depends_on = [aws_iam_role_policy_attachment.lambda_logs]
}
depends_on accepts a list of resource or module addresses and forces Terraform to finish those completely before starting this resource. Other legitimate uses include waiting for an aws_internet_gateway before an instance needs outbound access, or for an aws_s3_bucket_policy before a CloudFront distribution reads from the bucket.
Implicit versus explicit: when to use which
| Implicit (attribute reference) | Explicit (depends_on) |
|
|---|---|---|
| How it is created | Automatically, whenever you reference an attribute | You list the addresses by hand |
| Readability | High β the reference shows why | Lower β needs a comment explaining why |
| Plan accuracy | Precise; only real data flow | Can make plans more conservative and “known after apply” |
| Use when | Always, if any attribute can be passed | A hidden side effect (IAM propagation, policies, API enablement) must finish first |
The rule of thumb: if you can express the relationship with a reference, do that. Reach for depends_on only when the dependency is real but invisible in the data.
Ordering during destroy and replacement
The graph is walked in reverse for terraform destroy: the instance goes before the subnet, the subnet before the VPC. This is why you almost never hit “DependencyViolation” errors when tearing down an environment that Terraform built.
Replacement is more subtle. By default Terraform destroys the old object and then creates the new one. For anything in a serving path, set create_before_destroy = true in a lifecycle block; Terraform then reverses the two steps and also propagates the setting to every dependency so the graph stays consistent. We look at these lifecycle controls in Creating and Managing Infrastructure Resources in Terraform.
Debugging ordering problems
- “InvalidGroup.NotFound” or “resource does not exist” on first apply β a missing dependency; Terraform tried to use something before it was ready. Check whether you hard-coded an ID instead of referencing the resource.
- Eventual-consistency errors (IAM, DNS, API enablement on GCP) β add
depends_onor, as a last resort, atime_sleepresource from thehashicorp/timeprovider. - “Cycle” error β two resources reference each other. Break the loop by moving one attribute to a separate resource, for example using
aws_vpc_security_group_ingress_ruleinstead of two groups that reference each other inline. - Everything shows “known after apply” β an over-broad
depends_onprevents Terraform from reading values at plan time. Replace it with a precise attribute reference, and see Using Variables and Expressions in Terraform for keeping plans readable.
Common dependency mistakes
- Hard-coding IDs copied from the console. Terraform cannot see a dependency in a string literal, so ordering is random and the ID breaks in every other account.
- Adding
depends_on“just to be safe”. It serialises resources that could run in parallel, slows applies, and hides the real relationships. - Using
-targetfor ordering.terraform apply -target=aws_vpc.mainis for emergency recovery, not for sequencing. It leaves state partially applied and skips dependency checks. - Forgetting that data sources have dependencies too. A
datablock that looks up a resource created in the same run mustdepends_onit, or it will run first and return nothing.
Frequently asked questions
Does Terraform create resources in the order they appear in the file?
No. File order and file names are irrelevant. Only the dependency graph determines ordering, and independent resources run in parallel.
Can I control how many resources run in parallel?
Yes, with terraform apply -parallelism=N. The default is 10. Lower it if a provider’s API rate-limits you; raise it cautiously for very large configurations.
Does depends_on work on data sources and modules?
Yes. Both accept depends_on. On a data source it delays the read until the listed resources exist; on a module it applies to every resource inside the module.
Key takeaways
- Terraform orders resources with a dependency graph, not by file position.
- Referencing an attribute creates an implicit dependency β use this whenever possible.
depends_onis for real but invisible dependencies such as IAM policy attachments.- Destroy walks the graph in reverse; avoid hard-coded IDs and
-targetas a sequencing tool.
Want to build production-grade AWS and Azure infrastructure where ordering, state and modules all work together? Our DevOps course covers Terraform end to end with live projects, mentor support and placement assistance. Prefer video? Follow along on our YouTube channel.

