| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user; sudo for optional Graphviz install |
| Scope | Troubleshooting Terraform Error Cycle — mutual resource references, module dependency loops, depends_on-only cycles, reading cycle addresses, terraform graph with -draw-cycles, breaking cycles with locals and one-direction value flow, and validate-plan verification. Does not cover full graphviz tutorials or provider configuration cycle edge cases in depth. |
| Related guides | Terraform resource dependencies terraform graph terraform plan Terraform troubleshooting Terraform modules |
Error: Cycle means Terraform cannot order your resources because dependencies loop back on themselves:
A depends on B
B depends on ANothing is misspelled — the graph has no valid topological sort. terraform validate and terraform plan both fail until you break at least one edge in the loop.
Each scenario uses its own directory under ~/terraform-labs/terraform-cycle-error/. Examples use built-in terraform_data only.
What a Terraform cycle means
Terraform builds a directed dependency graph from:
- Attribute references (
resource.b.output) depends_onmeta-arguments- Module calls and provider configuration edges
A cycle exists when following dependency arrows eventually returns to a node you already visited. Terraform lists every address in the loop, for example Error: Cycle: terraform_data.alpha, terraform_data.beta.
The resource order in the message can differ between validate and plan — the same nodes are still in the cycle.
Find the resources in the cycle
Start with the error line. Strip module prefixes mentally to see the chain:
module.stack.terraform_data.db → db
module.stack.terraform_data.app → appFor a three-node depends_on ring, expect Error: Cycle: terraform_data.a, terraform_data.c, terraform_data.b — note the order can shuffle while the same three addresses stay in the loop.
Draw the loop on paper: a → b → c → a. That sketch tells you which edge to remove before you edit HCL.
Long module paths do not change the fix — you still break one dependency direction inside the module or at the root call site.
Fix mutual resource references
The most common cycle is two resources each reading the other's output.
mkdir -p ~/terraform-labs/terraform-cycle-error/errors/direct-mutual-ref
cd ~/terraform-labs/terraform-cycle-error/errors/direct-mutual-refWrite the circular configuration:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "alpha" {
input = terraform_data.beta.output
}
resource "terraform_data" "beta" {
input = terraform_data.alpha.output
}
EOFInitialize and validate to surface the cycle:
terraform init -input=falseTerraform has been successfully initialized!With providers ready, validate walks the dependency graph and should report the cycle:
terraform validate -no-colorError: Cycle: terraform_data.alpha, terraform_data.betaUse terraform graph to investigate
The default terraform graph output is simplified. For cycle debugging, use an operation graph with -draw-cycles on the cyclic configuration you just validated:
Highlight red cycle edges in the plan graph:
terraform graph -type=plan -draw-cycles | grep -E 'color = "red"|terraform_data\.(alpha|beta)'"[root] terraform_data.alpha (expand)" -> "[root] terraform_data.beta (expand)" [color = "red", penwidth = "2.0"]
"[root] terraform_data.beta (expand)" -> "[root] terraform_data.alpha (expand)" [color = "red", penwidth = "2.0"]Red edges mark the loop. On large configurations the DOT stream is noisy — grep for color = "red" or render a subgraph after you know which module to inspect. Editing the DOT file does not fix configuration; change HCL and regenerate.
With Graphviz installed, pipe to SVG for a visual check:
terraform graph -type=plan -draw-cycles | dot -Tsvg > cycle-graph.svgDense graphs still require the error address list as your anchor — visualization supplements reading, it does not replace it.
Break the loop by giving one side a value that does not depend on the other. Seed alpha with a static input; let beta consume alpha.output only:
resource "terraform_data" "alpha" {
input = "seed"
}
resource "terraform_data" "beta" {
input = terraform_data.alpha.output
}Copy the corrected wiring to a sibling directory:
mkdir -p ~/terraform-labs/terraform-cycle-error/fixes/direct-mutual-ref
cd ~/terraform-labs/terraform-cycle-error/fixes/direct-mutual-refWrite the one-direction configuration:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "alpha" {
input = "seed"
}
resource "terraform_data" "beta" {
input = terraform_data.alpha.output
}
EOFInitialize and confirm the refactored graph validates:
terraform init -input=false && terraform validate -no-colorSuccess! The configuration is valid.A clean plan confirms Terraform can order both resources:
terraform plan -no-colorPlan: 2 to add, 0 to change, 0 to destroy.Values now flow in one direction: alpha → beta.
Fix module dependency cycles
Modules do not isolate you from cycles — resources inside a child module can still reference each other in a loop.
mkdir -p ~/terraform-labs/terraform-cycle-error/errors/module-cycle/modules/stack
cd ~/terraform-labs/terraform-cycle-error/errors/module-cycleChild module with mutual references:
cat > modules/stack/main.tf <<'EOF'
resource "terraform_data" "app" {
input = terraform_data.db.output
}
resource "terraform_data" "db" {
input = terraform_data.app.output
}
EOFRoot module calls the child:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
module "stack" {
source = "./modules/stack"
}
EOFInitialize the root module first:
terraform init -input=falseWith the child module on disk, validate reports the internal cycle:
terraform validate -no-colorError: Cycle: module.stack.terraform_data.db, module.stack.terraform_data.appIntroduce a local bootstrap value so app no longer waits on db for its input:
locals {
bootstrap = "seed"
}
resource "terraform_data" "app" {
input = local.bootstrap
}
resource "terraform_data" "db" {
input = terraform_data.app.output
}Copy the corrected module to a sibling directory:
mkdir -p ~/terraform-labs/terraform-cycle-error/fixes/module-cycle/modules/stack
cd ~/terraform-labs/terraform-cycle-error/fixes/module-cycleChild module with one-direction flow:
cat > modules/stack/main.tf <<'EOF'
locals {
bootstrap = "seed"
}
resource "terraform_data" "app" {
input = local.bootstrap
}
resource "terraform_data" "db" {
input = terraform_data.app.output
}
EOFRoot module calls the child:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
module "stack" {
source = "./modules/stack"
}
EOFInitialize and confirm the refactored module validates:
terraform init -input=false && terraform validate -no-colorSuccess! The configuration is valid.app → db ordering is now valid. Indirect cycles can also form when root module outputs feed back into module inputs through data sources — trace the full reference chain when the error spans modules. See Terraform modules for call patterns.
Fix unnecessary depends_on cycles
HashiCorp recommends explicit depends_on only for hidden dependencies that cannot be expressed through normal references. A ring of depends_on entries creates a cycle even when input values are static strings.
mkdir -p ~/terraform-labs/terraform-cycle-error/errors/depends-on-cycle
cd ~/terraform-labs/terraform-cycle-error/errors/depends-on-cycleThree resources depend on each other explicitly:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "a" {
input = "a"
depends_on = [terraform_data.b]
}
resource "terraform_data" "b" {
input = "b"
depends_on = [terraform_data.c]
}
resource "terraform_data" "c" {
input = "c"
depends_on = [terraform_data.a]
}
EOFInitialize, then ask validate to walk the explicit dependency ring:
terraform init -input=falseThe depends_on edges form a ring even though inputs are static strings:
terraform validate -no-colorError: Cycle: terraform_data.b, terraform_data.a, terraform_data.cRemove the redundant depends_on blocks — these resources do not pass values to each other:
resource "terraform_data" "a" {
input = "a"
}
resource "terraform_data" "b" {
input = "b"
}
resource "terraform_data" "c" {
input = "c"
}Copy the corrected configuration to a sibling directory:
mkdir -p ~/terraform-labs/terraform-cycle-error/fixes/depends-on-cycle
cd ~/terraform-labs/terraform-cycle-error/fixes/depends-on-cycleWrite the configuration without redundant depends_on blocks:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "a" {
input = "a"
}
resource "terraform_data" "b" {
input = "b"
}
resource "terraform_data" "c" {
input = "c"
}
EOFInitialize and confirm the refactored graph plans three independent creates:
terraform init -input=false && terraform validate -no-color && terraform plan -no-colorSuccess! The configuration is valid.
Plan: 3 to add, 0 to change, 0 to destroy.When a hidden dependency truly exists (for example a local-exec provisioner that needs another resource to exist first), use one depends_on on the consumer only — not a chain that loops back.
Break the cycle correctly
Do not add more depends_on to "fix" a cycle. Pick a pattern that removes one edge:
| Pattern | When to use |
|---|---|
Static or local seed value |
One resource only needs a starting value, not peer output |
| One-direction reference | Data should flow A → B → C, never back |
| Intermediate resource | Split shared computation into a third node both sides read |
Remove redundant depends_on |
Implicit refs already express order, or no real dependency exists |
| Separate root module | Lifecycle or state boundaries genuinely differ |
Provider and module configuration can create indirect cycles when provider aliases, configuration_aliases, and module providers maps cross-reference. Those cases are rarer — see Provider configuration not present fixes when the error mentions provider configuration instead of resources.
Verify the fix
After refactoring, run the standard checks in a fix directory:
cd ~/terraform-labs/terraform-cycle-error/fixes/direct-mutual-refConfirm configuration is valid:
terraform validate -no-colorSuccess! The configuration is valid.Default graph should show a single direction edge:
terraform graphdigraph G {
...
"terraform_data.beta" -> "terraform_data.alpha";
}beta depends on alpha; no arrow points back.
Confirm plan is clean:
terraform plan -no-colorPlan: 2 to add, 0 to change, 0 to destroy.Destroy lab resources when finished:
terraform destroy -auto-approve -input=false 2>/dev/null || truePrevent dependency cycles
- Prefer attribute references over
depends_onwhen values actually flow between resources - Reserve
depends_onfor ordering Terraform cannot infer from expressions - Sketch module input/output paths before wiring peer modules bidirectionally
- Run
terraform validatein CI before plan on every pull request - Use
terraform graph -type=plan -draw-cycleswhen the error lists more than two addresses
Diagnostic checklist
| Symptom | Likely cause | Fix |
|---|---|---|
| Two resources in Cycle message | Mutual output references |
Seed one side with local or static value |
module.* addresses in Cycle |
Loop inside child module | Restructure module internals one direction |
| Three or more resources, static inputs | depends_on ring |
Remove unnecessary depends_on |
| Cycle after provider refactor | Provider/module config cross-deps | Trace providers map and aliases |
| Graph has red edges both ways | Confirmed mutual edge | Remove one reference direction |
References
- Resource dependencies — HashiCorp Developer
- depends_on meta-argument — HashiCorp Developer
- terraform graph command — HashiCorp Developer
Summary
Error: Cycle means Terraform's dependency graph has a loop — A depends on B and B depends on A, directly or through a chain. The error lists every address in the loop; trim module prefixes to see which edge to break.
Mutual attribute references are the everyday case: seed one resource with a local or static value and let the other consume it one way. Module cycles follow the same rule inside modules/. Rings of depends_on without data references are also cycles — remove explicit dependencies Terraform does not need.
Use terraform graph -type=plan -draw-cycles to highlight red edges, but treat the error message as primary on large configs. After refactoring, terraform validate and terraform plan should succeed, and the default graph should show arrows in one direction only.
For implicit versus explicit dependencies in depth, continue with Terraform resource dependencies. For graph rendering workflows, see terraform graph.

