Fix Terraform "Error: Cycle detected in resource dependencies"

intermediate๐Ÿ—๏ธ Terraform2026-03-25| Terraform 0.13+ / 1.x, any OS (Linux, macOS, Windows), any cloud provider (AWS, GCP, Azure)

Error Message

Error: Cycle detected in resource dependencies
#terraform#cycle#dependency#graph

The Error

Error: Cycle detected in resource dependencies

  aws_security_group.web (expand) -> aws_instance.app (expand) -> aws_security_group.web (expand)

Two or more resources are depending on each other in a circle. Resource A needs Resource B to exist first. But Resource B also needs Resource A. Neither can go first โ€” so Terraform refuses to apply.

Why This Happens

Before applying any changes, Terraform builds a directed acyclic graph (DAG) of all your resources. Each attribute reference adds an edge. If those edges loop back on themselves, the graph is no longer acyclic โ€” and the plan fails immediately.

Common causes:

  • Two resources each referencing the other's output (e.g., cross-referencing security_group_id and private_ip)
  • A depends_on chain that accidentally loops back to its origin
  • Two modules passing outputs back and forth to each other
  • A resource referencing an attribute of itself (less obvious than it sounds)

Step 1: Read the Cycle Path in the Error

Terraform always prints the exact cycle path. Read it before doing anything else:

Error: Cycle detected in resource dependencies

  aws_security_group.web (expand)
  -> aws_instance.app (expand)
  -> aws_security_group.web (expand)

The loop here is clear: aws_security_group.web โ†’ aws_instance.app โ†’ back to aws_security_group.web. That's your starting point. Go straight to those two resources in your code.

Step 2: Visualize the Full Dependency Graph

For complex configs with many resources, a visual graph saves a lot of hunting:

terraform graph | dot -Tsvg > graph.svg

Open graph.svg in any browser. You're looking for arrows pointing in both directions between two nodes. That bidirectional edge is your cycle. Install Graphviz first if you don't have it:

# Ubuntu/Debian
sudo apt install graphviz

# macOS
brew install graphviz

Step 3: Identify the Circular Reference in Code

Here's the classic broken pattern โ€” a security group and an EC2 instance that each need the other to exist first:

resource "aws_security_group" "web" {
  name = "web-sg"

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    # BAD: referencing aws_instance creates a cycle
    cidr_blocks = ["${aws_instance.app.private_ip}/32"]
  }
}

resource "aws_instance" "app" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"
  # BAD: this also references the security group above
  vpc_security_group_ids = [aws_security_group.web.id]
}

The security group needs the instance's private IP. The instance needs the security group's ID. Neither can be created first. Terraform is stuck.

Step 4: Break the Cycle

Pick the option that fits your situation.

Option A: Remove the bidirectional reference

Most of the time, one of the two references isn't strictly necessary. In the example above, does the security group really need to lock ingress to a specific instance IP? Usually not. Use a CIDR range instead:

resource "aws_security_group" "web" {
  name = "web-sg"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    # FIXED: static CIDR, no reference to aws_instance
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "app" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"
  vpc_security_group_ids = [aws_security_group.web.id]
}

Option B: Use aws_security_group_rule (separate resource)

Sometimes you genuinely need both resources to know about each other. The trick: create the security group first with no ingress rules, then add the rule as a standalone resource after the instance exists.

resource "aws_security_group" "web" {
  name = "web-sg"
  # No inline ingress rules here
}

resource "aws_instance" "app" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"
  vpc_security_group_ids = [aws_security_group.web.id]
}

# This resource depends on both, but neither depends on it โ€” no cycle
resource "aws_security_group_rule" "allow_instance" {
  type              = "ingress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = ["${aws_instance.app.private_ip}/32"]
  security_group_id = aws_security_group.web.id
}

The rule resource sits downstream of both. It depends on them โ€” they don't depend on it. Cycle broken.

Option C: Remove an unnecessary depends_on

Explicit depends_on blocks are a common source of accidental cycles. Terraform already tracks implicit dependencies through attribute references โ€” you don't need to declare them again:

# Check if this depends_on is creating a loop
resource "aws_s3_bucket_policy" "example" {
  bucket = aws_s3_bucket.main.id
  policy = data.aws_iam_policy_document.example.json

  # The bucket reference above already creates the dependency.
  # This explicit depends_on is redundant โ€” remove it.
  depends_on = [aws_s3_bucket.main]
}

Only use depends_on when there's no attribute reference between two resources but one still needs the other to exist first.

Option D: Refactor into separate modules

Cycles that span two modules are an architectural problem. The pattern: module A outputs something that module B needs, and module B outputs something that module A needs. The fix is to extract the shared resource into a third module that both consume:

# Instead of module A outputting to module B and module B outputting back to A,
# create a third "shared" module that owns the shared resource
module "network" {
  source = "./modules/network"
  # owns VPC, subnets, security groups
}

module "compute" {
  source = "./modules/compute"
  sg_id  = module.network.web_sg_id  # one-way dependency only
}

Step 5: Verify the Fix

Run terraform validate first, then terraform plan:

terraform validate
# Expected: Success! The configuration is valid.

terraform plan
# Should produce a clean plan with no cycle errors

Still seeing a cycle error? Re-read the path โ€” it may now point to a different pair of resources. Repeat Steps 1โ€“4 for each new cycle until the plan is clean.

Tips to Avoid This in the Future

  • Don't mix inline rules with separate rule resources. If you're using aws_security_group_rule resources, remove all ingress and egress blocks from the parent aws_security_group. Mixing both causes conflicts and cycles.
  • Sketch the dependency direction before writing code. A quick diagram takes two minutes. Arrows should always flow one way between any two resources โ€” never both ways.
  • Run terraform graph as you add resources. Catching a cycle at resource #5 is much easier than untangling it at resource #50.
  • Be careful with module-level depends_on. It creates an implicit dependency on every single resource inside that module โ€” not just the ones you intended. That broad sweep can pull in unexpected cycles.

Related Error Notes