Mastering Terraform depends_on Meta Argument

In today’s blog post, we are going to cover terraform depends_on meta argument. Terraform internally manage dependency between resources to determine the order in which resources will get provisioned, updated, or destroyed. Terraform does this by creating a dependency graph between the resources. For example say you need to provision a server and it’s storage. Now unless the storage is provisioned and ready to use, you cannot start provisioning the server. Terraform needs to identify the order in which it has to deploy, update or destroy a resource and terraform automatically manages this dependency graph.

However, sometimes Terraform’s automatic dependency inference is not enough. There are subtle, non-obvious dependencies that, if not explicitly handled, can lead to failed deployments, race conditions, or unexpected behavior. This is where the depends_on meta-argument comes into picture.

The Problem: Implicit Dependencies and Their Limitations

Terraform’s intelligence lies in its ability to build a dependency graph by analyzing your configuration. If resource A’s attribute is used as an input to resource B, Terraform automatically infers that resource A must be created or updated before resource B.

Consider this common scenario:

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

resource "aws_instance" "web_server" {
  ami           = "ami-0abcdef1234567890" # Replace with a valid AMI ID
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.public.id
}

Here, Terraform implicitly understands:

  1. aws_vpc.main must exist before aws_subnet.public (because of vpc_id = aws_vpc.main.id).
  2. aws_subnet.public must exist before aws_instance.web_server (because of subnet_id = aws_subnet.public.id).

This system works for direct attribute relationships. However, implicit dependencies fall short in several important scenarios:

  • No Direct Attribute Linkage: A resource might logically depend on another being fully operational, but there is no direct attribute in its configuration that links to the dependency. For example, a database user needs a database instance to be ready to accept connections, not just created.
  • External Actions or State Changes: Terraform might need to wait for an event or action that occurs outside of its direct management, or for an external system to reflect a change made by Terraform.
  • Provider Limitations or Race Conditions: Occasionally, a cloud provider’s API or a Terraform provider’s implementation might have subtle timing issues or race conditions that even implicit dependencies do not reliably resolve.

These are the situations where depends_on steps in to provide explicit control.

Introducing depends_on

The depends_on meta-argument allows you to explicitly define dependencies between blocks in your Terraform configuration. It forces Terraform to ensure that the specified dependencies are fully provisioned and stable before the block where depends_on is declared is processed.

Syntax:

depends_on is defined within a resource, data, or module block and takes a list of references to other blocks.

resource "resource_type" "resource_name" {
  # ... configuration ...
  depends_on = [
    resource_type.another_resource_name,
    data.data_type.data_name,
    module.my_module,
  ]
}

The key characteristic is that depends_on accepts a list, meaning you can specify multiple dependencies for a single block.

Where depends_on Can Be Used (with Examples)

Let us explore its application across the core Terraform block types:

1. Resources

This is the most common and widely recognized use case for depends_on. It ensures a resource is created or updated only after other specified resources are fully provisioned.

Scenario: An S3 bucket policy needs to reference an IAM role. While the policy attachment might implicitly depend on the role’s ARN, you want to ensure the IAM role is not just created but also fully propagated before the policy is attempted.

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

resource "aws_s3_bucket" "app_logs" {
  bucket = "my-application-logs-bucket-2025-unique" # Must be globally unique
}

resource "aws_s3_bucket_policy" "restrict_access" {
  bucket = aws_s3_bucket.app_logs.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowLambdaAccess"
        Effect = "Allow"
        Principal = {
          AWS = aws_iam_role.my_application_role.arn
        }
        Action = [
          "s3:GetObject",
          "s3:PutObject"
        ]
        Resource = "${aws_s3_bucket.app_logs.arn}/*"
      },
    ]
  })

  # Explicitly wait for the IAM role to be fully available
  # even though its ARN is referenced. This mitigates propagation delays.
  depends_on = [aws_iam_role.my_application_role, aws_s3_bucket.app_logs]
}

2. Data Sources

You can use depends_on with data blocks to ensure that the data source is refreshed or read after specific resources have been created or updated, or after other data sources have completed their operations. This is important when the external state being queried by the data source relies on a prior Terraform action.

Scenario: You have a null_resource that executes a script to create an AWS Systems Manager (SSM) Parameter, and then you immediately want to read that parameter using a data block. Implicit dependency might not be enough to guarantee the parameter is available right away.

resource "null_resource" "create_ssm_param" {
  provisioner "local-exec" {
    command = "aws ssm put-parameter --name '/my-app/db-connection-string' --value 'some-secret-value' --type 'SecureString' --overwrite"
  }

  # This trigger ensures the provisioner runs on every apply if the value changes
  # (in a real scenario, you'd manage secrets more securely, e.g., using Secrets Manager)
  triggers = {
    always_run = timestamp()
  }
}

data "aws_ssm_parameter" "db_connection" {
  name = "/my-app/db-connection-string"
  type = "SecureString"

  # Explicitly depend on the null_resource completing its execution
  # to ensure the SSM parameter is written before we attempt to read it.
  depends_on = [null_resource.create_ssm_param]
}

output "db_param_value" {
  value = data.aws_ssm_parameter.db_connection.value
  sensitive = true
}

3. Modules

Applying depends_on to a module block allows you to enforce that an entire module (and all the resources it defines) is deployed after other specified resources or other modules are complete. This provides a high-level ordering for complex infrastructure stacks.

Scenario: You have a database module that provisions a database cluster and an application module that deploys your application servers. The application must only start deploying once the database is fully set up and ready to accept connections. While there might be implicit dependencies via database outputs, a depends_on on the module can ensure the entire database stack is stable.

# modules/database/main.tf
# (defines aws_rds_cluster, aws_rds_cluster_instance, etc.)
# modules/database/outputs.tf
# (defines db_endpoint, db_port)

module "database" {
  source = "./modules/database"
  # ... other database specific variables ...
}

# modules/application/main.tf
# (defines aws_ec2_instance, aws_lb, etc.)

module "application" {
  source = "./modules/application"
  # ... application specific variables ...
  
  # Pass database outputs as inputs to the application module
  db_connection_string = "jdbc:mysql://${module.database.db_endpoint}:${module.database.db_port}/myapp"

  # Explicitly depend on the entire database module
  # to ensure its complete deployment before starting the application stack.
  depends_on = [module.database]
}

Best Practices and Considerations

While powerful, depends_on should be used with care:

  • Prioritize Implicit Dependencies: Always strive to use implicit dependencies (via attribute references or data sources) whenever possible. They are the most natural and robust way for Terraform to manage your graph. depends_on is a fallback for when implicit methods fail.
  • Clarity and Readability: When you do use depends_on, ensure the reason is clear. Add comments to your code explaining why this explicit dependency is necessary. Overusing it can make your configurations hard to read and debug.
  • Impact on Parallelism: Terraform is highly efficient at parallelizing resource creation. Every depends_on declaration creates a sequential bottleneck, forcing Terraform to wait. Excessive use can significantly increase your terraform apply times.
  • Debugging depends_on Issues: If you find a resource still failing despite depends_on, verify that the depended-upon object truly reaches the desired stable state (e.g., database accepting connections) and not just a “created” state. You might need custom null_resource and provisioner blocks with checks for complex external dependencies.
  • for_each vs. depends_on on Collection: When managing collections of resources created with count or for_each, you typically refer to the entire collection (e.g., aws_instance.web_servers) in depends_on if you want all instances to complete. If you need a specific instance, refer to it by its index or key (e.g., aws_instance.web_servers[0]).

Conclusion

The depends_on meta-argument is an important feature in a Terraform practitioner’s toolkit. It helps you to explicitly manage dependencies that Terraform cannot automatically detect, ensuring your infrastructure is provisioned in the precise order required, even when dealing with complex inter-service relationships or external system interactions.

By understanding its application across resource, data, and module blocks, and by adhering to best practices like prioritizing implicit dependencies and being mindful of its impact on parallelism, you can build more resilient, predictable, and robust infrastructure deployments using Terraform.

Author

Debjeet Bhowmik

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins.
In my free time, I write blogs on ckdbtech.com

Leave a Comment