Fix Terraform "Error: Cycle" Dependency Error

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:

text
A depends on B
B depends on A

Nothing 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.

NOTE
Run terraform init in each new directory. Optional Graphviz rendering is covered in terraform graph; this article focuses on diagnosing cycles, not DOT syntax.

What a Terraform cycle means

Terraform builds a directed dependency graph from:

  • Attribute references (resource.b.output)
  • depends_on meta-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:

text
module.stack.terraform_data.db  →  db
module.stack.terraform_data.app →  app

For 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.

bash
mkdir -p ~/terraform-labs/terraform-cycle-error/errors/direct-mutual-ref
cd ~/terraform-labs/terraform-cycle-error/errors/direct-mutual-ref

Write the circular configuration:

bash
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
}
EOF

Initialize and validate to surface the cycle:

bash
terraform init -input=false
output
Terraform has been successfully initialized!

With providers ready, validate walks the dependency graph and should report the cycle:

bash
terraform validate -no-color
output
Error: Cycle: terraform_data.alpha, terraform_data.beta

Use 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:

bash
terraform graph -type=plan -draw-cycles | grep -E 'color = "red"|terraform_data\.(alpha|beta)'
output
"[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:

bash
terraform graph -type=plan -draw-cycles | dot -Tsvg > cycle-graph.svg

Dense 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:

hcl
resource "terraform_data" "alpha" {
  input = "seed"
}

resource "terraform_data" "beta" {
  input = terraform_data.alpha.output
}

Copy the corrected wiring to a sibling directory:

bash
mkdir -p ~/terraform-labs/terraform-cycle-error/fixes/direct-mutual-ref
cd ~/terraform-labs/terraform-cycle-error/fixes/direct-mutual-ref

Write the one-direction configuration:

bash
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
}
EOF

Initialize and confirm the refactored graph validates:

bash
terraform init -input=false && terraform validate -no-color
output
Success! The configuration is valid.

A clean plan confirms Terraform can order both resources:

bash
terraform plan -no-color
output
Plan: 2 to add, 0 to change, 0 to destroy.

Values now flow in one direction: alphabeta.


Fix module dependency cycles

Modules do not isolate you from cycles — resources inside a child module can still reference each other in a loop.

bash
mkdir -p ~/terraform-labs/terraform-cycle-error/errors/module-cycle/modules/stack
cd ~/terraform-labs/terraform-cycle-error/errors/module-cycle

Child module with mutual references:

bash
cat > modules/stack/main.tf <<'EOF'
resource "terraform_data" "app" {
  input = terraform_data.db.output
}

resource "terraform_data" "db" {
  input = terraform_data.app.output
}
EOF

Root module calls the child:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

module "stack" {
  source = "./modules/stack"
}
EOF

Initialize the root module first:

bash
terraform init -input=false

With the child module on disk, validate reports the internal cycle:

bash
terraform validate -no-color
output
Error: Cycle: module.stack.terraform_data.db, module.stack.terraform_data.app

Introduce a local bootstrap value so app no longer waits on db for its input:

hcl
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:

bash
mkdir -p ~/terraform-labs/terraform-cycle-error/fixes/module-cycle/modules/stack
cd ~/terraform-labs/terraform-cycle-error/fixes/module-cycle

Child module with one-direction flow:

bash
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
}
EOF

Root module calls the child:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

module "stack" {
  source = "./modules/stack"
}
EOF

Initialize and confirm the refactored module validates:

bash
terraform init -input=false && terraform validate -no-color
output
Success! The configuration is valid.

appdb 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.

bash
mkdir -p ~/terraform-labs/terraform-cycle-error/errors/depends-on-cycle
cd ~/terraform-labs/terraform-cycle-error/errors/depends-on-cycle

Three resources depend on each other explicitly:

bash
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]
}
EOF

Initialize, then ask validate to walk the explicit dependency ring:

bash
terraform init -input=false

The depends_on edges form a ring even though inputs are static strings:

bash
terraform validate -no-color
output
Error: Cycle: terraform_data.b, terraform_data.a, terraform_data.c

Remove the redundant depends_on blocks — these resources do not pass values to each other:

hcl
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:

bash
mkdir -p ~/terraform-labs/terraform-cycle-error/fixes/depends-on-cycle
cd ~/terraform-labs/terraform-cycle-error/fixes/depends-on-cycle

Write the configuration without redundant depends_on blocks:

bash
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"
}
EOF

Initialize and confirm the refactored graph plans three independent creates:

bash
terraform init -input=false && terraform validate -no-color && terraform plan -no-color
output
Success! 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:

bash
cd ~/terraform-labs/terraform-cycle-error/fixes/direct-mutual-ref

Confirm configuration is valid:

bash
terraform validate -no-color
output
Success! The configuration is valid.

Default graph should show a single direction edge:

bash
terraform graph
output
digraph G {
  ...
  "terraform_data.beta" -> "terraform_data.alpha";
}

beta depends on alpha; no arrow points back.

Confirm plan is clean:

bash
terraform plan -no-color
output
Plan: 2 to add, 0 to change, 0 to destroy.

Destroy lab resources when finished:

bash
terraform destroy -auto-approve -input=false 2>/dev/null || true

Prevent dependency cycles

  • Prefer attribute references over depends_on when values actually flow between resources
  • Reserve depends_on for ordering Terraform cannot infer from expressions
  • Sketch module input/output paths before wiring peer modules bidirectionally
  • Run terraform validate in CI before plan on every pull request
  • Use terraform graph -type=plan -draw-cycles when 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


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.


Frequently Asked Questions

1. What does Error Cycle mean in Terraform?

Terraform found a circular dependency — resource or module A depends on B while B depends on A, directly or through a chain. No valid create or update order exists until you remove or restructure at least one dependency edge.

2. How do I find a Terraform dependency cycle?

Read the Cycle error line for resource addresses, then run terraform graph -type=plan -draw-cycles and look for red edges. Trim long module paths to the repeating nodes in the loop before you refactor.

3. Can depends_on cause a cycle without attribute references?

Yes. depends_on creates explicit dependency edges even when no attribute is passed between resources. A ring of depends_on entries with no data references still forms a cycle.

4. Should I add more depends_on to fix a cycle?

No. More depends_on usually adds edges and can make cycles worse. Break the loop by removing a reference, introducing a local or variable for a shared value, or restructuring so values flow in one direction.

5. Does terraform graph always show cycles clearly?

On small configurations, -draw-cycles highlights red edges in the plan graph. Large estates produce dense DOT output where the loop is harder to spot — use the error address list first, then graph a focused subdirectory or module.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)