How to Fix Terraform 'cycle detected' Error in depends_on Dependencies
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–30 mins
TL;DR
- What broke: Terraform detected a circular reference in the resource dependency graph — resource A depends on B which depends back on A (directly or transitively).
applyandplanboth abort. - How to fix it: Identify the cycle path from the error output, remove redundant
depends_onentries that duplicate implicit references, and restructure resources that have genuine bidirectional dependencies by extracting a shared intermediary. - Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your
.tffiles and get a corrected dependency graph without sending your config to any external server.
The Incident (What Does the Error Mean?)
Raw error output from terraform apply or terraform plan:
Error: Cycle: aws_iam_role.app_role, aws_iam_instance_profile.app_profile, aws_launch_template.app_lt
Terraform detected a dependency cycle in the following resources:
aws_iam_role.app_role
aws_iam_instance_profile.app_profile
aws_launch_template.app_lt
Terraform builds a directed acyclic graph (DAG) before executing any operation. If any edge in that graph creates a loop, the entire graph is invalid. No resources are created, modified, or destroyed. The run is dead on arrival. This is not a warning — it is a hard abort. In a CI/CD pipeline, this means your deployment is blocked at the plan stage.
The Attack Vector / Blast Radius
This is a deployment availability failure, not a security vulnerability — but the blast radius in production is severe:
- Full deployment freeze: Every resource in the module is blocked, not just the ones in the cycle. A cycle in a shared
base-inframodule can cascade and prevent unrelated application stacks from deploying. - Rollback impossibility: If this cycle is introduced mid-refactor,
terraform destroyalso fails against the same graph, leaving you unable to tear down partially-created resources. - Root cause is almost always one of three patterns:
- Redundant explicit
depends_onduplicating an implicit reference that already exists via attribute interpolation — Terraform counts both edges, creating a phantom cycle. - Cross-module circular references where module A outputs a value consumed by module B, and module B outputs a value consumed by module A.
- Resource self-reference in
provisionerorlifecycleblocks referencing an attribute of a resource that hasn't been created yet because it depends on the current resource.
- Redundant explicit
- The longer this sits undetected, the worse it gets. Engineers start adding more
depends_onentries to "fix" ordering issues, deepening the cycle and making it harder to trace.
How to Fix It
Identify the Cycle First
Run with full graph output:
terraform graph | dot -Tsvg > graph.svg
# Or without graphviz:
terraform graph 2>&1 | grep -A 20 "Cycle"
The error message itself lists the cycle path. Read it as a chain: resource_a → resource_b → resource_c → resource_a.
Basic Fix: Remove Redundant depends_on
The most common cause — an explicit depends_on that duplicates an implicit reference already created by using the resource's attribute directly.
resource "aws_iam_instance_profile" "app_profile" {
name = "app-profile"
role = aws_iam_role.app_role.name
}
resource "aws_launch_template" "app_lt" {
name_prefix = "app-"
image_id = var.ami_id
instance_type = "t3.medium"
iam_instance_profile {
name = aws_iam_instance_profile.app_profile.name
}
- depends_on = [
- aws_iam_role.app_role,
- aws_iam_instance_profile.app_profile
- ]
}
Why this works: aws_launch_template.app_lt already implicitly depends on aws_iam_instance_profile.app_profile via the .name attribute reference. Adding depends_on = [aws_iam_instance_profile.app_profile] creates a duplicate edge. If aws_iam_role.app_role also references aws_launch_template anywhere — directly or through a data source — Terraform closes the loop and errors. Remove the explicit depends_on entries that are already covered by attribute references.
Enterprise Best Practice: Break Genuine Bidirectional Dependencies with a Data Source or Intermediary
When two resources genuinely need each other's attributes (common with IAM policies referencing resource ARNs that aren't known until creation), the pattern is to decouple using a separate policy attachment resource rather than embedding the policy inline.
# BAD: aws_iam_role embeds a policy that references aws_s3_bucket,
# and aws_s3_bucket policy references aws_iam_role — cycle.
- resource "aws_iam_role" "app_role" {
- name = "app-role"
- assume_role_policy = data.aws_iam_policy_document.assume.json
- inline_policy {
- name = "s3-access"
- policy = data.aws_iam_policy_document.s3_policy.json # references aws_s3_bucket.app.arn
- }
- depends_on = [aws_s3_bucket.app]
- }
-
- resource "aws_s3_bucket_policy" "app" {
- bucket = aws_s3_bucket.app.id
- policy = data.aws_iam_policy_document.bucket_policy.json # references aws_iam_role.app_role.arn
- depends_on = [aws_iam_role.app_role]
- }
+ # GOOD: Decouple with a standalone policy resource attached after both exist
+
+ resource "aws_iam_role" "app_role" {
+ name = "app-role"
+ assume_role_policy = data.aws_iam_policy_document.assume.json
+ # No inline_policy referencing s3 bucket here
+ }
+
+ resource "aws_iam_role_policy" "s3_access" {
+ name = "s3-access"
+ role = aws_iam_role.app_role.id
+ policy = data.aws_iam_policy_document.s3_policy.json # references aws_s3_bucket.app.arn
+ # Implicit depends_on: aws_iam_role.app_role AND aws_s3_bucket.app — no cycle
+ }
+
+ resource "aws_s3_bucket_policy" "app" {
+ bucket = aws_s3_bucket.app.id
+ policy = data.aws_iam_policy_document.bucket_policy.json # references aws_iam_role.app_role.arn
+ # Implicit depends_on: aws_s3_bucket.app AND aws_iam_role.app_role — no cycle
+ }
Key principle: The intermediary resources (aws_iam_role_policy, aws_s3_bucket_policy) each depend on both parties but neither original resource depends on the other. The graph is now acyclic.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. terraform validate as the first pipeline step — always.
# .github/workflows/terraform.yml
- name: Validate Terraform
run: |
terraform init -backend=false
terraform validate
This catches cycles before plan ever runs and fails fast with zero cloud API calls.
2. Checkov for static graph analysis:
checkov -d . --framework terraform
# Checkov BC_TF_GRAPH_* checks flag dependency graph anomalies
3. terraform graph diff in PRs: Add a step that renders the dependency graph and posts it as a PR artifact. Reviewers can visually spot new bidirectional edges before merge.
4. OPA/Conftest policy to ban depends_on on resources that already use attribute references:
# conftest/terraform_depends_on.rego
package terraform.cycles
deny[msg] {
resource := input.resource[_][_][_]
count(resource.depends_on) > 0
# Flag for manual review — any explicit depends_on should be justified in PR
msg := sprintf("Explicit depends_on found in resource. Verify no implicit reference already exists: %v", [resource])
}
5. Module boundary discipline: Never have module A consume an output from module B if module B already consumes an output from module A. Enforce this with a documented architecture decision record (ADR) and a Conftest check on module blocks in your root configuration.