# Fix the "Cycle" Dependency Error in Terraform

URL: https://www.devopsness.com/blog/fix-terraform-cycle-error
Author: Kiril Urbonas
Published: 2026-08-01
Updated: 2026-10-31
Categories: DevOps
Tags: Terraform, Iac, Troubleshooting, Dependencies, DevOps

Summary: A "Cycle" error means two Terraform resources depend on each other; here is how to find the loop and break it cleanly.

---
# Fix the "Cycle" Dependency Error in Terraform

Fix `Error: Cycle:` by finding the reference loop and cutting the edge that points backward. Run `terraform graph -type=plan -draw-cycles` to color the loop, then restructure. The most common case, two security groups with inline rules pointing at each other, is fixed by moving the rules into separate `aws_vpc_security_group_ingress_rule` resources.

> [!VERIFIED]
> Terraform v1.17.1, hashicorp/aws provider v6.71.0, against the developer.hashicorp.com `terraform graph` docs and the AWS provider security group rule docs. Checked October 2026.

You run `terraform plan`, and instead of a plan you get `Error: Cycle:` followed by a list of resource addresses. No line number, no obvious typo, nothing you can grep for in a single file. It confuses people because the error is about relationships between resources, not the resources themselves.

## What a cycle is

Before Terraform applies anything, it builds a dependency graph. Every resource, data source, module, and output becomes a node. Every reference (`aws_subnet.main.id`, `module.network.vpc_id`, an explicit `depends_on`) becomes a directed edge. Terraform then walks that graph in dependency order so it creates things before the things that need them.

That only works if the graph is a **directed acyclic graph**: no node can, by following edges, eventually reach itself. A "Cycle" error means it can. Resource A depends on B, and B depends on A, either directly or through a chain of other resources. Terraform cannot decide which one to create first, so it refuses to plan.

## What the error looks like

The classic version comes from two security groups that reference each other:

```
Error: Cycle: aws_security_group.a, aws_security_group.b
```

Longer chains show the full loop, which is your best clue:

```
Error: Cycle: module.app.aws_iam_role.this,
module.app.aws_iam_role_policy.this,
aws_iam_role.this
```

Read the list as "each of these depends on the next, and the last one loops back to the first."

## Seeing the graph

When the loop runs through more than two resources, generate the graph and look at it:

```
$ terraform graph -type=plan -draw-cycles | dot -Tsvg > graph.svg
$ open graph.svg   # xdg-open on Linux
```

`terraform graph` emits the dependency graph in DOT format; piping it through Graphviz's `dot` gives you an image. Plain `terraform graph` prints a simplified resource-only view, and `-draw-cycles` only works together with a `-type` value, which is why the command above passes `-type=plan`. With it, Terraform colors the edges that form the cycle. If you do not have Graphviz, even the raw text output shows the edges as `"a" -> "b"` lines that you can trace by hand. Follow the arrows from any name in the error message and you will find the path that returns to where you started.

## Common real causes

A handful of patterns produce almost every cycle you will hit in practice.

**Two security groups referencing each other.** Group A allows traffic from group B, and group B allows traffic from group A, both with inline rules. Each SG now depends on the other's ID.

**A resource referencing its own output.** An attribute that feeds back into the same resource's arguments, often introduced by a `for_each` or `count` expression that reads from the thing it is building.

**Module input/output loops.** Module X takes an input from module Y's output, and module Y takes an input from module X's output. The loop is between the module boundaries, not any single resource.

**`depends_on` creating a loop.** Someone added an explicit `depends_on` to be safe, not realizing the target already depends on this resource. Now the edge goes both ways.

**IAM role and policy mutual references.** A role that references a policy which references the role's ARN back, common when an assume-role policy and an inline policy get tangled.

## Breaking the classic security group cycle

Here is the cyclic version. Two groups, each with an inline rule pointing at the other:

```hcl
# network/security_groups.tf (cyclic)
resource "aws_security_group" "a" {
  name   = "app-a"
  vpc_id = var.vpc_id

  ingress {
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.b.id]
  }
}

resource "aws_security_group" "b" {
  name   = "app-b"
  vpc_id = var.vpc_id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.a.id]
  }
}
```

Group `a` needs `b.id` to exist, and group `b` needs `a.id`. Neither can be created first. `Error: Cycle: aws_security_group.a, aws_security_group.b`.

The fix is to stop putting the rules inside the groups. Define the security groups with no inline rules, then attach the rules as separate resources. Now each rule depends on both groups, and the groups depend on nothing but the VPC:

```hcl
# network/security_groups.tf (fixed)
resource "aws_security_group" "a" {
  name   = "app-a"
  vpc_id = var.vpc_id
}

resource "aws_security_group" "b" {
  name   = "app-b"
  vpc_id = var.vpc_id
}

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                    = 5432
  to_port                      = 5432
}
```

The graph now flows one way: both groups get created, then both rules attach. No node can reach itself. The older `aws_security_group_rule` resource breaks the cycle the same way, but as of provider v6.71.0 the AWS docs say to avoid both it and inline `ingress`/`egress` blocks, because they struggle with multiple CIDR blocks, tags and descriptions. The principle is identical: move the relationship out of the resource and into a resource that depends on both ends.

> [!WARNING]
> Do not mix inline `ingress`/`egress` blocks or `aws_security_group_rule` with the `aws_vpc_security_group_*_rule` resources on the same security group. The provider docs warn this "may cause rule conflicts, perpetual differences, and result in rules being overwritten." Pick one style per group.

## Breaking the other cases

**Remove unnecessary `depends_on`.** If resource B already references an attribute of A, delete any explicit `depends_on = [A]`. The implicit edge is enough, and the explicit one is often what closed the loop.

**Restructure module boundaries.** When two modules feed each other, one of them is doing too much. Pull the shared resource up to the root module, or into a third module both can depend on, so the dependency runs in one direction. If that third module is shared across teams, pin it the way we describe in [versioning Terraform modules in a shared registry](/blog/terraform-module-versioning-shared-registries).

**Split a resource.** If a resource references its own output, separate the part that produces the value from the part that consumes it, the same move as the security group rules. Sometimes a `data` source reading the created resource, referenced later, avoids the self-reference entirely.

After any of these, run `terraform graph -type=plan -draw-cycles` again and confirm no edges are colored before you re-plan.

For the wider set of failures you will meet, see the [Terraform error troubleshooting guide](/blog/terraform-error-troubleshooting-guide).

## Questions people ask

### How do I find which resources cause a Terraform cycle?

Start with the addresses in the error: `Error: Cycle:` lists every node in the loop, in order. For long chains, run `terraform graph -type=plan -draw-cycles | dot -Tsvg > graph.svg`, which colors the cycle edges. Without Graphviz, read the raw DOT output and follow the `"a" -> "b"` lines from any listed address until you return to it.

### Can depends_on cause a cycle error in Terraform?

Yes, and it is one of the most common causes after a refactor. An explicit `depends_on` adds an edge even when an attribute reference already implies the opposite direction, which closes the loop. If resource B already reads an attribute of A, delete `depends_on = [A]` from B, and never point `depends_on` at something that consumes your outputs.

### Why does a cycle appear only when I destroy resources?

Destroy runs the graph in reverse, and `create_before_destroy` adds extra edges, so a configuration that plans cleanly can still cycle during `terraform destroy` or a replacement. Graph the destroy path with `terraform graph -type=plan-destroy -draw-cycles` to see it. The usual fix is the same: split the resource that sits on both sides of the loop.

## The call we'd make

Reach for separate rule resources by default. Inline security group rules read nicely in a demo, but the moment two groups need to talk to each other you are one edit away from a cycle. Standalone `aws_vpc_security_group_ingress_rule` resources cost a few extra lines and make your dependency graph honest: groups first, relationships second. When a cycle does show up, do not guess. Run `terraform graph`, trace the arrows back to the start, and cut the single edge that should never have pointed backward.

## Sources

- [Terraform CLI docs: terraform graph](https://developer.hashicorp.com/terraform/cli/commands/graph)
- [Terraform AWS provider docs: aws_vpc_security_group_ingress_rule](https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/r/vpc_security_group_ingress_rule.html.markdown)
- [Terraform releases on GitHub](https://github.com/hashicorp/terraform/releases)

