TerraformVIII · Dependencies and the Resource GraphDependencies
Explicit Dependencies with depends_on
What you'll learn
- Use `depends_on` for dependencies that are not captured as references
- Place `depends_on` inside the resource block and validate the syntax
- Explain how `depends_on` interacts with `lifecycle.create_before_destroy`
- Recognise over-use of `depends_on` as a code smell
Prerequisites
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
Implicit dependencies cover most cases. When aws_instance.web
references aws_subnet.a.id, Terraform records the edge and serialises
the instance after the subnet. No operator intervention required.
Some dependencies are not captured as references. The classic example is an EC2 instance that assumes an IAM role. The instance block does not reference any attribute of the role. The instance still must wait for the role to be propagated by AWS IAM before the instance can successfully assume it. There is no attribute on the role that the instance block could reference; the propagation is a side effect.
For these cases, Terraform provides the depends_on meta-argument.
The case for depends_on
Three common shapes:
1. IAM propagation. An EC2 instance has an instance profile attached to an IAM role. The instance’s user-data script assumes the role to read a secret. The instance must wait for the role and its policy attachments to be propagated before the user-data runs. The instance block does not read any attribute of the role; the dependency is invisible to the expression walker.
2. DNS propagation. A Route 53 record references an ELB. The record is created immediately after the ELB. AWS DNS is eventually consistent; the record’s first lookup may fail. The record block references the ELB’s DNS name (so there is an implicit dependency), but the propagation delay is independent of the resource being “ready” in Terraform’s sense.
3. Cross-module side effects. A module produces a value that another module depends on without exposing it as an output. The dependency is real; the value is not addressable from the consumer.
resource "aws_iam_role" "app" {
name = "app-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "app_s3" {
role = aws_iam_role.app.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
resource "aws_iam_instance_profile" "app" {
name = "app-profile"
role = aws_iam_role.app.name
}
resource "aws_instance" "web" {
ami = "ami-0e1bed4f"
instance_type = "t3.medium"
iam_instance_profile = aws_iam_instance_profile.app.name
depends_on = [
aws_iam_role_policy_attachment.app_s3,
]
}
The aws_instance.web block does not reference the policy attachment.
The user-data script assumes the role and reads an S3 object. Without
depends_on, the apply graph would allow the instance to be created in
the same wave as the policy attachment. The user-data would race
against IAM propagation and fail intermittently. With depends_on,
the instance waits for the attachment.
Placement and syntax
depends_on is a meta-argument. It lives inside the resource block,
alongside count, for_each, provider, and lifecycle. It is not
inside the lifecycle block.
Correct:
resource "aws_instance" "web" {
ami = "ami-0e1bed4f"
instance_type = "t3.medium"
depends_on = [
aws_iam_role_policy_attachment.app_s3,
]
}
Incorrect (will fail at plan time):
resource "aws_instance" "web" {
ami = "ami-0e1bed4f"
instance_type = "t3.medium"
lifecycle {
depends_on = [aws_iam_role_policy_attachment.app_s3]
}
}
The argument is a list of resource addresses. Each address is a full
Terraform address: aws_iam_role_policy_attachment.app_s3. References
to count and for_each instances use the bracket syntax:
aws_iam_role_policy_attachment.app_s3[0]. Module addresses use the
double-dot syntax: module.network.aws_subnet.a.
resource "aws_instance" "web" {
ami = "ami-0e1bed4f"
instance_type = "t3.medium"
depends_on = [
aws_iam_role_policy_attachment.app_s3,
aws_iam_instance_profile.app,
module.network.aws_route_table.private,
]
}
How depends_on interacts with lifecycle
depends_on and lifecycle.create_before_destroy are independent
controls:
depends_onadds an edge to the graph. The edge affects both create and destroy order: the dependent resource cannot be created until the dependency is created; the dependent resource cannot be destroyed until the dependency is destroyed.lifecycle.create_before_destroycontrols the order during replacement: the new resource is created before the old is destroyed. It does not change the graph; it changes the apply walker.
A resource with both:
resource "aws_instance" "web" {
ami = "ami-0e1bed4f"
instance_type = "t3.medium"
depends_on = [aws_iam_role_policy_attachment.app_s3]
lifecycle {
create_before_destroy = true
}
}
When the instance is replaced, Terraform creates the new instance, waits
for the new instance to be ready, then destroys the old instance. The
depends_on edge still applies: the new instance cannot be created
until the policy attachment is also ready (the attachment is not
recreated, but the edge is honoured).
The two controls can conflict in narrow cases. If the dependency is a
resource that is also being replaced, create_before_destroy on the
dependency can change the order Terraform uses to satisfy the edge.
The result is still correct; it is just not what a casual reading of
the configuration might suggest.
When NOT to use depends_on
depends_on is over-used. The signal that depends_on is wrong is
almost always “I am adding this because Terraform did not infer the
edge from a reference.” The right fix is usually to add the missing
reference, not to add depends_on.
Three patterns that look like depends_on but are actually bugs:
1. Forgetting to reference the output.
# Wrong:
resource "aws_instance" "web" {
ami = "ami-0e1bed4f"
instance_type = "t3.medium"
subnet_id = aws_subnet.a.id
# ... no reference to the role
depends_on = [aws_iam_role.app]
}
If the instance does not reference the role, the user-data script is
the only consumer. If the role is referenced via the instance profile
(iam_instance_profile = aws_iam_instance_profile.app.name), the
graph already has the edge. Adding depends_on = [aws_iam_role.app]
adds a redundant edge.
2. Serialising unrelated resources for “safety.”
# Code smell:
resource "aws_s3_bucket" "logs" {
bucket = "myapp-logs"
depends_on = [aws_s3_bucket.app_data]
}
The two buckets are independent. The depends_on does not buy
correctness; it serialises the apply for no reason.
3. Using depends_on to control module load order.
# Wrong:
module "network" {
source = "./modules/network"
}
resource "aws_instance" "web" {
# ...
depends_on = [module.network]
}
module blocks are evaluated when the configuration is parsed; the
depends_on = [module.network] is invalid syntax for a module. The
correct way to express a dependency on a module is to reference one of
its outputs.
How depends_on interacts with -target
When you run terraform apply -target=aws_instance.web, Terraform
narrows the plan to the targeted resource and its dependencies. If a
depends_on edge points to a resource that is not targeted, Terraform
checks whether the resource is already in state. If it is, the apply
proceeds. If it is not, the apply fails.
terraform apply -target=aws_instance.web
If aws_iam_role.app is not in state (because it was never applied or
was removed), the apply fails:
Error: Cycle: aws_instance.web (depends_on) includes aws_iam_role.app,
which is not in the plan.
The fix is to target the dependency explicitly:
terraform apply \
-target=aws_iam_role.app \
-target=aws_iam_instance_profile.app \
-target=aws_iam_role_policy_attachment.app_s3 \
-target=aws_instance.web
Or to drop -target and apply the full plan.
Failure modes
- Reference would have been enough. A
depends_onis added instead of an attribute reference. The apply is serialised unnecessarily. The fix is to add the reference. - Dependency in another state file. A
depends_onto a resource in a different Terraform state is invalid. Terraform does not cross state boundaries. Useterraform_remote_stateto consume the resource. - Non-existent address. A
depends_onto a misspelled address producesError: Reference to undeclared resourceat plan time. - Module address without double dots.
depends_on = [module.network]is a parse error. Module dependencies are expressed through output references, notdepends_on. depends_oninsidelifecycle. Parse error at plan time. Move the block.depends_onsurvives a state move that breaks the address.terraform state mvpreserves the dependency if the address still matches; otherwise thedepends_onbecomes invalid and the plan fails.
How to validate
terraform validate
terraform plan -out=tfplan
terraform graph -type=plan | grep -B1 "aws_instance.web"
The grep shows the immediate edges for the resource. Each edge should
be either a real reference or an intentional depends_on. Reviewers
should reject PRs that add depends_on without a comment explaining
why a reference is not sufficient.
Performance implications
Every depends_on is a serialisation point. A configuration with N
independent resources and D depends_on edges runs in at least
ceil(N / -parallelism) + D waves. For a 100-resource configuration
with 5 incidental depends_on blocks, the apply is at least 5 waves
slower than it could be.
The production rule: every depends_on is reviewed; every depends_on
is commented; every depends_on is removed when the side effect no
longer applies.
What to do in production
- Use
depends_ononly when the dependency is on a side effect that is not surfaced as an attribute (IAM propagation, DNS, secrets rotation, scheduled actions). - Do not use
depends_onto compensate for a missing reference. Add the reference. - Document every
depends_onin the module’s README. Reviewers must be able to verify the necessity. - Periodically run
terraform graphand removedepends_onblocks that are no longer required.
Verification
- Run
terraform validateand confirm the syntax is correct. - Run
terraform plan -out=tfplanand confirm the apply order honours thedepends_onedges. - Run
terraform graph -type=plan | grepfor each resource that has adepends_onand confirm the edges are intentional. - For each
depends_on, write a one-line comment explaining why a reference is not sufficient.
Knowledge check · 7 questions
Q1. When should `depends_on` be used?
Q2. `depends_on` accepts a list of resource addresses.
Q3. Where does the `depends_on` block belong in HCL?
Q4. Which of the following are valid uses of `depends_on`? (Select all that apply.)
Q5. An EC2 instance uses an IAM role. The role is created in the same apply. The user-data script fails because the role's policy attachment is not yet propagated. The instance's `depends_on` lists the role but not the policy attachment. What is the cleanest fix?
Q6. What is the relationship between `depends_on` and `lifecycle.create_before_destroy`?
Q7. Listing a non-existent resource in `depends_on` causes Terraform to error at plan time.
Passing score: 75%. Answers are checked in this browser.