# Terraform depends_on and Resource Dependencies Explained

> How Terraform builds its dependency graph, the difference between implicit and explicit dependencies, when to use depends_on, and how to fix dependency cycles.

- Source: https://www.itwonderlab.com/terraform-depends-on-dependencies/
- Published: 2026-05-19
- Updated: 2026-05-19
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## 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:

```hcl title="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:

```hcl title="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](https://www.itwonderlab.com/aws-terraform-tutorial-aws-nat-gateway/) route that must exist before instances that download packages at first boot, or an [internet gateway](https://www.itwonderlab.com/aws-terraform-tutorial-aws-internet-gateway/) before an EIP:

```hcl title="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:

```hcl title="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

```shell
$ 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](https://www.itwonderlab.com/aws-terraform-tutorial-aws-security-groups/) that allow traffic from each other in inline rules. Solve it by moving the rules into separate resources:

```hcl title="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](https://www.itwonderlab.com/terraform-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`](https://www.itwonderlab.com/terraform-lifecycle-meta-argument/).
