Dependency #

Cloud infrastructure rarely stands alone. An EC2 instance needs a subnet, a subnet needs a VPC, a VPC needs an internet gateway for public access. Terraform needs to know the correct order to create and destroy all of these resources. How Terraform manages dependencies — both automatically and explicitly — is one of the features that most sets it apart from traditional scripting approaches.

Implicit Dependencies #

An implicit dependency is one that Terraform infers automatically from references between resources. This is the most common and most recommended way to define dependencies.

flowchart TD
    A["aws_vpc.main"] --> B["aws_subnet.public\nvpc_id = aws_vpc.main.id"]
    A --> C["aws_internet_gateway.main\nvpc_id = aws_vpc.main.id"]
    B --> D["aws_instance.web\nsubnet_id = aws_subnet.public.id"]

    style A fill:#e3f2fd,stroke:#1565c0
    style D fill:#e8f5e9,stroke:#2e7d32
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  # The reference to aws_vpc.main.id automatically makes
  # Terraform know that this subnet depends on the VPC
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

resource "aws_internet_gateway" "main" {
  # The IGW also depends on the VPC
  vpc_id = aws_vpc.main.id
}

resource "aws_instance" "web" {
  ami       = var.ami_id
  # The instance depends on the subnet
  subnet_id = aws_subnet.public.id
}
flowchart TD
    vpc["aws_vpc.main"] --> subnet["subnet"]
    vpc --> igw["igw"]
    subnet --> instance["instance"]

Terraform calculates this graph automatically. You don’t need to write a single line of extra code to define the order.


Explicit Dependencies with depends_on #

There are cases where a dependency can’t be inferred from a direct reference. This happens when a resource depends on a side effect of another resource, not on its attributes.

flowchart TD
    A["aws_iam_role.node"] --> B["aws_iam_role_policy_attachment\n.eks_worker_node"]
    A --> C["aws_iam_role_policy_attachment\n.eks_cni"]

    D["aws_eks_cluster.main"] --> E["aws_eks_node_group.workers"]
    B --> E
    C --> E

    E -.->|"node_role_arn =\naws_iam_role.node.arn"| A
    E -.->|"depends_on: needs the policy\nALREADY ATTACHED"| B
    E -.->|"depends_on: needs the policy\nALREADY ATTACHED"| C

    style E fill:#fff3e0,stroke:#e65100
    style B fill:#e8f5e9,stroke:#2e7d32
    style C fill:#e8f5e9,stroke:#2e7d32
# REAL CASE: An EKS Node Group depends on an IAM policy already attached

resource "aws_iam_role" "node" {
  name = "eks-node-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action    = "sts:AssumeRole"
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "eks_worker_node" {
  role       = aws_iam_role.node.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}

resource "aws_iam_role_policy_attachment" "eks_cni" {
  role       = aws_iam_role.node.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
}

resource "aws_eks_node_group" "workers" {
  cluster_name    = aws_eks_cluster.main.name
  node_role_arn   = aws_iam_role.node.arn  # The role reference exists
  subnet_ids      = aws_subnet.private[*].id

  scaling_config {
    desired_size = 2
    max_size     = 5
    min_size     = 1
  }

  # Without this depends_on, the node group might start being created before
  # all policies are attached to the role.
  # aws_iam_role.node.arn doesn't guarantee the policies are attached yet.
  depends_on = [
    aws_iam_role_policy_attachment.eks_worker_node,
    aws_iam_role_policy_attachment.eks_cni,
  ]
}

When to Use depends_on #

USE depends_on IF:
  ✓ The resource depends on a side effect of another resource
    (not on the value of its attributes)
  ✓ The correct order can't be inferred from references
  ✓ The provider documentation explicitly mentions a specific dependency
  ✓ There's a known race condition due to IAM propagation timing

DON'T USE depends_on IF:
  ✗ The dependency is already implied by a direct reference
    (extra depends_on only adds noise)
  ✗ Trying to "slow down" execution — that's not the right approach
  ✗ You're not sure why it's needed — find the real root cause

Anti-Pattern: Over-Using depends_on #

# ANTI-PATTERN: Unnecessary depends_on
resource "aws_subnet" "public" {
  # The vpc_id reference already creates an implicit dependency
  # The depends_on below is redundant and only adds confusion
  depends_on = [aws_vpc.main]  # ✗ Not needed

  vpc_id     = aws_vpc.main.id  # ✓ The dependency already exists here
  cidr_block = "10.0.1.0/24"
}

# ANTI-PATTERN: depends_on on an entire module without a specific reason
module "database" {
  source = "./modules/database"
  
  depends_on = [module.networking]  # ✗ Too broad
  # Overly broad dependencies prevent parallelism that could otherwise happen
  # Name specific resources if truly needed
}

Dependencies Between Modules #

When using modules, dependencies can cross module boundaries.

flowchart TD
    V["module.vpc"] --> E["module.eks\nvpc_id = module.vpc.vpc_id"]
    V --> R["module.rds\nvpc_id = module.vpc.vpc_id"]

    E --- P1["PARALLEL\nafter the VPC finishes"]
    R --- P2["PARALLEL\nafter the VPC finishes"]

    style V fill:#e3f2fd,stroke:#1565c0
    style E fill:#e8f5e9,stroke:#2e7d32
    style R fill:#e8f5e9,stroke:#2e7d32
module "vpc" {
  source = "./modules/vpc"
  cidr   = "10.0.0.0/16"
}

module "eks" {
  source = "./modules/eks"

  # Reference to the vpc module's output — implicit dependency
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnet_ids
  # The EKS module will be created after the VPC module finishes
}

module "rds" {
  source = "./modules/rds"

  # RDS also depends on the VPC — but is independent of EKS
  # Terraform will create RDS and EKS in PARALLEL
  # after the VPC finishes
  vpc_id    = module.vpc.vpc_id
  subnet_ids = module.vpc.database_subnet_ids
}

Viewing the Dependency Graph #

# Generate and visualize the dependency graph
terraform graph | dot -Tsvg > dependency-graph.svg

# Filter only specific resources (needs grep/awk)
terraform graph | grep -A2 "aws_instance"

# Graph for a saved plan
terraform graph -plan=tfplan


Implicit vs Explicit Dependencies #

Terraform automatically detects dependencies from references in arguments. But sometimes you need to declare a dependency explicitly.

# IMPLICIT DEPENDENCY (automatically detected):
resource "aws_instance" "web" {
  subnet_id = aws_subnet.public.id
  # Terraform automatically knows: web depends_on subnet
  # No explicit declaration needed
}

# EXPLICIT DEPENDENCY (needs to be declared):
resource "aws_instance" "web" {
  ami           = "ami-12345"
  subnet_id     = aws_subnet.public.id
  
  depends_on = [
    aws_route_table_association.public,  # Make sure routing finishes first
    aws_security_group.web_sg,           # Make sure the SG is attached
  ]
  # Useful when the dependency isn't visible from the arguments
}
flowchart TD
    subgraph IMPLICIT["Implicit Dependency"]
        I1["aws_subnet.public"] --> I2["aws_instance.web
(subnet_id = subnet.id)"]
    end

    subgraph EXPLICIT["Explicit Dependency"]
        E1["aws_route_table_association"] --> E2["aws_instance.web
depends_on = [...]"]
        E3["aws_security_group"] --> E2
    end

    style I1 fill:#e3f2fd,stroke:#1565c0
    style I2 fill:#e8f5e9,stroke:#2e7d32
    style E1 fill:#fff3e0,stroke:#e65100
    style E3 fill:#fff3e0,stroke:#e65100
    style E2 fill:#e8f5e9,stroke:#2e7d32

Dependency Anti-Pattern: Over-Using depends_on #

# ANTI-PATTERN: depends_on on everything
resource "aws_instance" "web" {
  depends_on = [
    aws_vpc.main,
    aws_subnet.public,
    aws_security_group.web_sg,
    aws_iam_role.app_role,
    aws_s3_bucket.data_bucket,
  ]
  # Problems:
  # 1. Prevents parallelization — resources must wait for all dependencies
  # 2. Inaccurate dependencies — not all are truly dependencies
  # 3. Overrides more precise implicit dependencies
}

# CORRECT: Let Terraform detect implicit dependencies
resource "aws_instance" "web" {
  subnet_id = aws_subnet.public.id           # → implicit dependency
  vpc_security_group_ids = [aws_security_group.web_sg.id]  # → implicit
  iam_instance_profile = aws_iam_instance_profile.app.name  # → implicit
  # depends_on is NOT needed — references are enough
}

Debugging the Dependency Graph #

When Terraform does something you don’t expect, the dependency graph is the key to understanding why.

# Generate the dependency graph
terraform graph > graph.dot

# Visualize with Graphviz
terraform graph -type=plan | dot -Tpng > plan-graph.png
terraform graph -type=plan | dot -Tsvg > plan-graph.svg

# Filter only specific resources
terraform graph -draw-cycles -type=plan | dot -Tpng > cycles.png
flowchart TD
    subgraph CLUSTER["Dependency Graph"]
        VPC["aws_vpc.main"]
        PUB_SUB["aws_subnet.public"]
        PRI_SUB["aws_subnet.private"]
        IGW["aws_internet_gateway"]
        RT["aws_route_table"]
        NAT["aws_nat_gateway"]
        SG["aws_security_group"]
        EC2["aws_instance.web"]
        RDS["aws_db_instance.main"]
        
        VPC --> PUB_SUB
        VPC --> PRI_SUB
        VPC --> IGW
        VPC --> RT
        PUB_SUB --> NAT
        PUB_SUB --> EC2
        PRI_SUB --> RDS
        SG --> EC2
        NAT --> PRI_SUB
    end

    style VPC fill:#e3f2fd,stroke:#1565c0
    style EC2 fill:#e8f5e9,stroke:#2e7d32
    style RDS fill:#e8f5e9,stroke:#2e7d32
# Cycle analysis (dependency cycles)
# If there's a cycle, Terraform will error:
# "Error: Cycle: resource_a, resource_b, resource_a"
# Solution: use -target to break the cycle, or refactor the dependency

Circular Dependency Resolution #

A circular dependency occurs when two or more resources depend on each other.

# Error message on circular dependency:
# Error: Cycle: aws_security_group.a, aws_security_group.b, aws_security_group.a
# CIRCULAR DEPENDENCY EXAMPLE:

# SG-A allows ingress from SG-B
# SG-B allows ingress from SG-A
# → Terraform can't create both!

# SOLUTION 1: Merge into a single security group
resource "aws_security_group" "combined" {
  name = "combined-sg"
  
  ingress {
    from_port = 80
    to_port   = 80
    protocol  = "tcp"
    self      = true  # Allow from itself = allow from the same SG
  }
}

# SOLUTION 2: Use a CIDR block instead of an SG reference
resource "aws_security_group" "a" {
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["10.0.2.0/24"]  # CIDR of subnet B
  }
}

resource "aws_security_group" "b" {
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["10.0.1.0/24"]  # CIDR of subnet A
  }
}

Summary #

  • Implicit dependencies are the default — always prioritize direct references for defining dependencies, not depends_on.
  • depends_on is for side-effect dependencies — when a resource depends on a side effect (like an IAM policy attachment) rather than an attribute value.
  • Don’t over-use depends_on — every unnecessary depends_on reduces parallelism and slows down applies.
  • Dependencies cross module boundaries through references to module outputs — the mechanism is the same as regular resource dependencies.
  • Independent resources execute in parallel — Terraform maximizes efficiency by running non-dependent operations simultaneously.
  • terraform graph for visualization — use it when configurations are complex and you need to understand the actual execution order.

← Previous: Operation   Next: Lifecycle →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact