| title | Terraform Dependency Cycle | |||||
|---|---|---|---|---|---|---|
| slug | terraform-dependency-cycle | |||||
| technologies |
|
|||||
| severity | medium | |||||
| tags |
|
|||||
| related |
|
|||||
| last_reviewed | 2026-06-27 |
β·
β Error: Cycle: aws_security_group.app, aws_security_group.db
β΅
β Error: Cycle: aws_iam_role.lambda, aws_iam_policy.lambda,
β aws_iam_role_policy_attachment.lambda
Terraform orders operations by building a directed acyclic graph (DAG) from the references between resources. If those references form a loop β A depends on B and B depends (directly or transitively) on A β there is no valid order in which to create or destroy them, and Terraform refuses to proceed. The error lists the resource addresses that participate in the cycle.
- terraform (dependency graph builder)
medium β a configuration/graph error caught before any API call; nothing is changed. It can be subtle to untangle when the loop spans modules or implicit references.
- Two security groups that each reference the other's ID in a rule (the classic case).
- An explicit
depends_onthat points back at something already downstream. - A resource whose argument references an attribute of a resource that, in turn, references it.
- Module input/output wiring that loops (module A output feeds module B which feeds back into module A).
- A
lifecycle/replacement chain that creates a mutual dependency during destroy.
Each reference (aws_security_group.db.id inside aws_security_group.app)
creates a graph edge "app depends on db." A cycle means the graph cannot be
topologically sorted, so Terraform has no first node to act on. Security groups
are the canonical example because two groups commonly need to allow traffic from
each other, and inlining both rules forces a mutual reference. The fix is almost
always to break the edge by extracting the cross-reference into a standalone
resource that depends on both endpoints rather than coupling them directly.
# Triggers the cycle error and names the participating resources
terraform plan
# Emit the dependency graph to inspect the loop visually
terraform graph > graph.dot
# (render with: dot -Tsvg graph.dot -o graph.svg β read-only)
# Search for the mutual references that form the loop
grep -rn 'security_group' .$ terraform plan
Error: Cycle: aws_security_group.app, aws_security_group.db
$ grep -rn 'security_group' .
./main.tf:12: security_groups = [aws_security_group.db.id] # in app
./main.tf:24: security_groups = [aws_security_group.app.id] # in db
Seeing each group reference the other inside its inline rules confirms the mutual-reference cycle.
-
Break inline mutual rules into standalone
aws_security_group_rule(orvpc_security_group_ingress_rule) resources that reference both groups without the groups referencing each other:resource "aws_security_group" "app" {} resource "aws_security_group" "db" {} resource "aws_security_group_rule" "app_to_db" { type = "ingress" security_group_id = aws_security_group.db.id source_security_group_id = aws_security_group.app.id from_port = 5432 to_port = 5432 protocol = "tcp" }
-
Remove unnecessary
depends_onthat recreate the loop; let implicit references express ordering instead. -
For module loops, restructure so data flows one direction, or pass shared IDs in from the root.
terraform plan
# Expect: a normal plan with no "Cycle:" error and a clear create/update order.- Model bidirectional relationships (SG-to-SG, IAM role/policy) with separate rule/attachment resources from the start.
- Use
depends_onsparingly and only for true hidden dependencies. - Render
terraform graphin review when adding cross-resource references.
terraform Β· graph Β· dependencies Β· cycle Β· production