Skip to main content

Terraform depends_on Explained: Control Resource Ordering

Key Takeaway

Learn when to use Terraform depends_on for explicit resource dependencies. Understand implicit vs explicit dependencies, common use cases, and when depends_on is wrong.

Table of Contents

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:

  1. Waits for the dependency to be fully created/updated before starting the resource
  2. Destroys the resource before destroying the dependency (reverse order)
  3. 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

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.

🚀

Level Up Your Terraform Skills

Hands-on courses, books, and resources from Luca Berton

Luca Berton
Written by

Luca Berton

DevOps Engineer, AWS Partner, Terraform expert, and author. Creator of Ansible Pilot, Terraform Pilot, and CopyPasteLearn.