What's Actually Happening
Terraform has detected a circular dependency in your configuration. A cycle means resources depend on each other in a way that creates a loop, making it impossible for Terraform to determine which resource to create first. Terraform builds a dependency graph to plan operations, and cycles break this graph.
The Error You'll See
``` Error: Cycle: aws_instance.web, aws_security_group_rule.ingress, aws_security_group.web
Error: Graph error: Cycle detected
The graph contains a cycle that prevents Terraform from determining the correct order of operations. The cycle involves: - aws_instance.web - aws_security_group.web - aws_security_group_rule.ingress
Error: Module cycle: module.vpc -> module.compute -> module.vpc
Error: Cycle: data.aws_instance.existing, aws_instance.new (dependency chain) ```
Detailed cycle information:
``` Error: Cycle: aws_instance.web, aws_eip.web, aws_instance.web
Terraform cannot determine the correct order of operations because: aws_instance.web depends on aws_eip.web (via association) aws_eip.web depends on aws_instance.web (via instance_id)
This creates a circular dependency that cannot be resolved. ```
Why This Happens
Circular dependencies occur due to:
- 1.Direct mutual references - Two resources directly reference each other
- 2.Implicit dependencies - Resource A depends on B via depends_on, B depends on A via attribute
- 3.Transitive cycles - Long chain of dependencies that loops back
- 4.Module cycles - Modules referencing each other's outputs
- 5.Data source cycles - Data source depends on resource that needs the data
- 6.Self-referencing security groups - SG rules referencing the same SG
- 7.Network cycles - VPC, subnet, and instance forming a loop
- 8.Provider configuration cycles - Provider depends on resource for configuration
Step 1: Identify the Cycle Elements
Parse the error message:
```bash # Error shows cycle elements # Error: Cycle: resource1, resource2, resource3
# List each resource in the cycle terraform state list | grep -E 'resource1|resource2|resource3'
# Show configuration of each terraform console > aws_instance.web > aws_security_group.web ```
Visualize the dependency graph:
```bash # Generate graph terraform graph | dot -Tpng > graph.png
# Look for circular arrows # Or use graph viewer terraform graph > graph.dot # Open in graphviz ```
Step 2: Analyze Dependency Chain
Trace the specific dependencies:
```bash # For each resource in cycle, check its references grep -r "aws_instance.web" *.tf grep -r "aws_security_group.web" *.tf
# Or in terraform console terraform console > depends_on(aws_instance.web) > depends_on(aws_security_group.web) ```
Common dependency patterns:
```hcl # Example cycle: resource "aws_instance" "web" { # Depends on security_group vpc_security_group_ids = [aws_security_group.web.id] }
resource "aws_security_group" "web" { # Depends on instance via rule # This creates cycle }
resource "aws_security_group_rule" "ingress" { security_group_id = aws_security_group.web.id # Rule references instance's IP somehow } ```
Step 3: Fix Security Group Self-Reference
Common cycle pattern with security groups:
```hcl # WRONG - self-referencing cycle resource "aws_security_group" "web" { name = "web-sg"
ingress { from_port = 80 to_port = 80 protocol = "tcp" self = true # References itself description = "Allow traffic from instances with this SG" } }
# This can cause cycle if instance needs the SG ```
Fix with separate rules:
```hcl # CORRECT - separate resource resource "aws_security_group" "web" { name = "web-sg" # No inline rules }
resource "aws_security_group_rule" "self_ingress" { type = "ingress" security_group_id = aws_security_group.web.id from_port = 80 to_port = 80 protocol = "tcp" source_security_group_id = aws_security_group.web.id # Self reference in rule } ```
Step 4: Fix EIP and Instance Cycle
Common cycle with Elastic IPs:
```hcl # WRONG - circular dependency resource "aws_instance" "web" { # Instance needs EIP association }
resource "aws_eip" "web" { instance = aws_instance.web.id # EIP needs instance # Cycle: instance -> EIP -> instance }
# CORRECT - use separate association resource "aws_instance" "web" { # No EIP dependency }
resource "aws_eip" "web" { # Don't associate here domain = "vpc" }
resource "aws_eip_association" "web" { instance_id = aws_instance.web.id allocation_id = aws_eip.web.id # This breaks the cycle } ```
Step 5: Fix Data Source Cycles
Data source depending on resource being created:
```hcl # WRONG - data source needs resource that needs data data "aws_instance" "existing" { instance_id = aws_instance.new.id # Needs new instance }
resource "aws_instance" "new" { # Some attribute from data source ami = data.aws_instance.existing.ami # Cycle }
# CORRECT - separate the dependency data "aws_instance" "existing" { instance_id = "i-existing-id" # Use existing instance }
resource "aws_instance" "new" { ami = data.aws_instance.existing.ami } ```
Step 6: Fix Module Cycles
Modules referencing each other:
```hcl # WRONG - module cycle module "vpc" { source = "./modules/vpc" # Needs compute module's outputs subnet_cidrs = module.compute.subnet_cidrs }
module "compute" { source = "./modules/compute" # Needs vpc module's outputs vpc_id = module.vpc.vpc_id }
# Cycle: vpc -> compute -> vpc ```
Fix by restructuring:
```hcl # CORRECT - move shared data to root variable "subnet_cidrs" { default = ["10.0.1.0/24", "10.0.2.0/24"] }
module "vpc" { source = "./modules/vpc" subnet_cidrs = var.subnet_cidrs }
module "compute" { source = "./modules/compute" vpc_id = module.vpc.vpc_id } ```
Step 7: Use depends_on Carefully
Explicit dependencies can create cycles:
```hcl # WRONG - explicit depends_on cycle resource "aws_instance" "web" { depends_on = [aws_security_group.web] }
resource "aws_security_group" "web" { # Nothing should depend on this }
resource "aws_security_group_rule" "ingress" { security_group_id = aws_security_group.web.id depends_on = [aws_instance.web] # Creates cycle }
# CORRECT - remove inappropriate depends_on resource "aws_instance" "web" { vpc_security_group_ids = [aws_security_group.web.id] # Implicit dependency is correct }
resource "aws_security_group_rule" "ingress" { security_group_id = aws_security_group.web.id # No depends_on on instance } ```
Step 8: Fix Provider Configuration Cycles
Provider depends on resource for configuration:
```hcl # WRONG - provider needs resource provider "kubernetes" { host = aws_eks_cluster.main.endpoint # Needs cluster }
resource "aws_eks_cluster" "main" { # Provider needs this before it exists }
# Kubernetes resources need provider that needs cluster # Cycle ```
Fix with Terraform 0.14+ alias pattern:
```hcl # CORRECT - use conditional provider provider "kubernetes" { alias = "eks" host = data.aws_eks_cluster.main.endpoint }
data "aws_eks_cluster" "main" { name = aws_eks_cluster.main.name }
resource "aws_eks_cluster" "main" { name = "my-cluster" }
# Use aliased provider for cluster resources resource "kubernetes_config_map" "aws_auth" { provider = kubernetes.eks
depends_on = [aws_eks_cluster.main] } ```
Step 9: Break Cycles with lifecycle
Use create_before_destroy:
```hcl resource "aws_security_group" "web" { name = "web-sg"
lifecycle { create_before_destroy = true } }
# New SG created before old one destroyed # Can help with certain update cycles ```
Use ignore_changes for attributes that might cause cycles:
resource "aws_instance" "web" {
lifecycle {
ignore_changes = [
# Attributes modified by other resources
root_block_device,
]
}
}Step 10: Use Split Approach
Break cycle by splitting resources:
```bash # Split into multiple apply steps # First apply: base resources terraform apply -target=aws_vpc.main terraform apply -target=aws_subnet.public terraform apply -target=aws_security_group.web
# Second apply: dependent resources terraform apply -target=aws_instance.web terraform apply -target=aws_eip.web
# Or restructure configuration into separate files # vpc.tf -> compute.tf # Apply in sequence ```
Step 11: Common Cycle Patterns and Solutions
Network <-> Instance cycle:
``hcl
# If instance needs network that needs instance
# Separate into: network -> instance
# Remove reverse dependency
IAM <-> Resource cycle:
``hcl
# IAM role needs instance profile needs role
# Use: role -> profile -> resource
# Don't reference back
Data <-> Resource cycle:
``hcl
# Data source needs resource that needs data
# Change data source to reference pre-existing resource
# Or use separate terraform apply
Verify the Fix
After resolving cycles:
```bash # Validate configuration terraform validate
# Should show: Success! The configuration is valid.
# Run plan to verify no cycles terraform plan
# Should produce plan without cycle errors
# Generate graph to verify no loops terraform graph | dot -Tpng > graph.png # Check visually for cycles ```
Prevention Best Practices
Design dependency order correctly:
## Dependency Design Principles
1. Create resources in layers: network -> security -> compute -> application
2. Never reference child resources from parent configuration
3. Use outputs to pass data downstream, never upstream
4. Avoid depends_on unless absolutely necessary
5. Separate data sources from resources they depend onReview configuration for cycles:
```bash # Before apply, validate terraform validate
# Check graph terraform graph
# Look for mutual references between resources grep -E "aws_instance.*aws_eip|aws_eip.*aws_instance" *.tf ```
Use proper module structure:
```hcl # Modules should receive inputs from parent # Modules should provide outputs to parent # Modules should NOT reference other modules directly
# Parent controls flow: variable "vpc_id" {} module "compute" { vpc_id = var.vpc_id # From parent, not another module } ```