Resource Ordering
Terraform does not create, update, or destroy resources in the order you happen to write them in your configuration. Instead it derives a correct order automatically from the relationships between resources. Most of that ordering is inferred for you simply by referencing one resource’s attributes inside another, and you only reach for an explicit depends_on when a real dependency exists that Terraform cannot see. Understanding how this works is the difference between configurations that “just apply” and ones that fail with race conditions or unresolvable cycles.
How Terraform decides what to do first
Before doing anything, Terraform builds a dependency graph of every resource, data source, provider, and output. It then walks that graph in topological order: a node is only acted on once everything it depends on has finished. Independent branches of the graph are processed in parallel (ten concurrent operations by default, tunable with -parallelism=N).
The key insight is that ordering is a property of your references, not of file layout or block position. You can define a security group below the instance that uses it, in a different file entirely, and Terraform will still create the security group first.
Implicit dependencies via references
The primary way to express order is the most natural one: when resource B reads an attribute of resource A, Terraform records an edge “B depends on A.” This is called an implicit dependency, and it is by far the most common form.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "app" {
# Referencing aws_vpc.main.id creates the dependency edge.
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
subnet_id = aws_subnet.app.id
}
Here Terraform learns the order aws_vpc.main → aws_subnet.app → aws_instance.web purely from the .id references. Note that aws_vpc.main.id is not even known until the VPC is created, so Terraform must wait — the value is a placeholder during planning and gets resolved during apply. On destroy, the same graph is walked in reverse: the instance is torn down first, then the subnet, then the VPC.
Prefer implicit dependencies wherever possible. They are self-documenting and stay correct automatically as your configuration evolves. Reach for
depends_ononly when there is genuinely no attribute to reference.
Explicit dependencies with depends_on
Sometimes resource B depends on resource A but never references any of its attributes. The dependency is real at the cloud-provider level but invisible in HCL — for example, an application needs an IAM policy to be attached before it can use a role, even though the application config doesn’t read the attachment’s outputs. For these hidden dependencies, declare the edge explicitly.
resource "aws_iam_role" "app" {
name = "app-role"
assume_role_policy = data.aws_iam_policy_document.assume.json
}
resource "aws_iam_role_policy" "s3_access" {
role = aws_iam_role.app.id
policy = data.aws_iam_policy_document.s3.json
}
resource "aws_instance" "worker" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.small"
iam_instance_profile = aws_iam_instance_profile.app.name
# The instance never references the policy, but it must exist
# and be attached before the instance boots and runs its code.
depends_on = [aws_iam_role_policy.s3_access]
}
depends_on takes a list of references to whole resources or modules (no attributes). Use it sparingly: overusing it serializes work that could run in parallel and makes the configuration brittle.
Reference vs. depends_on at a glance
| Aspect | Implicit (reference) | Explicit (depends_on) |
|---|---|---|
| How it’s created | Reading a.b.attr in another resource | Listing the resource in depends_on |
| Best for | Normal data flow between resources | Hidden side-effect dependencies |
| Self-documenting | Yes — the value usage shows why | No — intent must be inferred or commented |
| Maintenance risk | Low — updates with your references | Higher — easy to leave stale |
| Affects parallelism | Only where data genuinely flows | Can over-serialize if misused |
Seeing the order Terraform chose
The plan output reflects the derived order, and you can inspect the raw graph directly. Both commands work identically in OpenTofu (tofu graph, tofu plan).
terraform graph | dot -Tsvg > graph.svg
terraform plan
Output:
Terraform will perform the following actions:
# aws_vpc.main will be created
+ resource "aws_vpc" "main" {
+ cidr_block = "10.0.0.0/16"
+ id = (known after apply)
}
# aws_subnet.app will be created
+ resource "aws_subnet" "app" {
+ cidr_block = "10.0.1.0/24"
+ id = (known after apply)
+ vpc_id = (known after apply)
}
Plan: 3 to add, 0 to change, 0 to destroy.
The (known after apply) placeholders are the visible evidence of an ordering constraint: vpc_id cannot be filled in until aws_vpc.main exists, so the subnet is guaranteed to be created afterward.
Cycles and how to avoid them
Because ordering comes from edges, two resources that reference each other create a cycle, which Terraform cannot resolve.
Output:
Error: Cycle: aws_security_group.a, aws_security_group.b
Break cycles by splitting the mutual reference into a separate resource — for security groups, use standalone aws_security_group_rule (or aws_vpc_security_group_ingress_rule) resources instead of inline rules that reference each other.
Best Practices
- Express ordering by referencing attributes, not by reordering blocks or files — block position is irrelevant.
- Reserve
depends_onfor genuine hidden dependencies, and add a comment explaining why it’s needed. - Keep
depends_onlists minimal; every extra entry can reduce parallelism and add maintenance risk. - Run
terraform graphwhen an apply order surprises you, rather than guessing. - Split mutually-referencing resources (like paired security group rules) into standalone resources to avoid cycles.
- Trust reverse-order destroys: design with the knowledge that dependents are removed before their dependencies.