Dependency Graph#
Terraform builds a directed acyclic graph (DAG) of all resources and their dependencies. This graph determines the order of operations.
flowchart TD
VPC["aws_vpc.main"] --> Subnet["aws_subnet.public"]
VPC --> SG["aws_security_group.web"]
Subnet --> Instance["aws_instance.web"]
SG --> Instance
Instance --> EIP["aws_eip.web"]Terraform creates resources top-down (VPC first, EIP last) and destroys bottom-up (EIP first, VPC last).
Implicit Dependencies#
Most dependencies are automatic — Terraform detects them from attribute references:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
# Implicit dependency: references aws_vpc.main.id
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
# Implicit dependency: references aws_subnet.public.id
resource "aws_instance" "web" {
subnet_id = aws_subnet.public.id
ami = data.aws_ami.latest.id
}Terraform automatically knows: VPC → Subnet → Instance.
Explicit Dependencies#
Sometimes resources depend on each other without a direct attribute reference:
resource "aws_iam_role_policy" "s3_access" {
role = aws_iam_role.app.id
policy = data.aws_iam_policy_document.s3.json
}
resource "aws_instance" "app" {
ami = data.aws_ami.latest.id
instance_type = "t3.micro"
iam_instance_profile = aws_iam_instance_profile.app.name
# The instance needs the policy attached before it starts,
# but there's no direct attribute reference to the policy
depends_on = [aws_iam_role_policy.s3_access]
}Use depends_on sparingly — it’s a sign that the dependency isn’t naturally expressed through references. Overuse makes the graph harder to understand.
Lifecycle Rules#
Lifecycle rules modify how Terraform handles resource creation, updates, and destruction:
resource "aws_instance" "web" {
ami = data.aws_ami.latest.id
instance_type = var.instance_type
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [ami, tags["UpdatedAt"]]
replace_triggered_by = [null_resource.redeploy.id]
}
}create_before_destroy#
Default behavior: destroy old → create new. This causes downtime.
With create_before_destroy: create new → destroy old. Zero-downtime replacement.
flowchart LR
subgraph Default["Default (destroy first)"]
D1["Destroy old"] --> D2["Create new"]
D1 -.->|"⚠️ Downtime"| D2
end
subgraph CBD["create_before_destroy"]
C1["Create new"] --> C2["Verify healthy"]
C2 --> C3["Destroy old"]
C1 -.->|"✅ No downtime"| C3
endresource "aws_launch_template" "web" {
image_id = data.aws_ami.latest.id
instance_type = var.instance_type
lifecycle {
create_before_destroy = true
}
}prevent_destroy#
Protects critical resources from accidental deletion:
resource "aws_db_instance" "production" {
identifier = "prod-database"
engine = "postgres"
instance_class = "db.r6g.large"
lifecycle {
prevent_destroy = true
}
}If you try to destroy this resource, Terraform errors:
Error: Instance cannot be destroyed
Resource aws_db_instance.production has lifecycle.prevent_destroy set,
but the plan calls for this resource to be destroyed.To actually destroy it: remove prevent_destroy, apply, then remove the resource.
ignore_changes#
Tell Terraform to ignore specific attribute changes (useful when external systems modify resources):
resource "aws_instance" "web" {
ami = data.aws_ami.latest.id
instance_type = var.instance_type
lifecycle {
# Auto-scaling group changes these; don't fight it
ignore_changes = [
ami,
instance_type,
tags["LastScaledAt"],
]
}
}
# Ignore ALL changes (Terraform manages creation only)
resource "aws_ecs_service" "app" {
desired_count = 2
lifecycle {
ignore_changes = all
}
}Common use cases:
- Tags modified by external automation
- AMI updates handled by a separate process
- Desired count managed by auto-scaling
replace_triggered_by#
Force replacement when another resource changes:
resource "null_resource" "config_version" {
triggers = {
config_hash = filemd5("config/app.json")
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.latest.id
instance_type = "t3.micro"
lifecycle {
replace_triggered_by = [null_resource.config_version.id]
}
}When config/app.json changes → null_resource is replaced → instance is replaced.
Resource Targeting#
Apply only specific resources (escape hatch, not normal workflow):
# Plan only the VPC
terraform plan -target=aws_vpc.main
# Apply only a specific module
terraform apply -target=module.vpc
# Multiple targets
terraform apply -target=aws_instance.web -target=aws_security_group.webWhen to use targeting:
- Debugging a specific resource
- Recovering from a partially failed apply
- Bootstrapping circular dependencies
When NOT to use targeting:
- Regular workflow (apply everything)
- Avoiding slow plans (fix the root cause instead)
Timeouts#
Some resources take time to create/update/destroy:
resource "aws_db_instance" "main" {
identifier = "prod-db"
engine = "postgres"
instance_class = "db.r6g.large"
timeouts {
create = "60m" # RDS can take a while
update = "60m"
delete = "30m"
}
}
resource "aws_eks_cluster" "main" {
name = "prod-cluster"
timeouts {
create = "30m"
update = "60m"
delete = "15m"
}
}Preconditions and Postconditions#
Validate assumptions before and after resource operations:
data "aws_ami" "app" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["app-*"]
}
}
resource "aws_instance" "app" {
ami = data.aws_ami.app.id
instance_type = var.instance_type
lifecycle {
precondition {
condition = data.aws_ami.app.architecture == "x86_64"
error_message = "AMI must be x86_64."
}
postcondition {
condition = self.private_ip != ""
error_message = "Instance must have a private IP assigned."
}
}
}Dependency Visualization#
# Generate dependency graph (DOT format)
terraform graph | dot -Tpng > graph.png
# Filter to specific resource type
terraform graph -type=planKey Takeaways#
- Let Terraform infer dependencies — references create implicit ordering automatically
- Use
depends_onsparingly — only when there’s a hidden dependency with no reference create_before_destroyfor zero downtime — new resource is ready before old is removedprevent_destroyfor critical resources — databases, state buckets, production dataignore_changesfor external modifications — when other systems legitimately change attributes- Never use
-targetin normal workflow — it’s a debugging tool, not a deployment strategy - Add timeouts for slow resources — RDS, EKS, and CloudFront can take 30+ minutes