TerraformPilot

Troubleshooting

Fix Terraform Error - Cycle Detected in Resource Dependencies

Fix the Terraform cycle error when resources have circular dependencies. Covers dependency graphs, security group rules, module restructuring, and debugging.

LLuca Berton3 min read

Quick Answer

#

Two or more resources reference each other, creating a circular dependency Terraform cannot resolve. Break the cycle by extracting inline rules into separate resources, removing unnecessary depends_on, or restructuring your modules.

The Error

#

When running terraform plan or terraform validate, you see:

Error: Cycle: aws_security_group.a, aws_security_group.b
Error: Cycle: module.network, module.compute
Error: Cycle: aws_route53_record.www, aws_lb.main, aws_lb_listener.https

Terraform builds a directed acyclic graph (DAG) of all resources and their dependencies. When it detects a loop — A depends on B, and B depends on A — it cannot determine which to create first.

What Causes This Error

#

1. Security Groups Referencing Each Other

#

The most common cause. Two security groups where each allows traffic from the other:

# BAD — circular dependency
resource "aws_security_group" "app" {
  ingress {
    security_groups = [aws_security_group.db.id]  # depends on db
  }
}
 
resource "aws_security_group" "db" {
  ingress {
    security_groups = [aws_security_group.app.id]  # depends on app
  }
}

2. Unnecessary depends_on

#

Adding depends_on that creates a loop:

resource "aws_instance" "web" {
  depends_on = [aws_security_group.web]
}
 
resource "aws_security_group" "web" {
  # References the instance somehow
  tags = {
    Instance = aws_instance.web.id  # depends on web → CYCLE
  }
}

3. Modules That Reference Each Other

#
module "network" {
  source      = "./modules/network"
  instance_id = module.compute.instance_id  # depends on compute
}
 
module "compute" {
  source    = "./modules/compute"
  subnet_id = module.network.subnet_id  # depends on network → CYCLE
}

4. Output/Variable Chains

#

A chain of outputs and variables across modules that forms a circle when all dependencies are resolved.

How to Fix It

#

Solution 1: Use Separate Security Group Rules

#

Instead of inline ingress/egress blocks, use standalone aws_security_group_rule resources:

# Create security groups without inline rules
resource "aws_security_group" "app" {
  name   = "app-sg"
  vpc_id = var.vpc_id
}
 
resource "aws_security_group" "db" {
  name   = "db-sg"
  vpc_id = var.vpc_id
}
 
# Add rules as separate resources — no cycle
resource "aws_security_group_rule" "app_to_db" {
  type                     = "ingress"
  from_port                = 5432
  to_port                  = 5432
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.app.id
  security_group_id        = aws_security_group.db.id
}
 
resource "aws_security_group_rule" "db_to_app" {
  type                     = "ingress"
  from_port                = 8080
  to_port                  = 8080
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.db.id
  security_group_id        = aws_security_group.app.id
}

Solution 2: Visualize the Dependency Graph

#

Find exactly where the cycle is:

# Generate the graph
terraform graph | dot -Tpng > graph.png
 
# Or use text output
terraform graph 2>&1 | grep -E "->|cycle"
 
# Filter for specific resources
terraform graph | grep -E "(security_group|instance)" | dot -Tpng > filtered.png

If you don't have dot (Graphviz):

# Install Graphviz
sudo apt-get install graphviz   # Debian/Ubuntu
brew install graphviz           # macOS
 
# Or just read the text output
terraform graph 2>&1
# Look for bidirectional arrows between resources

Solution 3: Remove Unnecessary depends_on

#

Terraform automatically infers dependencies from attribute references. Explicit depends_on is rarely needed:

# BAD — depends_on is redundant here (subnet_id already creates dependency)
resource "aws_instance" "web" {
  subnet_id = aws_subnet.private.id
  depends_on = [aws_subnet.private]  # Remove this
}
 
# GOOD — implicit dependency through attribute reference
resource "aws_instance" "web" {
  subnet_id = aws_subnet.private.id  # Terraform knows this depends on the subnet
}

Only use depends_on when there's a hidden dependency Terraform can't detect (e.g., IAM policy propagation delay).

Solution 4: Restructure Module Dependencies

#

Break the cycle by passing values through a third module or restructuring:

# BAD — modules reference each other
module "network" {
  source      = "./modules/network"
  instance_id = module.compute.instance_id
}
module "compute" {
  source    = "./modules/compute"
  subnet_id = module.network.subnet_id
}
 
# GOOD — network creates first, compute depends on it, tags update separately
module "network" {
  source = "./modules/network"
}
 
module "compute" {
  source    = "./modules/compute"
  subnet_id = module.network.subnet_id
}
 
# Tag the network with instance info as a separate resource
resource "aws_ec2_tag" "subnet_instance" {
  resource_id = module.network.subnet_id
  key         = "Instance"
  value       = module.compute.instance_id
}

Solution 5: Use Data Sources to Break the Chain

#

If one side doesn't need to be a hard dependency:

# Instead of referencing the resource directly
# Use a data source for the existing resource
data "aws_security_group" "db" {
  filter {
    name   = "group-name"
    values = ["db-sg"]
  }
  depends_on = [aws_security_group.db]
}

Common Cycle Patterns

#
PatternCauseFix
SG ↔ SGInline ingress/egress referencing each otherUse aws_security_group_rule
Module ↔ ModuleCross-module attribute referencesRestructure or use a shared variable
Resource ↔ Resource via depends_onUnnecessary explicit dependencyRemove depends_on, use implicit refs
IAM Role ↔ Policy ↔ Instance ProfileRole attached to profile that references roleSeparate policy attachment
Route53 ↔ ALB ↔ Listener ↔ CertificateDNS validation for cert needs record, cert needs listenerUse aws_acm_certificate_validation

Troubleshooting Checklist

#
  1. ✅ Run terraform graph to visualize the cycle
  2. ✅ Are there inline security group rules referencing each other?
  3. ✅ Are there unnecessary depends_on blocks?
  4. ✅ Do modules pass outputs back and forth?
  5. ✅ Can you split the cycle by extracting a resource?
  6. ✅ Can you use a data source for one side of the dependency?

Prevention Tips

#
  • Avoid inline security group rules — always use aws_security_group_rule for cross-group references
  • Minimize depends_on — Terraform infers most dependencies automatically
  • Keep modules unidirectional — module A outputs to module B, never both ways
  • Run terraform validate often — catches cycles before plan
  • Use terraform graph during design — visualize dependencies before they become problems
#

Conclusion

#

Cycle errors mean Terraform found a circular dependency in your resource graph. The fix is always to break the circle: extract inline rules into standalone resources, remove unnecessary depends_on, or restructure your modules so dependencies flow in one direction. Use terraform graph to see exactly where the cycle is.

#cycle#dependencies#graph

Share this article