Terraform "Error: Cycle" In Resource Dependencies — How To Detect And Break Circular References
Terraform "Error: Cycle" in Resource Dependencies — How to Detect and Break Circular References
If you've spent any meaningful time with Terraform, you've probably encountered this at the worst possible moment:
Error: Cycle: aws_security_group.app, aws_security_group.db
Everything was working fine, you added one more resource or dependency, and now Terraform refuses to plan. The cycle error is one of those infuriating issues that feels cryptic at first but follows very predictable patterns once you know what to look for.
Let me walk you through how Terraform's dependency graph works, why cycles happen, and — most importantly — how to actually fix them.
Why Terraform Uses a Dependency Graph
Terraform builds a directed acyclic graph (DAG) to figure out the order in which to create, update, or destroy resources. Every reference like aws_instance.web.id or var.subnet_id creates an edge in that graph, pointing from the resource that needs information to the resource that provides it.
The "acyclic" part is critical. If Resource A depends on Resource B, and Resource B depends on Resource A, Terraform has no idea where to start. It can't create A without B existing, and it can't create B without A existing. That's a cycle, and Terraform stops dead.
Visualizing the Problem
Before we talk about fixes, let's look at how to actually see the cycle. Terraform's error message names the resources involved, but sometimes the chain is longer than two resources and the message alone doesn't give you enough context.
Use terraform graph to render the dependency graph:
terraform graph | dot -Tsvg > graph.svg
You'll need Graphviz installed (brew install graphviz or apt-get install graphviz). Open the SVG in a browser and you'll see a visual representation of your resource dependencies. Cycles usually jump out immediately — look for arrows that form a loop.
If your graph is massive, you can filter it:
terraform graph | grep -A5 "security_group"
Not perfect, but useful for narrowing down where to look.
The Most Common Cycle Patterns
Pattern 1: Security Group Mutual References
This is probably the single most common source of cycle errors in AWS configurations. You have an application security group and a database security group, and you want each to allow traffic from the other:
# This will cause a cycle
resource "aws_security_group" "app" {
name = "app-sg"
egress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.db.id] # References db
}
}
resource "aws_security_group" "db" {
name = "db-sg"
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id] # References app
}
}
App depends on DB. DB depends on App. Classic cycle.
The fix: Use aws_security_group_rule resources to separate the rule definitions from the group definitions:
resource "aws_security_group" "app" {
name = "app-sg"
# No inline rules referencing db
}
resource "aws_security_group" "db" {
name = "db-sg"
# No inline rules referencing app
}
# Rules are defined separately, after both groups exist
resource "aws_security_group_rule" "app_to_db" {
type = "egress"
from_port = 5432
to_port = 5432
protocol = "tcp"
source_security_group_id = aws_security_group.db.id
security_group_id = aws_security_group.app.id
}
resource "aws_security_group_rule" "db_from_app" {
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
}
By splitting the rules into their own resources, both security groups can be created independently. The rule resources then reference both groups, but the groups themselves have no dependency on each other.
Pattern 2: Module Circular Dependencies
This is trickier because the cycle spans module boundaries and can be harder to spot. Imagine a networking module and an application module:
# main.tf
module "networking" {
source = "./modules/networking"
app_server_id = module.application.instance_id # References application
}
module "application" {
source = "./modules/application"
subnet_id = module.networking.subnet_id # References networking
}
Networking needs application, application needs networking. Same problem, bigger scope.
The fix: Restructure so that foundational infrastructure flows in one direction. Networking should know nothing about your application layer. Pass outputs explicitly and in one direction:
module "networking" {
source = "./modules/networking"
# Networking should be self-contained
}
module "application" {
source = "./modules/application"
subnet_id = module.networking.subnet_id
sg_id = module.networking.app_security_group_id
}
If you genuinely need the application's attributes in your networking module, that's usually a sign that the module boundaries are wrong. Consider merging them or extracting a third module that handles the integration.
Pattern 3: depends_on Misuse
The depends_on meta-argument is a blunt instrument, and it's possible to create cycles with it even when the resource references themselves look fine:
resource "aws_iam_role" "lambda_role" {
name = "lambda-execution-role"
# ...
depends_on = [aws_lambda_function.processor] # Explicit cycle!
}
resource "aws_lambda_function" "processor" {
role = aws_iam_role.lambda_role.arn # Implicit dependency
# ...
}
The lambda function implicitly depends on the role (through role = aws_iam_role.lambda_role.arn), and the role explicitly depends on the function via depends_on. This is almost always a mistake — depends_on should be reserved for dependencies that Terraform can't automatically detect, like a resource that reads from a database that another resource populates.
The fix: Remove the spurious depends_on. If you actually need the IAM role to wait for something the Lambda creates, rethink the architecture. Chicken-and-egg problems usually indicate a design issue.
Detecting Hidden Cycles with Targeted Plans
Sometimes your cycle only manifests under certain conditions — like when you're trying to destroy resources or when you're applying a specific change. Use targeted plans to isolate the problem:
# Try planning a specific resource to see its dependency chain
terraform plan -target=aws_security_group.app
# Or check what depends on a resource you're trying to change
terraform state show aws_security_group.db
The state show command will list the resource's current attributes, and you can manually trace which other resources reference those attributes.
The Nuclear Option: replace to Break State Cycles
Occasionally (and I mean rarely), you'll have a cycle that only exists in state due to configuration drift or a previous botched apply. In these cases, you can use terraform state commands to untangle things:
# Remove a resource from state to break the cycle temporarily
terraform state rm aws_security_group.db
# Import it back once the cycle is resolved
terraform import aws_security_group.db sg-0abc123def456
I want to be very clear: this is a last resort. Removing resources from state means Terraform no longer tracks them. If you then run apply, Terraform will try to create a new resource with the same name, which will likely fail or create duplicates. Only do this if you understand exactly what you're doing and you have a plan to reconcile the state.
Practical Checklist for Diagnosing Cycles
When you hit a cycle error, work through this list:
-
Read the error message carefully. Terraform lists all resources involved in the cycle. Write them down.
-
Run
terraform graph | dot -Tsvg > graph.svgand visually inspect the dependency chain. -
Search for inline rules or blocks that reference other managed resources. Security groups with inline ingress/egress rules are a common culprit.
-
Audit your
depends_onusage. Every explicitdepends_onshould have a documented reason. Remove any that are speculative or cargo-culted. -
Check module outputs and inputs. Draw out which module feeds which. If you have a loop in that diagram, you have your answer.
-
Look for cross-resource references in unexpected places — things like
tags,nameattributes, ordescriptionfields that reference another resource's output. These create real dependencies even if they look benign.
Preventing Cycles Before They Happen
The best fix is not introducing the cycle in the first place. A few habits that help:
Separate definition from configuration. Resources like security groups, IAM roles, and network ACLs should be defined in one pass without referencing sibling resources. Attach rules, policies, and associations in separate resources afterward.
Think in layers. Foundational infrastructure (networking, IAM) should never depend on application-layer resources. Information flows upward from infrastructure to application, not the other way around.
Review terraform graph output during code review. Make it part of your pipeline. You can add a CI step that generates the graph and fails if there are obvious issues — though detecting cycles programmatically from the DOT output is non-trivial, the visualization alone is worth it for peer review.
# Add to your CI pipeline
terraform graph > /dev/null || echo "Graph generation failed — possible cycle"
Actually, if Terraform can't generate the plan, terraform graph will also fail. So running this in CI gives you a cheap early warning.
Wrapping Up
Cycle errors feel mysterious because the error message doesn't tell you why the cycle exists, just which resources are involved. Once you understand that every resource reference is a directed edge in a graph, and that Terraform needs that graph to be acyclic to do its job, the fix becomes obvious: find where the circle closes and break it.
For most real-world cycles, the solution is almost always the same: separate resource definition from resource relationship. Create the resources independently, then create a third resource that expresses the relationship between them.
The security group rule pattern I showed earlier isn't just a workaround — it's actually the correct abstraction. Security groups are identities. Rules are relationships. Keeping them separate is better design, and it happens to also be what Terraform needs to build a valid dependency graph.
Was this article helpful?
CI/CD Engineering Lead
Automation evangelist who believes no deployment should require a human. I write pipelines, break pipelines, and write about both. Code-first, always.
Related Articles
Terraform "Error: No Valid Credential Sources Found" On AWS — Fixing Authentication And Provider Configuration Issues
If you've spent any time with Terraform and AWS, you've almost certainly run into this gem at some point: It's one of those errors that looks simple but ca...
Terraform Error: Invalid For_each Argument Must Be A Map Or Set Of Strings - Complete Fix Guide
If you've been working with Terraform's `for_each` argument, you've probably hit this error at least once. Don't worry – it's one of those frustrating but...
Fix Terraform 'Error Acquiring the State Lock' with DynamoDB
Resolve Terraform DynamoDB state lock errors caused by permission issues, missing tables, or misconfigured backends.
Fix Terraform State Lock Stuck After Failed Apply
Safely unlock Terraform state when a lock is stuck after a crashed apply, CI timeout, or interrupted operation.
Terraform Dynamic Blocks For Managing Variable Infrastructure Resources
If you've been working with Terraform for a while, you've probably hit that wall where you need to create resources with configurations that vary based on...
Writing Reusable Terraform Modules: A Practical Guide
Learn how to write Terraform modules that are actually reusable — with input validation, sensible defaults, and clean interfaces across environments.
More in Terraform
View all →Terraform CLI: Cheat Sheet
Terraform CLI cheat sheet with commands organized by workflow — init, plan, apply, destroy, state manipulation, imports, and workspace management.
Terraform from Zero to Production: Project Structure, Modules, State, and CI/CD
Build production-grade Terraform infrastructure — project structure, module design, state management, testing, and CI/CD pipeline integration.
Testing Terraform with Terratest: A Practical Guide
How to write unit and integration tests for Terraform modules using Terratest — because untested infrastructure is a liability.
Terraform Module Design Patterns for Large Teams
Battle-tested Terraform module patterns for teams — from file structure to versioning to composition. If it's not in code, it doesn't exist.