LearnDevOpsHands-On Labs
Troubleshooting Matrix/Error: Cycle: resources depend on each other
TerraformHigh Severity

Error: Cycle: resources depend on each other

Fix Terraform "Error: Cycle": run terraform graph to see it, then break the loop with separate security group rules, remove depends_on, or split modules.

Observed Error Signature / Log Pattern
Error: Cycle: aws_security_group.app, aws_security_group.db

Incident Overview

TerraformTerraform

Fix Terraform "Error: Cycle": run terraform graph to see it, then break the loop with separate security group rules, remove depends_on, or split modules.

hard fix·6 min read

By Vishvesh Patel·DevOps Engineer·Updated 21 September 2026

BASHDiagnostic / remediation command
Error: Cycle: aws_security_group.app, aws_security_group.db

Error: Cycle: module.vpc.aws_route_table.private, module.vpc.aws_nat_gateway.this, module.vpc.aws_subnet.public

Error: Cycle: aws_iam_role.lambda, aws_iam_role_policy.lambda, aws_lambda_function.api

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

• Read the cycle, it names every resource in the loop

The list is the loop in order. Two security groups referencing each other is the single most common instance, and the fix is to move the rules out of the group resources.

• Visualise the graph when the cycle is not obvious

terraform graph emits DOT. Piping it through dot -Tsvg gives a picture, but grepping for the resources named in the error is usually enough to see which attribute reference closes the loop.

• Break the loop by extracting the mutual reference into its own resource

Rules, attachments and associations exist as standalone resources precisely so that two objects can reference each other without the objects themselves forming a cycle.

Terraform builds a directed acyclic graph of resources and walks it. A cycle means A must exist before B and B must exist before A, which cannot be satisfied, so the plan fails before anything runs.

The error lists every resource in the loop:

BASHDiagnostic / remediation command
terraform plan -no-color 2>&1 | grep -A5 "Error: Cycle"

Pattern 1: Security groups referencing each other

By far the most common. The app group allows traffic to the database group, and the database group allows traffic from the app group:

Each group's definition depends on the other's id. Cycle.

The fix is to define the groups empty and attach the rules separately:

Now both groups are created first with no dependencies, then both rules. No cycle.

Note the newer aws_vpc_security_group_ingress_rule and aws_vpc_security_group_egress_rule replace the older aws_security_group_rule, and are worth preferring: one rule per resource makes plans readable and avoids the perpetual-diff problems the older resource had.

Do not mix inline blocks and rule resources on the same group. Terraform fights itself: the inline blocks describe the complete rule set, so it removes rules added by the separate resources on every apply, and the next apply adds them back. If you use rule resources, the group must have no inline ingress or egress blocks at all.

BASHDiagnostic / remediation command
resource "aws_security_group" "app" {
  name = "app"
  egress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.db.id]   # needs db
  }
}

resource "aws_security_group" "db" {
  name = "db"
  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]  # needs app
  }
}

Pattern 2: An unnecessary depends_on

The function legitimately depends on the role through role = .... The added depends_on points the other way and closes the loop.

Terraform infers dependencies from expression references automatically. An explicit depends_on is only needed for dependencies it cannot see, such as an IAM policy that must exist before an API call the provider makes implicitly. Adding it "to be safe" is how most of these cycles get created.

Remove it and let the reference do the work.

BASHDiagnostic / remediation command
resource "aws_iam_role" "lambda" {
  name               = "lambda-exec"
  assume_role_policy = data.aws_iam_policy_document.assume.json
  depends_on         = [aws_lambda_function.api]     # wrong direction
}

resource "aws_lambda_function" "api" {
  role = aws_iam_role.lambda.arn                      # genuinely needs the role
}

Pattern 3: Modules depending on each other

Terraform treats a module as a unit in the graph, so this is a cycle even if no individual resource pair is circular.

Usually the modules are drawn at the wrong boundary. Move the one resource that creates the back-reference out into the root, or into a third module that depends on both:

Dependencies flow one way and the cycle is gone.

BASHDiagnostic / remediation command
module "network" {
  source     = "./modules/network"
  cluster_id = module.cluster.id
}

module "cluster" {
  source    = "./modules/cluster"
  subnet_ids = module.network.subnet_ids
}

Seeing the graph

Needs Graphviz. For a large configuration the picture is unreadable, so narrow it:

In practice the error message names the whole loop, and reading the resources it lists for a mutual reference is faster than rendering anything.

BASHDiagnostic / remediation command
terraform graph -type=plan > graph.dot
dot -Tsvg graph.dot -o graph.svg

Cycles that only appear on destroy

Destroy reverses the graph, and a configuration that applies cleanly can have a cycle in reverse. create_before_destroy is the usual trigger, because it changes the ordering constraints:

When one resource in a dependency chain has it and another does not, the ordering can become unsatisfiable. The rule is that create_before_destroy has to propagate: if a resource has it, everything it depends on generally needs it too. Terraform's documentation is explicit that mixing them within a dependency chain causes exactly this.

BASHDiagnostic / remediation command
terraform plan -destroy

A checklist

Frequently Asked Questions

What causes a cycle error in Terraform?

Two or more resources that each need the other to exist first. Terraform builds a directed acyclic graph from the references in your expressions plus any explicit depends_on, and walks it in dependency order. A cycle makes that order impossible to compute, so the plan fails before anything is created. The error lists every resource in the loop, and the cause is almost always either a mutual attribute reference or a depends_on pointing against an existing reference.

How do I fix two security groups that reference each other?

Define both groups with no inline rules, then create the rules as separate aws_vpc_security_group_ingress_rule and aws_vpc_security_group_egress_rule resources. The groups are then created first with no dependencies on each other, and the rules afterwards, which breaks the loop. The important caveat is not to mix the two styles: a group with inline ingress or egress blocks treats those as the authoritative complete set, so it will remove rules created by separate resources on every apply.

Should I use depends_on to be safe?

No. Terraform infers dependencies from every reference in your expressions, so role = aws_iam_role.lambda.arn already creates the ordering. Adding depends_on on top of that is redundant at best, and when pointed the wrong way it creates exactly the cycle you are trying to avoid. Reserve it for dependencies Terraform genuinely cannot see, such as an IAM policy that must exist before a provider makes an implicit API call, and remove any you cannot justify.

Why does a cycle only appear during terraform destroy?

Because destroy walks the graph in reverse, and the reversed ordering constraints are not always satisfiable even when the forward ones are. The usual trigger is create_before_destroy, which changes how Terraform orders replacement. When one resource in a dependency chain has it set and the resources it depends on do not, the combined constraints can become impossible. The rule is to propagate it: if a resource uses create_before_destroy, everything it depends on generally needs it too.

How do I visualise the dependency graph?

terraform graph -type=plan writes the graph in Graphviz DOT format, which dot -Tsvg graph.dot -o graph.svg renders. For anything beyond a small configuration the full picture is too dense to read, so filter it first by piping through grep for the resource types named in the error. In practice the error message already lists the whole cycle, so scanning those specific resources for a mutual reference is usually faster than rendering a diagram.

Reference and practice

Learn the underlying concept

Other Terraform errors

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Get notified about new tutorials and features.

• Roadmaps

• Interview Prep

• Certifications

• Company

© 2026 DevOpsLesson. All rights reserved.

Need to practice this scenario?

Simulate this error condition and practice recovery commands safely in our in-browser terminal.

Open Terraform Sandbox