Terraform automatically determines resource ordering from references in your code. You rarely need depends_on — but when you do, it’s critical. This guide explains when implicit dependencies work, when they don’t, and when depends_on is the right tool.
Implicit Dependencies (The Default)
Terraform builds a dependency graph from resource references:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # ← Reference creates implicit dependency
cidr_block = "10.0.1.0/24"
}
resource "aws_instance" "web" {
subnet_id = aws_subnet.public.id # ← Depends on subnet
ami = "ami-abc123"
instance_type = "t3.micro"
}
Terraform knows: VPC → Subnet → Instance. No depends_on needed.
terraform graph:
aws_vpc.main
└── aws_subnet.public
└── aws_instance.web
When You Need depends_on
1. IAM Policy Before Resource That Uses It
The most common case. IAM policies take seconds to propagate across AWS:
resource "aws_iam_role" "lambda" {
name = "lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "lambda_s3" {
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
resource "aws_lambda_function" "main" {
function_name = "processor"
role = aws_iam_role.lambda.arn
runtime = "python3.12"
handler = "main.handler"
filename = "lambda.zip"
# Lambda creation might fail if IAM policy hasn't propagated
depends_on = [aws_iam_role_policy_attachment.lambda_s3]
}
Without depends_on, Terraform sees the role ARN reference (implicit dep on the role) but not on the policy attachment. The Lambda might be created before the policy attaches, causing permission errors on first invocation.
2. S3 Bucket Policy Before Enabling Logging
resource "aws_s3_bucket" "logs" {
bucket = "my-access-logs"
}
resource "aws_s3_bucket_policy" "logs" {
bucket = aws_s3_bucket.logs.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "logging.s3.amazonaws.com" }
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.logs.arn}/*"
}]
})
}
resource "aws_s3_bucket_logging" "main" {
bucket = aws_s3_bucket.main.id
target_bucket = aws_s3_bucket.logs.id
target_prefix = "access-logs/"
# Logging fails if bucket policy isn't in place
depends_on = [aws_s3_bucket_policy.logs]
}
3. VPC Endpoint Before Private Subnet Resources
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.s3"
}
resource "aws_instance" "private" {
subnet_id = aws_subnet.private.id
ami = "ami-abc123"
instance_type = "t3.micro"
# Instance needs S3 endpoint to reach S3 from private subnet
depends_on = [aws_vpc_endpoint.s3]
user_data = <<-EOF
#!/bin/bash
aws s3 cp s3://my-bucket/config.tar.gz /tmp/
EOF
}
4. Module-Level Dependencies
module "networking" {
source = "./modules/networking"
}
module "database" {
source = "./modules/database"
vpc_id = module.networking.vpc_id # Implicit dep
subnet_ids = module.networking.subnet_ids # Implicit dep
}
module "app" {
source = "./modules/app"
vpc_id = module.networking.vpc_id
db_endpoint = module.database.endpoint
# Explicit: app needs database security group rules to be fully applied
depends_on = [module.database]
}
5. Provisioners That Need External Resources
resource "aws_security_group_rule" "ssh" {
type = "ingress"
security_group_id = aws_security_group.main.id
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
resource "aws_instance" "web" {
ami = "ami-abc123"
instance_type = "t3.micro"
# SSH provisioner needs the security group rule to be in place
depends_on = [aws_security_group_rule.ssh]
provisioner "remote-exec" {
inline = ["echo 'Connected!'"]
}
}
When NOT to Use depends_on
Don’t Use It for Reference-Based Dependencies
# ❌ Unnecessary — the vpc_id reference already creates the dependency
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
depends_on = [aws_vpc.main] # REDUNDANT!
}
# ✅ The reference is enough
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
Don’t Use It to Force Sequential Execution
# ❌ Don't chain unrelated resources
resource "aws_s3_bucket" "a" { bucket = "bucket-a" }
resource "aws_s3_bucket" "b" {
bucket = "bucket-b"
depends_on = [aws_s3_bucket.a] # WHY? These are independent
}
# ✅ Let Terraform create them in parallel
resource "aws_s3_bucket" "a" { bucket = "bucket-a" }
resource "aws_s3_bucket" "b" { bucket = "bucket-b" }
depends_on Behavior
When you add depends_on, Terraform:
- Waits for the dependency to be fully created/updated before starting the resource
- Destroys the resource before destroying the dependency (reverse order)
- Replaces the resource if the dependency is replaced
depends_on = [
aws_iam_role_policy_attachment.lambda_s3,
aws_vpc_endpoint.s3,
]
# Multiple dependencies are supported
depends_on in Data Sources
data "aws_ami" "latest" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["my-app-*"]
}
# Wait for Packer build to complete before querying
depends_on = [null_resource.packer_build]
}
Hands-On Courses
- Terraform for Beginners on CopyPasteLearn
- Terraform By Example — practical code examples
Conclusion
Terraform’s implicit dependencies (from resource references) handle 90% of ordering. Use depends_on only when there’s a hidden dependency Terraform can’t see: IAM propagation delays, bucket policies needed before logging, VPC endpoints before private resources, or module-level orchestration. If you find yourself adding depends_on everywhere, you’re probably doing it wrong — check if a resource reference would create the dependency automatically.