Dependencies
Understand implicit and explicit resource dependencies in Terraform
Dependencies
In the previous tutorial, we organized our project structure. Now let's talk about order — because Terraform needs to know what to create first.
You can't attach a security group to a VPC that doesn't exist yet. That's like trying to put furniture in a room before building the house. Most of the time, Terraform figures this out automatically. Sometimes you need to give it a hint.
Implicit Dependencies
"How does Terraform know what order to create things?"
When a resource references another resource's attribute, Terraform automatically understands the dependency. It just works:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # Reference creates dependency
cidr_block = "10.0.1.0/24"
}
Terraform sees that aws_subnet.public uses aws_vpc.main.id and knows:
- Create VPC first
- Then create subnet
You don't need to declare this. It just works. How cool is that?
Dependency Chain
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
subnet_id = aws_subnet.public.id # Depends on subnet
}
Create order: VPC → Subnet → Instance
Parallel Creation
"Does Terraform create everything one at a time?"
Nope! Resources without dependencies are created in parallel — Terraform is smart about speed:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_s3_bucket" "logs" {
bucket = "my-logs-bucket"
}
resource "aws_sqs_queue" "tasks" {
name = "task-queue"
}
These three resources have no dependencies on each other. Terraform creates all three simultaneously. Parallel processing, baby.
Explicit Dependencies
"What if Terraform can't see the relationship?"
Sometimes Terraform can't figure it out from the code alone. Use depends_on to make it explicit.
When You Need depends_on
resource "aws_iam_role" "lambda" {
name = "lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_lambda_function" "processor" {
function_name = "processor"
role = aws_iam_role.lambda.arn # Implicit dependency on role
handler = "index.handler"
runtime = "nodejs18.x"
filename = "lambda.zip"
# But Lambda might fail if policy isn't attached yet!
depends_on = [aws_iam_role_policy_attachment.lambda_basic]
}
The Lambda function references the role ARN (implicit dependency), but it doesn't reference the policy attachment. Without depends_on, Terraform might try to create the Lambda before the policy is attached, and the Lambda would fail to invoke. Sneaky bug.
VPC Endpoints Example
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-west-2.s3"
}
resource "aws_instance" "private" {
ami = "ami-12345678"
instance_type = "t2.micro"
subnet_id = aws_subnet.private.id
# Instance expects S3 access via VPC endpoint
depends_on = [aws_vpc_endpoint.s3]
}
Database Before Application
resource "aws_db_instance" "main" {
identifier = "mydb"
engine = "postgres"
instance_class = "db.t3.micro"
# ...
}
resource "aws_ecs_service" "api" {
name = "api"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
# App needs database ready (even though no direct reference)
depends_on = [aws_db_instance.main]
}
depends_on Gotchas
Before you slap depends_on on everything...
Don't Overuse It
# Bad: Unnecessary depends_on
resource "aws_instance" "web" {
subnet_id = aws_subnet.public.id # Already creates implicit dependency!
depends_on = [aws_subnet.public] # Redundant
}
If there's already a reference, you don't need depends_on. That's redundant — like wearing a belt and suspenders and tying your pants with rope.
Module Dependencies
module "database" {
source = "./modules/database"
# ...
}
module "application" {
source = "./modules/application"
database_url = module.database.connection_string # Implicit
# But if app needs db ready beyond just the URL...
depends_on = [module.database]
}
Multiple Dependencies
resource "aws_lambda_function" "processor" {
# ...
depends_on = [
aws_iam_role_policy_attachment.lambda_vpc,
aws_iam_role_policy_attachment.lambda_logs,
aws_vpc_endpoint.lambda,
]
}
Lifecycle Rules
Control how resources are created, updated, and destroyed. These are the safety rails.
create_before_destroy
"Can I swap out a resource without downtime?"
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t2.micro"
lifecycle {
create_before_destroy = true
}
}
When the AMI changes:
- Create new instance
- Update references
- Destroy old instance
Useful for zero-downtime deployments. New server comes up, old server goes down. Smooth.
prevent_destroy
"What if someone accidentally destroys the production database?"
Prevent it:
resource "aws_db_instance" "production" {
identifier = "prod-db"
engine = "postgres"
instance_class = "db.t3.large"
lifecycle {
prevent_destroy = true
}
}
Terraform will error if you try to destroy this resource. It's the "are you absolutely sure?" guard for critical infrastructure.
To actually destroy it:
- Remove
prevent_destroyfrom config - Run
terraform apply - Then
terraform destroy
ignore_changes
"Some external process keeps changing my tags and Terraform keeps reverting them!"
Tell Terraform to look the other way:
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t2.micro"
lifecycle {
ignore_changes = [ami]
}
}
Changes to AMI outside Terraform won't trigger replacement. Useful when:
- ASG updates AMI independently
- External process modifies resource
- You want Terraform to manage some attributes but not others
# Ignore all changes
lifecycle {
ignore_changes = all
}
# Ignore multiple attributes
lifecycle {
ignore_changes = [
ami,
user_data,
tags["LastModified"],
]
}
replace_triggered_by
Force replacement when another resource changes:
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t2.micro"
lifecycle {
replace_triggered_by = [
aws_security_group.web.id, # Replace instance when SG changes
]
}
}
Targeting Resources
"Can I apply just one resource at a time?"
Yep. Useful for debugging.
Apply Specific Resources
# Apply only the VPC
terraform apply -target=aws_vpc.main
# Apply multiple targets
terraform apply -target=aws_vpc.main -target=aws_subnet.public
# Apply a module
terraform apply -target=module.networking
When to Use Targeting
- Debugging: Test one resource at a time
- Emergency fixes: Update critical resource without touching others
- Large changes: Break up big applies
When NOT to Use Targeting
Regular workflow. Targeting can leave state inconsistent — Terraform even warns you:
Warning: Resource targeting is in effect
Dependency Visualization
Show Dependencies
terraform graph
Output is GraphViz format. Visualize it:
terraform graph | dot -Tpng > graph.png
Or use online viewers like webgraphviz.com.
Understanding the Graph
digraph {
aws_vpc.main
aws_subnet.public -> aws_vpc.main
aws_instance.web -> aws_subnet.public
}
Arrows point from dependent to dependency (reverse of creation order).
Common Patterns
NAT Gateway Dependencies
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_eip" "nat" {
domain = "vpc"
# EIP must be created after IGW exists
depends_on = [aws_internet_gateway.main]
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
# Explicit dependency on IGW
depends_on = [aws_internet_gateway.main]
}
ECS Service Dependencies
resource "aws_ecs_cluster" "main" {
name = "my-cluster"
}
resource "aws_ecs_task_definition" "app" {
family = "app"
# ...
}
resource "aws_lb_target_group" "app" {
# ...
}
resource "aws_lb_listener" "app" {
load_balancer_arn = aws_lb.main.arn
# ...
}
resource "aws_ecs_service" "app" {
name = "app"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "app"
container_port = 8080
}
# Service needs listener configured before it can register targets
depends_on = [aws_lb_listener.app]
}
Database with Schema
resource "aws_db_instance" "main" {
identifier = "mydb"
engine = "postgres"
instance_class = "db.t3.micro"
# ...
}
resource "null_resource" "db_schema" {
triggers = {
schema_version = var.schema_version
}
provisioner "local-exec" {
command = "psql ${aws_db_instance.main.endpoint} < schema.sql"
}
depends_on = [aws_db_instance.main]
}
resource "aws_ecs_service" "app" {
# ...
depends_on = [null_resource.db_schema] # App needs schema ready
}
Circular Dependencies
"Terraform says I have a cycle. What did I do?"
Terraform doesn't allow circular dependencies (A depends on B, B depends on A). It's the Terraform equivalent of a deadlock:
# This won't work!
resource "aws_security_group" "web" {
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.lb.id] # Depends on lb
}
}
resource "aws_security_group" "lb" {
egress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.web.id] # Depends on web - CIRCULAR!
}
}
Solution: Separate Rules
resource "aws_security_group" "web" {
name = "web"
}
resource "aws_security_group" "lb" {
name = "lb"
}
resource "aws_security_group_rule" "web_from_lb" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
security_group_id = aws_security_group.web.id
source_security_group_id = aws_security_group.lb.id
}
resource "aws_security_group_rule" "lb_to_web" {
type = "egress"
from_port = 80
to_port = 80
protocol = "tcp"
security_group_id = aws_security_group.lb.id
source_security_group_id = aws_security_group.web.id
}
Create both security groups first (no dependency), then create rules that reference both. Break the circle. Problem solved.
What's Next?
Dependencies are the invisible backbone of every Terraform project. You now know:
- Implicit dependencies (just reference an attribute — magic!)
- Explicit dependencies with
depends_on(when Terraform needs help) - Lifecycle rules for safety and zero-downtime deployments
- How to fix circular dependencies
Now let's handle sensitive stuff — managing secrets safely in Terraform. Let's go!