Terraform depends_on and Resource Dependencies Explained

· 2 min read · Terraform & OpenTofu Tutorials

How Terraform decides the order #

Terraform does not run your files from top to bottom. It builds a dependency graph of all resources and creates in parallel everything that does not depend on something else (10 operations at once by default). It destroys in the reverse order. Understanding the graph explains most ordering problems.

Implicit dependencies #

A reference to another resource's attribute creates a dependency automatically:

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

resource "aws_subnet" "private" {
  vpc_id     = aws_vpc.main.id     # the subnet depends on the VPC
  cidr_block = "10.0.1.0/24"
}

The subnet is created after the VPC and destroyed before it. This is the preferred way: the dependency is visible exactly where the value is used.

Explicit dependencies with depends_on #

Use depends_on when there is a dependency that Terraform cannot see because no attribute is referenced. The classic example on AWS is an IAM permission that the application needs but does not mention:

main.tf
resource "aws_iam_role_policy" "app" {
  name   = "app"
  role   = aws_iam_role.app.id
  policy = data.aws_iam_policy_document.app.json
}

resource "aws_instance" "app" {
  ami                  = data.aws_ami.ubuntu.id
  instance_type        = "t3.micro"
  iam_instance_profile = aws_iam_instance_profile.app.name

  # The application reads from S3 at boot, so the policy must exist first
  depends_on = [aws_iam_role_policy.app]
}

Another common one is a NAT gateway route that must exist before instances that download packages at first boot, or an internet gateway before an EIP:

main.tf
resource "aws_eip" "nat" {
  domain     = "vpc"
  depends_on = [aws_internet_gateway.main]
}

depends_on also works in module blocks and data sources, but with side effects (see below).

Cautions with depends_on #

  • A depends_on on a module or data source makes Terraform treat all their values as unknown until apply, which can cause more changes in the plan and "known after apply" values. Use it sparingly and prefer a reference.
  • It does not wait for the resource to be ready in the real world, only for the API call to complete. For eventual-consistency problems (a role that is not yet usable) use the resource's own waiting options or a time_sleep resource as a last resort.
  • Do not use it "just in case". It makes the graph less parallel and the code harder to understand.

Dependencies between modules #

Pass outputs as inputs, and the dependency is implicit:

main.tf
module "network" {
  source = "./modules/network"
}

module "app" {
  source     = "./modules/app"
  subnet_ids = module.network.private_subnet_ids   # app depends on network
}

See the graph #

$ terraform graph | dot -Tpng > graph.png

It needs Graphviz (dot). For large configurations the graph is huge, but it is useful to see why a resource waits for another.

Dependency cycles #

Error: Cycle: aws_security_group.a, aws_security_group.b

A cycle appears when A references B and B references A. The classic case is two security groups that allow traffic from each other in inline rules. Solve it by moving the rules into separate resources:

security.tf
resource "aws_security_group" "a" { name = "a" }
resource "aws_security_group" "b" { name = "b" }

resource "aws_vpc_security_group_ingress_rule" "a_from_b" {
  security_group_id            = aws_security_group.a.id
  referenced_security_group_id = aws_security_group.b.id
  ip_protocol                  = "tcp"
  from_port                    = 443
  to_port                      = 443
}

resource "aws_vpc_security_group_ingress_rule" "b_from_a" {
  security_group_id            = aws_security_group.b.id
  referenced_security_group_id = aws_security_group.a.id
  ip_protocol                  = "tcp"
  from_port                    = 443
  to_port                      = 443
}

Now the groups do not depend on each other, only the rules do. See common errors.

Order when destroying #

Dependencies work backwards on destroy. If destroying fails because something still uses a resource (for example a subnet with a network interface created by a load balancer), the dependency is not in your code: remove the other resource first or check what is attached. See also lifecycle create_before_destroy.

#Terraform #OpenTofu #AWS