Initializing Enclave...

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). apply and plan both abort.
  • How to fix it: Identify the cycle path from the error output, remove redundant depends_on entries 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 .tf files 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-infra module can cascade and prevent unrelated application stacks from deploying.
  • Rollback impossibility: If this cycle is introduced mid-refactor, terraform destroy also fails against the same graph, leaving you unable to tear down partially-created resources.
  • Root cause is almost always one of three patterns:
    1. Redundant explicit depends_on duplicating an implicit reference that already exists via attribute interpolation — Terraform counts both edges, creating a phantom cycle.
    2. Cross-module circular references where module A outputs a value consumed by module B, and module B outputs a value consumed by module A.
    3. Resource self-reference in provisioner or lifecycle blocks referencing an attribute of a resource that hasn't been created yet because it depends on the current resource.
  • The longer this sits undetected, the worse it gets. Engineers start adding more depends_on entries 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.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →