RAPID INCIDENT TRIAGE & ACTION PLAN•Syntax
01. Error Signature
Terraform Self-Reference Dependency Cycle
02. Root Cause Vector

A Terraform resource directly or indirectly references itself in its own attribute expressions, creating a dependency cycle that halts al...

03. Rapid Prescription

Refresh remote state lock, ensure provider version constraints, and audit state drift.

Zero-Trust Client IDE/ Syntax

Terraform Self-Reference Dependency Cycle

A Terraform resource directly or indirectly references itself in its own attribute expressions, creating a dependency cycle that halts all plan and apply operations.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(85L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Terraform module for PCI-secure-zone legacy API gateway (DO NOT MODIFY WITHOUT ARCHITECTURE REVIEW)
# TODO: This entire module is a mess, refactor after Q4 migration to new-registry.internal
# WARNING: Contains legacy hardcoded values for my-registry.internal/legacy-api:v2.1.3

provider "aws" {
  region = "us-east-1"
  # TODO: Replace with proper IAM role after PCI audit
  access_key = "AKIAIOSFODNN7EXAMPLE1234"
  secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}

# Legacy VPC for PCI zone - DO NOT DELETE
resource "aws_vpc" "pci_legacy_vpc" {
  cidr_block = "10.0.0.0/16"
  enable_dns_hostnames = true
  
  # VULNERABILITY: Self-reference creates dependency cycle with security group
  tags = {
    Name = "pci-legacy-${aws_security_group.pci_api_sg.id}"  # Circular reference here
    Environment = "legacy-prod"
  }
}

# Security group referencing VPC ID (normal dependency)
resource "aws_security_group" "pci_api_sg" {
  name        = "pci-api-gateway-sg"
  description = "Legacy PCI API security group (temporary workaround)"
  vpc_id      = aws_vpc.pci_legacy_vpc.id  # Normal forward reference
  
  # TODO: Fix ingress rules after migration
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]  # TEMPORARY - will fix after PCI audit
  }
  
  # Stripe webhook secret embedded in description (bad practice)
  # Description contains: sk_live_51H8zq2KZ03p4qWX2yV8nM7aP9rT6FgH5JkL3N1BvC4X7Z0QwA9S2D4F6H8J0L2N4
  
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Database subnet group with hardcoded credentials (legacy system)
resource "aws_db_subnet_group" "pci_legacy_db" {
  name       = "pci-legacy-db-subnet-group"
  subnet_ids = [aws_subnet.pci_private_a.id, aws_subnet.pci_private_b.id]
  
  # TODO: Move to Secrets Manager after migration
  tags = {
    ConnectionString = "postgresql://dbadmin:V3ryS3cur3P@ssw0rd!2024@pci-db.internal:5432/legacy_pci_data"
    ManagedBy = "terraform-legacy-pci-module"
  }
}

# Private subnet A (commented out old CIDR)
# cidr_block = "10.0.1.0/24"  # Old pre-migration range
resource "aws_subnet" "pci_private_a" {
  vpc_id     = aws_vpc.pci_legacy_vpc.id
  cidr_block = "10.0.2.0/24"
  availability_zone = "us-east-1a"
}

# Private subnet B - using same VPC reference
resource "aws_subnet" "pci_private_b" {
  vpc_id     = aws_vpc.pci_legacy_vpc.id
  cidr_block = "10.0.3.0/24"
  availability_zone = "us-east-1b"
  
  # Embedded RSA key in tag (terrible practice - legacy system)
  tags = {
    BackupKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArL8LpM8vC6p7K2Vq9T8HsJwLmN4P3qR0XzA2bF5G7H9J2K4\nL3N6P8Q1S3D5F7H9J2K4L3N6P8Q1S3D5F7H9J2K4L3N6P8Q1S3D5F7H9J2K4\nL3N6P8Q1S3D5F7H9J2K4L3N6P8Q1S3D5F7H9J2K4L3N6P8Q1S3D5F7H9J2K4\n-----END RSA PRIVATE KEY-----"
  }
}

# TODO: Add NAT gateway after PCI compliance review
# resource "aws_nat_gateway" "pci_nat" {
#   allocation_id = aws_eip.nat.id
#   subnet_id     = aws_subnet.pci_public.id
# }
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

How to Fix Terraform 'Error: cycle in dependency graph' Caused by Self-Reference

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins

TL;DR

  • What broke: A Terraform resource block contains an expression that references its own resource address (e.g., aws_security_group.this.id inside the aws_security_group.this block itself), creating a circular node in the DAG that Terraform's graph walker cannot resolve.
  • How to fix it: Extract the self-referencing value into a local, a separate data source lookup, or restructure the dependency chain so no resource depends on its own computed attributes during the plan phase.
  • Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your failing .tf block and get corrected HCL instantly without sending your state file or ARNs to any server.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

You ran terraform plan and hit a hard stop:

Error: cycle in dependency graph

Cycle: aws_security_group.app_sg (expand) -> aws_security_group_rule.allow_self -> aws_security_group.app_sg (expand)

Terraform detected a cycle in the dependency graph. This is always a bug.
Please report it at https://github.com/hashicorp/terraform/issues

Or a self-reference variant:

Error: cycle in dependency graph

Cycle: aws_iam_role.lambda_exec (expand) -> aws_iam_role_policy.inline -> aws_iam_role.lambda_exec (expand)

Immediate consequence: terraform plan exits with code 1. No diff is generated. No apply is possible. If this is in a CI/CD pipeline, the deployment is fully blocked. If the cycle was introduced mid-refactor on a live workspace, your infrastructure is frozen — you cannot add, change, or destroy any resource in the affected module until the graph is clean.

Terraform builds a Directed Acyclic Graph (DAG) of all resources and their dependencies before executing any operation. A self-reference or circular dependency makes the graph non-acyclic. The graph walker has no valid traversal order and throws a fatal error. This is not a warning. There is no partial plan.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is not a security misconfiguration in the traditional CVE sense, but the blast radius in a production IaC workflow is severe:

  1. Full deployment freeze. Every plan, apply, and destroy in the affected root module or workspace fails. If your module is shared (a child module called by multiple root modules), every caller is broken simultaneously.

  2. State drift accumulation. If the cycle was introduced after a partial apply, some resources may have been created before Terraform hit the cycle on a subsequent run. Those resources now exist in the real cloud environment but cannot be managed, modified, or destroyed via Terraform until the cycle is resolved. Manual intervention or terraform state rm may be required.

  3. CI/CD pipeline paralysis. In GitOps workflows (Atlantis, Terraform Cloud, GitHub Actions), a cycle error blocks all PRs touching that module. Teams waiting on downstream infrastructure — application deployments, DNS records, certificate provisioning — are stalled.

  4. The most common self-reference traps:

    • Security Group self-referencing rules: Defining an ingress rule that allows traffic from the same SG (self = true is valid, but referencing aws_security_group.this.id inside the same resource block is not).
    • IAM Role + inline policy cycle: An aws_iam_role referencing an aws_iam_role_policy output, and that policy referencing the role's ARN via the role resource reference rather than a local.
    • depends_on creating accidental back-edges: Adding depends_on to a resource that is already an implicit dependency creates a cycle.
    • Module output self-loops: A module output referencing a resource that depends_on the module itself.

How to Fix It (The Solution)

Root Cause Identification First

Run this to get the full cycle path printed to stdout before touching any code:

terraform graph | grep -A 5 "cycle"
# Or pipe to graphviz for a visual:
terraform graph | dot -Tsvg > graph.svg

The terraform graph output will show you the exact node names forming the cycle.


Scenario 1: Security Group Self-Reference (Most Common)

The Bad Pattern — resource referencing its own computed id:

- resource "aws_security_group" "app_sg" {
-   name        = "app-sg"
-   description = "Application security group"
-   vpc_id      = var.vpc_id
-
-   ingress {
-     from_port       = 8080
-     to_port         = 8080
-     protocol        = "tcp"
-     # CYCLE: referencing aws_security_group.app_sg.id inside aws_security_group.app_sg
-     security_groups = [aws_security_group.app_sg.id]
-   }
- }
+ resource "aws_security_group" "app_sg" {
+   name        = "app-sg"
+   description = "Application security group"
+   vpc_id      = var.vpc_id
+   # No ingress block here — self-referencing rules must be separate resources
+ }
+
+ resource "aws_security_group_rule" "allow_self_ingress" {
+   type              = "ingress"
+   from_port         = 8080
+   to_port           = 8080
+   protocol          = "tcp"
+   self              = true  # Terraform-native self-reference — no cycle
+   security_group_id = aws_security_group.app_sg.id
+ }

Key principle: Use self = true in aws_security_group_rule for intra-SG rules. Never reference the parent SG's computed id inside its own ingress/egress inline block.


Scenario 2: IAM Role + Policy Circular Dependency

- resource "aws_iam_role" "lambda_exec" {
-   name               = "lambda-exec-role"
-   assume_role_policy = data.aws_iam_policy_document.assume.json
-   # CYCLE: inline_policy block referencing aws_iam_role_policy.inline
-   inline_policy {
-     name   = aws_iam_role_policy.inline.name
-     policy = aws_iam_role_policy.inline.policy
-   }
- }
-
- resource "aws_iam_role_policy" "inline" {
-   role   = aws_iam_role.lambda_exec.id
-   policy = data.aws_iam_policy_document.lambda_perms.json
- }
+ # Use EITHER inline_policy inside the role OR a separate aws_iam_role_policy. Never both.
+ resource "aws_iam_role" "lambda_exec" {
+   name               = "lambda-exec-role"
+   assume_role_policy = data.aws_iam_policy_document.assume.json
+   # No inline_policy block — managed by separate resource below
+ }
+
+ resource "aws_iam_role_policy" "inline" {
+   name   = "lambda-exec-inline-policy"
+   role   = aws_iam_role.lambda_exec.id
+   policy = data.aws_iam_policy_document.lambda_perms.json
+ }

Scenario 3: `depends_on` Back-Edge Creating a Cycle

- resource "aws_s3_bucket" "artifacts" {
-   bucket = "my-artifacts-${var.env}"
-   # CYCLE: depends_on creates a back-edge to a resource that already
-   # implicitly depends on this bucket
-   depends_on = [aws_s3_bucket_policy.artifacts_policy]
- }
-
- resource "aws_s3_bucket_policy" "artifacts_policy" {
-   bucket = aws_s3_bucket.artifacts.id  # implicit dependency on artifacts
-   policy = data.aws_iam_policy_document.bucket_policy.json
- }
+ resource "aws_s3_bucket" "artifacts" {
+   bucket = "my-artifacts-${var.env}"
+   # Removed depends_on — the implicit dependency via aws_s3_bucket_policy.artifacts_policy.bucket
+   # already establishes the correct order: bucket -> policy
+ }
+
+ resource "aws_s3_bucket_policy" "artifacts_policy" {
+   bucket = aws_s3_bucket.artifacts.id
+   policy = data.aws_iam_policy_document.bucket_policy.json
+ }

Rule: Never add depends_on to resource A pointing to resource B if resource B already has an implicit reference to resource A. Terraform's implicit dependency tracking handles it. Explicit depends_on on top creates a back-edge.


Enterprise Best Practice: Use `locals` to Break Computed Attribute Chains

When a value needs to be shared across resources that would otherwise create a cycle, extract it into a local computed from a non-cyclic source:

- # Pattern that risks cycles when the computed value is reused
- resource "aws_lb" "app" {
-   name = "app-lb-${aws_lb_target_group.app.name}"
-   ...
- }
+ # Compute the shared name segment once, reference the local everywhere
+ locals {
+   app_name_slug = "app-${var.env}-${var.region}"
+ }
+
+ resource "aws_lb" "app" {
+   name = "lb-${local.app_name_slug}"
+   ...
+ }
+
+ resource "aws_lb_target_group" "app" {
+   name = "tg-${local.app_name_slug}"
+   ...
+ }

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

Don't wait for terraform plan in a pipeline to catch this. Shift left.

1. `terraform validate` as a Pre-Commit Hook

# .git/hooks/pre-commit
#!/bin/bash
terraform init -backend=false
terraform validate
if [ $? -ne 0 ]; then
  echo "Terraform validation failed. Fix cycles before committing."
  exit 1
fi

terraform validate catches self-references and most cycle patterns without requiring cloud credentials or a backend.

2. `tflint` + `terraform graph` in GitHub Actions

# .github/workflows/tf-lint.yml
- name: Validate Terraform Graph (Cycle Detection)
  run: |
    terraform init -backend=false
    terraform graph > /tmp/tf_graph.dot
    # Fail if graphviz detects cycles (non-DAG structure)
    python3 -c "
    import subprocess, sys
    result = subprocess.run(['terraform', 'validate'], capture_output=True, text=True)
    if result.returncode != 0:
        print(result.stderr)
        sys.exit(1)
    "

3. Checkov Policy — Flag `depends_on` Overuse

# checkov custom check: flag explicit depends_on on resources
# that already have implicit references (common cycle source)
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck

class CheckExplicitDependsOn(BaseResourceCheck):
    def __init__(self):
        name = "Ensure depends_on is not redundantly set alongside implicit references"
        id = "CKV_CUSTOM_TF_001"
        super().__init__(name=name, id=id, supported_resources=['*'], block_type='resource')

    def scan_resource_conf(self, conf):
        # Flag any resource with both depends_on and attribute-level references
        has_depends_on = 'depends_on' in conf
        return not has_depends_on  # Trigger review on any depends_on usage

4. Atlantis `pre_workflow_hooks` — Block Cycle PRs

# atlantis.yaml
version: 3
projects:
  - dir: .
    workflow: safe-plan
workflows:
  safe-plan:
    plan:
      steps:
        - run: terraform init -backend=false
        - run: terraform validate  # Fails fast on cycles before plan
        - plan

Bottom line: terraform validate is free, fast, and catches cycles in under 2 seconds. There is no valid reason it should not be the first step in every CI pipeline touching Terraform code.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.What exactly causes a 'cycle in dependency graph' error in Terraform?
The error originates from a violation of Terraform's core Directed Acyclic Graph (DAG) model. A cycle is formed when a resource, either directly or through an intermediary chain, depends on its own computed state—such as an `aws_security_group` referencing its own `id` within its definition. This creates a logical paradox where Terraform cannot resolve a creation order, halting all operations. Understanding this graph theory foundation is critical; it's not a bug but a fundamental constraint of declarative infrastructure-as-code.
Q2.How do I identify which resources are forming the cycle?
You can manually trace the cycle using `terraform graph` or visual tools like Graphviz, but this is a diagnostic trap. Manually rewriting dependencies to break the cycle is error-prone and often introduces security vulnerabilities—like incorrectly ordered security group rules that create unintended public access. One misstep expands the blast radius. Instead of risking manual edits, drop the broken manifest into the StackEngine Local Sandbox above. It will automatically analyze the graph, enforce zero-trust principles, and compile a provably correct, cycle-free fix in seconds.
Q3.Can `depends_on` cause a dependency cycle even if I don't have a self-reference?
Absolutely, and at enterprise scale, this becomes a compliance nightmare. A redundant `depends_on` that creates a back-edge over an existing implicit dependency is a silent time bomb in mono-repos. Manually auditing thousands of lines for these anti-patterns is unsustainable and fails audit trails. For automated pull request generation with Maker-Checker review, upgrade to the StackEngine CI/CD Automator (Team Edition). It integrates directly into your pipelines to preemptively detect, break, and document dependency cycles, ensuring continuous compliance and eliminating runtime failures before they reach production.