Terraform’s depends_on meta-argument is often seen as a crutch, a way to force execution order when the dependency graph should be implicit.

Let’s see it in action. Imagine you have a simple Terraform configuration that creates an AWS S3 bucket and then, using a null_resource, runs a script that needs to know the bucket’s ARN.

resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-unique-terraform-bucket-12345"
  acl    = "private"
}

resource "null_resource" "run_script" {
  triggers = {
    bucket_arn = aws_s3_bucket.my_bucket.arn
  }

  provisioner "local-exec" {
    command = "echo 'Bucket ARN is: ${self.triggers.bucket_arn}' > bucket_info.txt"
  }
}

Here, run_script implicitly depends on my_bucket because it references aws_s3_bucket.my_bucket.arn. Terraform’s graph engine correctly infers this. When you run terraform apply, it will create the S3 bucket first, then use its ARN in the null_resource.

But what if the dependency isn’t directly about resource attributes? Consider this scenario: you’re creating an EC2 instance and then want to configure a load balancer after the instance is fully running and has a public IP.

resource "aws_instance" "web_server" {
  ami           = "ami-0abcdef1234567890" # Example AMI
  instance_type = "t2.micro"
  tags = {
    Name = "HelloWorld"
  }
}

resource "aws_lb" "app_lb" {
  name               = "app-lb"
  internal           = false
  load_balancer_type = "application"
  subnets            = ["subnet-0123456789abcdef0", "subnet-0fedcba9876543210"] # Example subnets
}

resource "aws_lb_target_group" "app_tg" {
  name     = "app-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = "vpc-0123456789abcdef0" # Example VPC ID
}

resource "aws_lb_listener" "app_listener" {
  load_balancer_arn = aws_lb.app_lb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app_tg.arn
  }
}

resource "aws_lb_target_group_attachment" "web_server_attachment" {
  target_group_arn = aws_lb_target_group.app_tg.arn
  target_id        = aws_instance.web_server.id
  port             = 80
}

In this setup, aws_lb_target_group_attachment depends on aws_instance.web_server.id and aws_lb_target_group.app_tg.arn. The load balancer (aws_lb) and listener (aws_lb_listener) depend on each other via their ARNs. Terraform will correctly order these.

However, imagine a more complex scenario where you need to ensure a security group is fully created and associated with a network interface before a specific resource attempts to use it, even if the ARN is available earlier. Or, you have a custom provider that doesn’t expose a clear attribute to signal readiness. This is where depends_on becomes relevant.

The depends_on meta-argument allows you to explicitly declare a dependency between resources, even when Terraform cannot infer it automatically from resource attributes. It’s a way to inject an ordering constraint into the execution plan.

Let’s say you have a situation where you need to create an IAM role, and then create a Lambda function that uses that role. The Lambda function resource might reference the role’s ARN, but perhaps the role creation involves multiple steps or external dependencies that aren’t immediately reflected in its ARN.

resource "aws_iam_role" "lambda_role" {
  name = "my-lambda-execution-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_lambda_function" "my_lambda" {
  function_name    = "my-cool-lambda"
  role             = aws_iam_role.lambda_role.arn
  handler          = "index.handler"
  runtime          = "nodejs18.x"
  filename         = "lambda_function.zip"
  source_code_hash = filebase64sha256("lambda_function.zip")

  # Even though role.arn is used, we want to be absolutely sure
  # the IAM role is fully provisioned before the Lambda is created.
  depends_on = [
    aws_iam_role.lambda_role
  ]
}

In this example, aws_lambda_function.my_lambda references aws_iam_role.lambda_role.arn. Terraform would normally infer this dependency. However, explicitly adding depends_on = [aws_iam_role.lambda_role] guarantees that the aws_iam_role.lambda_role resource will be fully created and its state finalized before Terraform begins creating aws_lambda_function.my_lambda. This can prevent race conditions or errors if the IAM role’s ARN is available, but the underlying IAM entity isn’t fully propagated or ready for use by other AWS services.

The most common use case for depends_on is when a resource’s creation or update depends on a side effect of another resource that isn’t directly exposed as an attribute. For instance, if a resource performs an action that requires a specific DNS record to exist, and that DNS record is managed by a separate aws_route53_record resource, you might use depends_on on the DNS record resource.

A more nuanced situation arises with custom providers or external services. If a resource provisioner (like local-exec or remote-exec) needs to interact with a resource that has a complex creation lifecycle, and that lifecycle’s completion isn’t signaled by a simple attribute reference, depends_on can enforce the necessary ordering. For example, a null_resource that runs a script to register an instance with a custom service might depends_on the aws_instance resource, ensuring the instance is running and accessible before the registration script attempts to connect.

Another scenario is when dealing with resource deletion. While depends_on primarily affects creation order, it can also influence the order in which resources are destroyed. If resource A depends_on resource B, Terraform will destroy B after A. This is crucial when resource A might hold a lock or prevent B from being deleted cleanly.

Consider a situation where you have a database instance and an application that connects to it. You want to ensure the application is shut down gracefully before the database is deleted.

resource "aws_db_instance" "app_db" {
  # ... database configuration ...
}

resource "aws_instance" "app_server" {
  # ... instance configuration ...
  # This instance needs to connect to the DB.
}

# This null_resource represents the shutdown process of the app server.
resource "null_resource" "shutdown_app" {
  # Trigger this only when the app_server is about to be destroyed.
  triggers = {
    instance_id = aws_instance.app_server.id
  }

  provisioner "local-exec" {
    command = "echo 'Shutting down application on instance ${self.triggers.instance_id}...' && sleep 30" # Simulate graceful shutdown
  }

  # Ensure shutdown happens BEFORE the DB is destroyed.
  depends_on = [
    aws_instance.app_server
  ]
}

# We want the DB to be destroyed AFTER the app server is shut down.
# Terraform's default destroy order would destroy app_server before db_instance.
# However, if we explicitly want to ensure the shutdown_app resource runs
# and completes before the DB is touched, we can add a dependency here.
resource "aws_db_instance" "app_db" {
  # ... database configuration ...

  # This is a bit of a workaround to ensure the shutdown process completes.
  # In a real scenario, you might have a resource that actively waits for
  # the app_server to be gone or for a specific event.
  # For simplicity here, we'll just ensure the shutdown_app runs first.
  # This is more about controlling the order of destroy for related components.

  # A more direct way is to make the DB depend on the shutdown resource.
  # This is less common for destroy-only dependencies.
}

In the destroy phase, Terraform builds a dependency graph for destruction. If resource_A depends on resource_B, resource_A will be destroyed before resource_B. This is the inverse of the creation graph. So, if aws_db_instance.app_db depended on null_resource.shutdown_app, the shutdown_app would be destroyed before app_db. This is precisely what we want for a graceful shutdown followed by resource deletion.

The key takeaway is that depends_on is a tool to override Terraform’s automatic dependency analysis when that analysis is insufficient or when you need to enforce an explicit ordering for reasons not captured by resource attribute references. It’s a way to tell Terraform, "I know you think these are independent, but trust me, one must complete before the other starts."

The next thing you’ll likely encounter is how to manage secrets and sensitive data across resources, especially when one resource’s output is another’s input.

Want structured learning?

Take the full Terraform course →