depends_on in Terraform
Terraform automatically infers resource dependencies by analyzing attribute references between resources. depends_on is a meta-argument that adds explicit ordering constraints when Terraform can’t infer the dependency from configuration — typically when a resource has a hidden or indirect dependency that isn’t visible in resource attributes.
Implicit vs Explicit Dependencies
Terraform resolves most dependencies automatically:
# Implicit dependency — Terraform sees that aws_instance references aws_security_groupresource "aws_security_group" "web" { name = "web-sg" vpc_id = aws_vpc.main.id}
resource "aws_instance" "web" { ami = data.aws_ami.ubuntu.id instance_type = "t3.micro" vpc_security_group_ids = [aws_security_group.web.id] # Creates an implicit dependency # Terraform knows: create aws_security_group.web before aws_instance.web}depends_on is needed only when Terraform can’t see the dependency:
# The IAM policy attachment affects what the instance can do,# but the instance resource doesn't reference the attachment directly.# Without depends_on, the instance might start before the role is fully attached.resource "aws_iam_role_policy_attachment" "app" { role = aws_iam_role.app.name policy_arn = aws_iam_policy.app.arn}
resource "aws_instance" "web" { ami = data.aws_ami.ubuntu.id instance_type = "t3.micro" iam_instance_profile = aws_iam_instance_profile.app.name
# Explicit dependency — the instance's startup script needs the IAM permissions depends_on = [aws_iam_role_policy_attachment.app]}Common depends_on Use Cases
IAM Permission Propagation
IAM changes in AWS propagate asynchronously. Resources that depend on IAM permissions might start before permissions are available:
resource "aws_iam_role" "lambda" { name = "lambda-execution-role" assume_role_policy = jsonencode({ Statement = [{ Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } Action = "sts:AssumeRole" }] })}
resource "aws_iam_role_policy_attachment" "lambda_vpc" { role = aws_iam_role.lambda.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"}
resource "aws_lambda_function" "api" { filename = "lambda.zip" function_name = "api-handler" role = aws_iam_role.lambda.arn
# Wait for policy attachment — Lambda creation can fail if the role # doesn't have VPC permissions yet when it's being created depends_on = [aws_iam_role_policy_attachment.lambda_vpc]}Database Initialization Before Application
resource "aws_db_instance" "main" { identifier = "app-db" engine = "postgres" instance_class = "db.t3.micro"}
resource "null_resource" "run_migrations" { provisioner "local-exec" { command = "psql ${aws_db_instance.main.endpoint} < schema.sql" }}
resource "aws_ecs_service" "app" { name = "app" task_definition = aws_ecs_task_definition.app.arn desired_count = 2
# Don't start the app until migrations have run depends_on = [null_resource.run_migrations]}Security Group Before VPC Endpoint
resource "aws_security_group" "vpc_endpoint" { name = "vpc-endpoint-sg" vpc_id = aws_vpc.main.id
ingress { from_port = 443 to_port = 443 protocol = "tcp" security_groups = [aws_security_group.app.id] }}
resource "aws_vpc_endpoint" "s3" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.us-east-1.s3" vpc_endpoint_type = "Interface" subnet_ids = aws_subnet.private[*].id security_group_ids = [aws_security_group.vpc_endpoint.id]
depends_on = [aws_security_group.vpc_endpoint]}Module-Level depends_on
depends_on on a module block means: don’t start creating any resources in this module until all dependencies are ready.
module "networking" { source = "./modules/networking" cidr_block = "10.0.0.0/16"}
module "application" { source = "./modules/application"
vpc_id = module.networking.vpc_id subnet_ids = module.networking.private_subnet_ids
# Even though vpc_id and subnet_ids create implicit dependencies, # the application module also needs the NAT gateway to be ready # (which isn't referenced directly) depends_on = [module.networking]}What depends_on Does NOT Do
depends_on affects creation and destruction ordering only. It does not:
- Pass data between resources (use attribute references for that)
- Retry operations until a dependency is healthy
- Wait for an application-level condition (like a service to become healthy)
For health checks, use aws_lb_target_group_attachment waiters, or null_resource with polling scripts.
Avoid Overusing depends_on
Adding unnecessary depends_on declarations slows down Terraform by forcing sequential execution where parallelism is safe. Terraform applies resources in parallel by default — depends_on breaks that.
# BAD: unnecessary depends_on that forces sequential creationresource "aws_s3_bucket" "assets" { ... }resource "aws_s3_bucket" "logs" { depends_on = [aws_s3_bucket.assets] # These are independent — no reason to sequence}
# GOOD: leave independent resources without depends_onresource "aws_s3_bucket" "assets" { ... }resource "aws_s3_bucket" "logs" { ... } # Created in parallel with assets bucketIf you find yourself adding depends_on frequently, consider whether your resources are properly connected by attribute references — that’s Terraform’s preferred way to express ordering.