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:#2e7d32resource "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:#2e7d32module "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:#2e7d32Dependency 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_onis 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 unnecessarydepends_onreduces 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 graphfor visualization — use it when configurations are complex and you need to understand the actual execution order.