Skip to main content
RunBook Academy

TerraformVIII · Dependencies and the Resource GraphDependencies

Explicit Dependencies with depends_on

Intermediate⏱ ~14 minbash

What you'll learn

  • Use `depends_on` for dependencies that are not captured as references
  • Place `depends_on` inside the resource block and validate the syntax
  • Explain how `depends_on` interacts with `lifecycle.create_before_destroy`
  • Recognise over-use of `depends_on` as a code smell

Prerequisites

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

Implicit dependencies cover most cases. When aws_instance.web references aws_subnet.a.id, Terraform records the edge and serialises the instance after the subnet. No operator intervention required.

Some dependencies are not captured as references. The classic example is an EC2 instance that assumes an IAM role. The instance block does not reference any attribute of the role. The instance still must wait for the role to be propagated by AWS IAM before the instance can successfully assume it. There is no attribute on the role that the instance block could reference; the propagation is a side effect.

For these cases, Terraform provides the depends_on meta-argument.

The case for depends_on

Three common shapes:

1. IAM propagation. An EC2 instance has an instance profile attached to an IAM role. The instance’s user-data script assumes the role to read a secret. The instance must wait for the role and its policy attachments to be propagated before the user-data runs. The instance block does not read any attribute of the role; the dependency is invisible to the expression walker.

2. DNS propagation. A Route 53 record references an ELB. The record is created immediately after the ELB. AWS DNS is eventually consistent; the record’s first lookup may fail. The record block references the ELB’s DNS name (so there is an implicit dependency), but the propagation delay is independent of the resource being “ready” in Terraform’s sense.

3. Cross-module side effects. A module produces a value that another module depends on without exposing it as an output. The dependency is real; the value is not addressable from the consumer.

resource "aws_iam_role" "app" {
  name = "app-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
      Action = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "app_s3" {
  role       = aws_iam_role.app.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}

resource "aws_iam_instance_profile" "app" {
  name = "app-profile"
  role = aws_iam_role.app.name
}

resource "aws_instance" "web" {
  ami                  = "ami-0e1bed4f"
  instance_type        = "t3.medium"
  iam_instance_profile = aws_iam_instance_profile.app.name

  depends_on = [
    aws_iam_role_policy_attachment.app_s3,
  ]
}

The aws_instance.web block does not reference the policy attachment. The user-data script assumes the role and reads an S3 object. Without depends_on, the apply graph would allow the instance to be created in the same wave as the policy attachment. The user-data would race against IAM propagation and fail intermittently. With depends_on, the instance waits for the attachment.

Placement and syntax

depends_on is a meta-argument. It lives inside the resource block, alongside count, for_each, provider, and lifecycle. It is not inside the lifecycle block.

Correct:

resource "aws_instance" "web" {
  ami           = "ami-0e1bed4f"
  instance_type = "t3.medium"

  depends_on = [
    aws_iam_role_policy_attachment.app_s3,
  ]
}

Incorrect (will fail at plan time):

resource "aws_instance" "web" {
  ami           = "ami-0e1bed4f"
  instance_type = "t3.medium"

  lifecycle {
    depends_on = [aws_iam_role_policy_attachment.app_s3]
  }
}

The argument is a list of resource addresses. Each address is a full Terraform address: aws_iam_role_policy_attachment.app_s3. References to count and for_each instances use the bracket syntax: aws_iam_role_policy_attachment.app_s3[0]. Module addresses use the double-dot syntax: module.network.aws_subnet.a.

resource "aws_instance" "web" {
  ami           = "ami-0e1bed4f"
  instance_type = "t3.medium"

  depends_on = [
    aws_iam_role_policy_attachment.app_s3,
    aws_iam_instance_profile.app,
    module.network.aws_route_table.private,
  ]
}

How depends_on interacts with lifecycle

depends_on and lifecycle.create_before_destroy are independent controls:

  • depends_on adds an edge to the graph. The edge affects both create and destroy order: the dependent resource cannot be created until the dependency is created; the dependent resource cannot be destroyed until the dependency is destroyed.
  • lifecycle.create_before_destroy controls the order during replacement: the new resource is created before the old is destroyed. It does not change the graph; it changes the apply walker.

A resource with both:

resource "aws_instance" "web" {
  ami           = "ami-0e1bed4f"
  instance_type = "t3.medium"

  depends_on = [aws_iam_role_policy_attachment.app_s3]

  lifecycle {
    create_before_destroy = true
  }
}

When the instance is replaced, Terraform creates the new instance, waits for the new instance to be ready, then destroys the old instance. The depends_on edge still applies: the new instance cannot be created until the policy attachment is also ready (the attachment is not recreated, but the edge is honoured).

The two controls can conflict in narrow cases. If the dependency is a resource that is also being replaced, create_before_destroy on the dependency can change the order Terraform uses to satisfy the edge. The result is still correct; it is just not what a casual reading of the configuration might suggest.

When NOT to use depends_on

depends_on is over-used. The signal that depends_on is wrong is almost always “I am adding this because Terraform did not infer the edge from a reference.” The right fix is usually to add the missing reference, not to add depends_on.

Three patterns that look like depends_on but are actually bugs:

1. Forgetting to reference the output.

# Wrong:
resource "aws_instance" "web" {
  ami           = "ami-0e1bed4f"
  instance_type = "t3.medium"
  subnet_id     = aws_subnet.a.id
  # ... no reference to the role
  depends_on = [aws_iam_role.app]
}

If the instance does not reference the role, the user-data script is the only consumer. If the role is referenced via the instance profile (iam_instance_profile = aws_iam_instance_profile.app.name), the graph already has the edge. Adding depends_on = [aws_iam_role.app] adds a redundant edge.

2. Serialising unrelated resources for “safety.”

# Code smell:
resource "aws_s3_bucket" "logs" {
  bucket = "myapp-logs"
  depends_on = [aws_s3_bucket.app_data]
}

The two buckets are independent. The depends_on does not buy correctness; it serialises the apply for no reason.

3. Using depends_on to control module load order.

# Wrong:
module "network" {
  source = "./modules/network"
}

resource "aws_instance" "web" {
  # ...
  depends_on = [module.network]
}

module blocks are evaluated when the configuration is parsed; the depends_on = [module.network] is invalid syntax for a module. The correct way to express a dependency on a module is to reference one of its outputs.

How depends_on interacts with -target

When you run terraform apply -target=aws_instance.web, Terraform narrows the plan to the targeted resource and its dependencies. If a depends_on edge points to a resource that is not targeted, Terraform checks whether the resource is already in state. If it is, the apply proceeds. If it is not, the apply fails.

terraform apply -target=aws_instance.web

If aws_iam_role.app is not in state (because it was never applied or was removed), the apply fails:

Error: Cycle: aws_instance.web (depends_on) includes aws_iam_role.app,
  which is not in the plan.

The fix is to target the dependency explicitly:

terraform apply \
  -target=aws_iam_role.app \
  -target=aws_iam_instance_profile.app \
  -target=aws_iam_role_policy_attachment.app_s3 \
  -target=aws_instance.web

Or to drop -target and apply the full plan.

Failure modes

  1. Reference would have been enough. A depends_on is added instead of an attribute reference. The apply is serialised unnecessarily. The fix is to add the reference.
  2. Dependency in another state file. A depends_on to a resource in a different Terraform state is invalid. Terraform does not cross state boundaries. Use terraform_remote_state to consume the resource.
  3. Non-existent address. A depends_on to a misspelled address produces Error: Reference to undeclared resource at plan time.
  4. Module address without double dots. depends_on = [module.network] is a parse error. Module dependencies are expressed through output references, not depends_on.
  5. depends_on inside lifecycle. Parse error at plan time. Move the block.
  6. depends_on survives a state move that breaks the address. terraform state mv preserves the dependency if the address still matches; otherwise the depends_on becomes invalid and the plan fails.

How to validate

terraform validate
terraform plan -out=tfplan
terraform graph -type=plan | grep -B1 "aws_instance.web"

The grep shows the immediate edges for the resource. Each edge should be either a real reference or an intentional depends_on. Reviewers should reject PRs that add depends_on without a comment explaining why a reference is not sufficient.

Performance implications

Every depends_on is a serialisation point. A configuration with N independent resources and D depends_on edges runs in at least ceil(N / -parallelism) + D waves. For a 100-resource configuration with 5 incidental depends_on blocks, the apply is at least 5 waves slower than it could be.

The production rule: every depends_on is reviewed; every depends_on is commented; every depends_on is removed when the side effect no longer applies.

What to do in production

  • Use depends_on only when the dependency is on a side effect that is not surfaced as an attribute (IAM propagation, DNS, secrets rotation, scheduled actions).
  • Do not use depends_on to compensate for a missing reference. Add the reference.
  • Document every depends_on in the module’s README. Reviewers must be able to verify the necessity.
  • Periodically run terraform graph and remove depends_on blocks that are no longer required.

Verification

  • Run terraform validate and confirm the syntax is correct.
  • Run terraform plan -out=tfplan and confirm the apply order honours the depends_on edges.
  • Run terraform graph -type=plan | grep for each resource that has a depends_on and confirm the edges are intentional.
  • For each depends_on, write a one-line comment explaining why a reference is not sufficient.

Knowledge check · 7 questions

  1. Q1. When should `depends_on` be used?

  2. Q2. `depends_on` accepts a list of resource addresses.

  3. Q3. Where does the `depends_on` block belong in HCL?

  4. Q4. Which of the following are valid uses of `depends_on`? (Select all that apply.)

  5. Q5. An EC2 instance uses an IAM role. The role is created in the same apply. The user-data script fails because the role's policy attachment is not yet propagated. The instance's `depends_on` lists the role but not the policy attachment. What is the cleanest fix?

  6. Q6. What is the relationship between `depends_on` and `lifecycle.create_before_destroy`?

  7. Q7. Listing a non-existent resource in `depends_on` causes Terraform to error at plan time.

Passing score: 75%. Answers are checked in this browser.