Tutorial 20: Resource Meta-Arguments (depends_on, lifecycle)
Learning Objectives
By the end of this tutorial, you will be able to:
- Use the
depends_on
meta-argument to create explicit dependencies - Implement
lifecycle
rules to control resource behavior - Handle resource replacement and deletion scenarios
- Optimize Terraform operations with proper dependency management
- Apply advanced lifecycle patterns for complex infrastructures
Prerequisites
- Understanding of Terraform resource dependencies
- Completed Tutorial 19: Resource Meta-Arguments (count, for_each)
- Knowledge of Terraform state management
Introduction
The depends_on
and lifecycle
meta-arguments provide fine-grained control over resource creation, updates, and deletion. These meta-arguments help manage complex dependency chains and protect critical resources from unintended changes.
The depends_on Meta-Argument
Explicit Dependencies
Terraform automatically detects most dependencies through resource references, but sometimes you need to create explicit dependencies that aren't apparent from resource attributes.
# variables.tf
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "enable_flow_logs" {
description = "Enable VPC flow logs"
type = bool
default = true
}
# main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "main-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
# This route table depends on both VPC and IGW existing
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "public-route-table"
}
# Explicit dependency ensures IGW is fully attached before creating routes
depends_on = [aws_internet_gateway.main]
}
# IAM role for VPC Flow Logs
resource "aws_iam_role" "flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "vpc-flow-logs-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "vpc-flow-logs.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "vpc-flow-logs-policy"
role = aws_iam_role.flow_logs[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams"
]
Effect = "Allow"
Resource = "*"
}
]
})
}
# CloudWatch Log Group for VPC Flow Logs
resource "aws_cloudwatch_log_group" "vpc_flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "/aws/vpc/flowlogs"
retention_in_days = 7
}
# VPC Flow Logs - explicit dependency on IAM role being fully configured
resource "aws_flow_log" "vpc" {
count = var.enable_flow_logs ? 1 : 0
iam_role_arn = aws_iam_role.flow_logs[0].arn
log_destination = aws_cloudwatch_log_group.vpc_flow_logs[0].arn
traffic_type = "ALL"
vpc_id = aws_vpc.main.id
# Ensure IAM role and policy are fully created before flow logs
depends_on = [
aws_iam_role_policy.flow_logs
]
}
Cross-Module Dependencies
# modules/database/main.tf
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-db-subnet-group"
subnet_ids = var.private_subnet_ids
tags = {
Name = "${var.project_name}-db-subnet-group"
}
}
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-database"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [var.db_security_group_id]
skip_final_snapshot = var.environment != "prod"
tags = {
Name = "${var.project_name}-database"
Environment = var.environment
}
}
# modules/application/main.tf
resource "aws_launch_template" "app" {
name_prefix = "${var.project_name}-app-"
image_id = var.ami_id
instance_type = var.instance_type
vpc_security_group_ids = [var.app_security_group_id]
user_data = base64encode(templatefile("${path.module}/user_data.sh", {
db_endpoint = var.db_endpoint
db_name = var.db_name
}))
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.project_name}-app-instance"
}
}
}
resource "aws_autoscaling_group" "app" {
name = "${var.project_name}-app-asg"
vpc_zone_identifier = var.private_subnet_ids
target_group_arns = [aws_lb_target_group.app.arn]
health_check_type = "ELB"
min_size = var.min_instances
max_size = var.max_instances
desired_capacity = var.desired_instances
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
# Ensure database is ready before launching application instances
depends_on = [var.database_ready]
tag {
key = "Name"
value = "${var.project_name}-app-asg"
propagate_at_launch = false
}
}
# Root main.tf
module "database" {
source = "./modules/database"
project_name = var.project_name
environment = var.environment
private_subnet_ids = module.network.private_subnet_ids
db_security_group_id = module.security.db_security_group_id
db_name = var.db_name
db_username = var.db_username
db_password = var.db_password
}
module "application" {
source = "./modules/application"
project_name = var.project_name
private_subnet_ids = module.network.private_subnet_ids
app_security_group_id = module.security.app_security_group_id
db_endpoint = module.database.db_endpoint
db_name = var.db_name
# Pass database instance as dependency
database_ready = module.database.db_instance
# Alternative: explicit dependency
depends_on = [module.database]
}
Complex Dependency Scenarios
# DNS and Certificate Management with Dependencies
resource "aws_acm_certificate" "app" {
domain_name = var.domain_name
subject_alternative_names = ["*.${var.domain_name}"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
# DNS validation records
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.app.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = data.aws_route53_zone.main.zone_id
}
# Certificate validation
resource "aws_acm_certificate_validation" "app" {
certificate_arn = aws_acm_certificate.app.arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
timeouts {
create = "5m"
}
}
# Load Balancer - depends on certificate being validated
resource "aws_lb" "app" {
name = "${var.project_name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids
enable_deletion_protection = var.environment == "prod"
tags = {
Name = "${var.project_name}-alb"
}
}
# HTTPS Listener - explicit dependency on certificate validation
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.app.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.app.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
# Ensure certificate is validated before creating HTTPS listener
depends_on = [aws_acm_certificate_validation.app]
}
# DNS record pointing to load balancer
resource "aws_route53_record" "app" {
zone_id = data.aws_route53_zone.main.zone_id
name = var.domain_name
type = "A"
alias {
name = aws_lb.app.dns_name
zone_id = aws_lb.app.zone_id
evaluate_target_health = true
}
# Ensure load balancer is ready before creating DNS record
depends_on = [aws_lb.app]
}
data "aws_route53_zone" "main" {
name = var.domain_name
private_zone = false
}
The lifecycle Meta-Argument
Create Before Destroy
Useful for resources that can't have downtime during replacement.
# Launch Template with create_before_destroy
resource "aws_launch_template" "app" {
name_prefix = "${var.project_name}-app-"
image_id = var.ami_id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.app.id]
user_data = base64encode(templatefile("${path.module}/user_data.sh", {
app_version = var.app_version
}))
# Create new launch template before destroying old one
lifecycle {
create_before_destroy = true
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.project_name}-app"
Version = var.app_version
}
}
}
# Security Group with create_before_destroy
resource "aws_security_group" "app" {
name_prefix = "${var.project_name}-app-"
vpc_id = var.vpc_id
description = "Security group for application instances"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
# Ensure new security group is created before destroying old one
lifecycle {
create_before_destroy = true
}
tags = {
Name = "${var.project_name}-app-security-group"
}
}
# Auto Scaling Group using the launch template
resource "aws_autoscaling_group" "app" {
name = "${var.project_name}-app-asg"
vpc_zone_identifier = var.private_subnet_ids
target_group_arns = [aws_lb_target_group.app.arn]
health_check_type = "ELB"
min_size = var.min_instances
max_size = var.max_instances
desired_capacity = var.desired_instances
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
# Rolling update configuration
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
}
}
tag {
key = "Name"
value = "${var.project_name}-app-instance"
propagate_at_launch = true
}
}
Prevent Destroy
Protect critical resources from accidental deletion.
# Critical database with prevent_destroy
resource "aws_db_instance" "production" {
identifier = "${var.project_name}-prod-db"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.r5.large"
allocated_storage = 100
max_allocated_storage = 1000
storage_encrypted = true
kms_key_id = aws_kms_key.db.arn
db_name = var.db_name
username = var.db_username
password = var.db_password
backup_retention_period = 30
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.project_name}-prod-db-final-snapshot"
# Prevent accidental destruction of production database
lifecycle {
prevent_destroy = true
}
tags = {
Name = "${var.project_name}-production-database"
Environment = "prod"
Critical = "true"
}
}
# S3 bucket for backups with prevent_destroy
resource "aws_s3_bucket" "backups" {
bucket = "${var.project_name}-backups-${random_id.bucket_suffix.hex}"
# Prevent accidental deletion of backup bucket
lifecycle {
prevent_destroy = true
}
tags = {
Name = "${var.project_name}-backups"
Purpose = "backups"
Critical = "true"
}
}
resource "aws_s3_bucket_versioning" "backups" {
bucket = aws_s3_bucket.backups.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_lifecycle_configuration" "backups" {
bucket = aws_s3_bucket.backups.id
rule {
id = "backup_lifecycle"
status = "Enabled"
noncurrent_version_expiration {
noncurrent_days = 90
}
abort_incomplete_multipart_upload {
days_after_initiation = 7
}
}
}
# KMS key for encryption
resource "aws_kms_key" "db" {
description = "KMS key for database encryption"
deletion_window_in_days = 30
# Protect encryption key from deletion
lifecycle {
prevent_destroy = true
}
tags = {
Name = "${var.project_name}-db-encryption-key"
}
}
resource "random_id" "bucket_suffix" {
byte_length = 4
}
Ignore Changes
Ignore changes to specific attributes that might be modified outside Terraform.
# Auto Scaling Group with ignore_changes for desired_capacity
resource "aws_autoscaling_group" "app" {
name = "${var.project_name}-app-asg"
vpc_zone_identifier = var.private_subnet_ids
target_group_arns = [aws_lb_target_group.app.arn]
health_check_type = "ELB"
min_size = var.min_instances
max_size = var.max_instances
desired_capacity = var.desired_instances
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
# Ignore changes to desired_capacity as it may be modified by auto-scaling
lifecycle {
ignore_changes = [desired_capacity]
}
tag {
key = "Name"
value = "${var.project_name}-app-asg"
propagate_at_launch = false
}
}
# EC2 instance with ignore_changes for tags that might be added by other systems
resource "aws_instance" "app" {
count = var.instance_count
ami = var.ami_id
instance_type = var.instance_type
subnet_id = var.subnet_ids[count.index % length(var.subnet_ids)]
vpc_security_group_ids = [aws_security_group.app.id]
# Base tags that Terraform manages
tags = {
Name = "${var.project_name}-app-${count.index + 1}"
Environment = var.environment
ManagedBy = "terraform"
}
# Ignore changes to tags that might be added by monitoring or other systems
lifecycle {
ignore_changes = [
tags["LastBackup"],
tags["MonitoringAgent"],
tags["SecurityScan"]
]
}
}
# RDS instance ignoring minor version updates
resource "aws_db_instance" "app" {
identifier = "${var.project_name}-app-db"
engine = "mysql"
engine_version = "8.0"
instance_class = var.db_instance_class
allocated_storage = var.db_storage
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password
auto_minor_version_upgrade = true
# Ignore minor version updates as they're applied automatically
lifecycle {
ignore_changes = [engine_version]
}
tags = {
Name = "${var.project_name}-app-database"
}
}
Replace Triggered By
Force replacement when specific values change.
# EC2 instance that recreates when user data changes
resource "aws_instance" "app" {
count = var.instance_count
ami = var.ami_id
instance_type = var.instance_type
subnet_id = var.subnet_ids[count.index % length(var.subnet_ids)]
vpc_security_group_ids = [aws_security_group.app.id]
user_data = templatefile("${path.module}/user_data.sh", {
app_version = var.app_version
configuration = var.app_configuration
environment = var.environment
})
# Force replacement when specific variables change
lifecycle {
replace_triggered_by = [
var.app_version,
var.force_replacement_trigger
]
}
tags = {
Name = "${var.project_name}-app-${count.index + 1}"
Version = var.app_version
DeployedAt = timestamp()
}
}
# Variable to trigger replacement
variable "force_replacement_trigger" {
description = "Change this value to force instance replacement"
type = string
default = "1"
}
# Lambda function that recreates when code changes
resource "aws_lambda_function" "api" {
filename = "api.zip"
function_name = "${var.project_name}-api"
role = aws_iam_role.lambda.arn
handler = "index.handler"
runtime = "nodejs18.x"
source_code_hash = data.archive_file.api.output_base64sha256
environment {
variables = var.lambda_environment_variables
}
# Force replacement when environment variables change significantly
lifecycle {
replace_triggered_by = [
var.lambda_force_redeploy
]
}
tags = {
Name = "${var.project_name}-api"
Version = var.api_version
}
}
data "archive_file" "api" {
type = "zip"
source_dir = "${path.module}/src"
output_path = "api.zip"
}
Advanced Lifecycle Patterns
Blue-Green Deployment Pattern
# variables.tf
variable "deployment_color" {
description = "Current deployment color (blue or green)"
type = string
default = "blue"
validation {
condition = contains(["blue", "green"], var.deployment_color)
error_message = "Deployment color must be either 'blue' or 'green'."
}
}
variable "previous_color" {
description = "Previous deployment color"
type = string
default = "green"
}
# main.tf
locals {
deployment_name = "${var.project_name}-${var.deployment_color}"
is_blue_active = var.deployment_color == "blue"
}
# Launch template for current deployment
resource "aws_launch_template" "current" {
name_prefix = "${local.deployment_name}-"
image_id = var.ami_id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.app.id]
user_data = base64encode(templatefile("${path.module}/user_data.sh", {
deployment_color = var.deployment_color
app_version = var.app_version
}))
lifecycle {
create_before_destroy = true
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "${local.deployment_name}-instance"
DeploymentColor = var.deployment_color
Version = var.app_version
}
}
}
# Auto Scaling Group for current deployment
resource "aws_autoscaling_group" "current" {
name = "${local.deployment_name}-asg"
vpc_zone_identifier = var.private_subnet_ids
health_check_type = "ELB"
min_size = var.min_instances
max_size = var.max_instances
desired_capacity = var.desired_instances
# Only attach to load balancer target group when this is the active deployment
target_group_arns = local.is_blue_active ? [aws_lb_target_group.blue.arn] : [aws_lb_target_group.green.arn]
launch_template {
id = aws_launch_template.current.id
version = "$Latest"
}
lifecycle {
create_before_destroy = true
}
tag {
key = "Name"
value = "${local.deployment_name}-asg"
propagate_at_launch = false
}
tag {
key = "DeploymentColor"
value = var.deployment_color
propagate_at_launch = true
}
}
# Target groups for blue-green deployment
resource "aws_lb_target_group" "blue" {
name = "${var.project_name}-blue-tg"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 5
interval = 30
path = "/health"
matcher = "200"
}
tags = {
Name = "${var.project_name}-blue-target-group"
DeploymentColor = "blue"
}
}
resource "aws_lb_target_group" "green" {
name = "${var.project_name}-green-tg"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 5
interval = 30
path = "/health"
matcher = "200"
}
tags = {
Name = "${var.project_name}-green-target-group"
DeploymentColor = "green"
}
}
# Load balancer listener that routes to active deployment
resource "aws_lb_listener" "app" {
load_balancer_arn = aws_lb.app.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = local.is_blue_active ? aws_lb_target_group.blue.arn : aws_lb_target_group.green.arn
}
}
Database Migration Pattern
# Database with migration-friendly lifecycle
resource "aws_db_instance" "app" {
identifier = "${var.project_name}-db-${var.db_version}"
engine = "mysql"
engine_version = var.db_engine_version
instance_class = var.db_instance_class
allocated_storage = var.db_storage
max_allocated_storage = var.db_max_storage
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password
backup_retention_period = 7
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"
# Create snapshot before any major changes
skip_final_snapshot = false
final_snapshot_identifier = "${var.project_name}-db-${var.db_version}-final"
# Allow engine version updates but create before destroy for major versions
lifecycle {
create_before_destroy = var.db_major_version_upgrade
ignore_changes = var.db_ignore_minor_versions ? [engine_version] : []
}
tags = {
Name = "${var.project_name}-database"
Version = var.db_version
Snapshot = "enabled"
}
}
# Database parameter group with versioning
resource "aws_db_parameter_group" "app" {
family = "${var.db_engine}${var.db_major_version}"
name = "${var.project_name}-db-params-${var.db_version}"
dynamic "parameter" {
for_each = var.db_parameters
content {
name = parameter.value.name
value = parameter.value.value
}
}
lifecycle {
create_before_destroy = true
}
tags = {
Name = "${var.project_name}-db-parameters"
Version = var.db_version
}
}
# Read replica with conditional creation
resource "aws_db_instance" "read_replica" {
count = var.create_read_replica ? 1 : 0
identifier = "${var.project_name}-db-replica"
replicate_source_db = aws_db_instance.app.identifier
instance_class = var.replica_instance_class
publicly_accessible = false
auto_minor_version_upgrade = false
# Read replica lifecycle management
lifecycle {
ignore_changes = [
replicate_source_db, # Don't change source after creation
]
}
tags = {
Name = "${var.project_name}-database-replica"
Type = "read-replica"
}
}
Best Practices
1. Dependency Management
# Good: Clear dependency chain
resource "aws_iam_role" "lambda" {
name = "${var.project_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" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
role = aws_iam_role.lambda.name
}
resource "aws_lambda_function" "api" {
filename = "api.zip"
function_name = "${var.project_name}-api"
role = aws_iam_role.lambda.arn
handler = "index.handler"
runtime = "nodejs18.x"
# Explicit dependency ensures role is fully configured
depends_on = [
aws_iam_role_policy_attachment.lambda_basic,
aws_cloudwatch_log_group.lambda
]
}
# Pre-create log group with retention
resource "aws_cloudwatch_log_group" "lambda" {
name = "/aws/lambda/${var.project_name}-api"
retention_in_days = 14
}
2. Lifecycle Rule Application
# Strategic use of lifecycle rules
resource "aws_launch_template" "app" {
name_prefix = "${var.project_name}-"
image_id = var.ami_id
instance_type = var.instance_type
# Use create_before_destroy for zero-downtime updates
lifecycle {
create_before_destroy = true
}
}
resource "aws_db_instance" "production" {
identifier = "${var.project_name}-prod-db"
# ... other configuration
# Protect critical production resources
lifecycle {
prevent_destroy = var.environment == "prod"
ignore_changes = var.environment == "prod" ? [password] : []
}
}
resource "aws_autoscaling_group" "app" {
name = "${var.project_name}-asg"
# ... other configuration
# Ignore capacity changes made by auto-scaling policies
lifecycle {
ignore_changes = [desired_capacity]
}
}
3. Conditional Lifecycle Rules
# Environment-specific lifecycle rules
resource "aws_instance" "app" {
count = var.instance_count
ami = var.ami_id
instance_type = var.instance_type
tags = {
Name = "${var.project_name}-app-${count.index + 1}"
Environment = var.environment
}
# Different lifecycle rules based on environment
dynamic "lifecycle" {
for_each = var.environment == "prod" ? [1] : []
content {
prevent_destroy = true
ignore_changes = [ami, user_data]
}
}
}
Key Takeaways
- Explicit Dependencies: Use
depends_on
when implicit dependencies aren't sufficient - Resource Protection: Use
prevent_destroy
for critical production resources - Zero Downtime: Use
create_before_destroy
for resources that can't have downtime - External Changes: Use
ignore_changes
for attributes modified outside Terraform - Forced Updates: Use
replace_triggered_by
to control when resources are recreated - Environment Awareness: Apply different lifecycle rules based on environment
Next Steps
- Tutorial 21: Learn about data sources and external data integration
- Practice implementing blue-green deployment patterns
- Experiment with complex dependency scenarios
- Review the Terraform Meta-Arguments Documentation