Introduction
Terraform lifecycle rules control what happens when a resource needs to be created, updated, or destroyed. They’re essential for production infrastructure — preventing accidental deletion, enabling zero-downtime deployments, and handling resources that are modified outside of Terraform.
Quick Reference
resource "aws_instance" "web" {
# ... resource config ...
lifecycle {
create_before_destroy = true # Create new before destroying old
prevent_destroy = true # Block terraform destroy
ignore_changes = [tags] # Ignore external changes
replace_triggered_by = [null_resource.trigger.id] # Force replacement
}
}
All Four Lifecycle Rules
1. prevent_destroy — Protect Critical Resources
prevent_destroy = true makes Terraform refuse to destroy a resource. Any terraform destroy or plan that would destroy the resource produces an error instead.
resource "aws_db_instance" "production" {
identifier = "prod-database"
engine = "postgresql"
instance_class = "db.r6g.large"
allocated_storage = 100
lifecycle {
prevent_destroy = true
}
}
If you try to destroy it:
Error: Instance cannot be destroyed
on main.tf line 1:
1: resource "aws_db_instance" "production" {
Resource aws_db_instance.production has lifecycle.prevent_destroy set,
but the plan calls for this resource to be destroyed.
Use for:
- Production databases
- S3 buckets with important data
- KMS encryption keys
- IAM roles used by multiple services
- DNS zones
To actually destroy it: Set prevent_destroy = false first, then run terraform destroy.
2. create_before_destroy — Zero-Downtime Updates
When Terraform needs to replace a resource (destroy + create), it normally destroys first, then creates. create_before_destroy = true reverses this order: create the new resource first, verify it works, then destroy the old one.
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
}
}
Timeline without create_before_destroy:
1. Destroy old instance → DOWNTIME STARTS
2. Create new instance → DOWNTIME ENDS
Timeline with create_before_destroy:
1. Create new instance → No downtime
2. Verify new instance
3. Destroy old instance → Seamless cutover
Common use cases:
Auto Scaling Groups:
resource "aws_launch_template" "app" {
name_prefix = "app-"
image_id = var.ami_id
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
}
}
Security Groups (referenced by instances):
resource "aws_security_group" "web" {
name_prefix = "web-"
vpc_id = aws_vpc.main.id
lifecycle {
create_before_destroy = true
}
}
Watch out for naming conflicts: If a resource has a unique name (like an S3 bucket or IAM role), creating a new one before destroying the old one will fail with a “name already exists” error. Use name_prefix instead of name to avoid this.
3. ignore_changes — Handle External Modifications
ignore_changes tells Terraform to ignore changes to specific attributes. If something modifies the resource outside of Terraform (AWS Console, another tool, auto-scaling), Terraform won’t try to revert it.
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
lifecycle {
ignore_changes = [tags, ami]
}
}
Now if someone adds tags in the AWS Console or the AMI changes, terraform plan won’t show a diff.
Ignore all attributes:
lifecycle {
ignore_changes = all
}
This makes Terraform manage only the initial creation — after that, it ignores all changes. Useful for resources you want Terraform to create but not manage day-to-day.
Common use cases:
Auto-scaled instance counts:
resource "aws_autoscaling_group" "app" {
min_size = 2
max_size = 10
desired_capacity = 2
lifecycle {
ignore_changes = [desired_capacity]
}
}
ECS services with auto-scaling:
resource "aws_ecs_service" "app" {
name = "app"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 2
lifecycle {
ignore_changes = [desired_count, task_definition]
}
}
Tags managed by AWS (auto-tagging, Service Catalog):
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
lifecycle {
ignore_changes = [tags["aws:autoscaling:groupName"], tags["kubernetes.io/cluster"]]
}
}
4. replace_triggered_by — Force Replacement on External Change
Added in Terraform 1.2. Forces a resource to be replaced when another resource or attribute changes:
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
lifecycle {
replace_triggered_by = [
aws_security_group.web.id,
null_resource.force_replace.id
]
}
}
resource "null_resource" "force_replace" {
triggers = {
deploy_version = var.deploy_version
}
}
When var.deploy_version changes, null_resource.force_replace is recreated, which triggers replacement of aws_instance.web.
Use cases:
- Force EC2 replacement when security group changes
- Trigger redeployment based on a version variable
- Chain replacements across dependent resources
Combining Lifecycle Rules
You can use multiple rules together:
# Production database: protected, ignores external tag changes
resource "aws_db_instance" "production" {
identifier = "prod-database"
engine = "postgresql"
instance_class = "db.r6g.large"
lifecycle {
prevent_destroy = true
ignore_changes = [tags, engine_version]
}
}
# Web server: zero-downtime replacement, ignore auto-scaling tags
resource "aws_launch_template" "web" {
name_prefix = "web-"
image_id = var.ami_id
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
ignore_changes = [tags]
}
}
Production Patterns
Pattern 1: Protected Database with Managed Backups
resource "aws_db_instance" "main" {
identifier = "production-db"
engine = "postgresql"
engine_version = "15.4"
instance_class = "db.r6g.large"
allocated_storage = 100
backup_retention_period = 7
deletion_protection = true # AWS-level protection
lifecycle {
prevent_destroy = true # Terraform-level protection
ignore_changes = [engine_version] # Allow RDS auto minor upgrades
}
}
Pattern 2: Blue-Green Deployment
resource "aws_lb_target_group" "app" {
name_prefix = "app-"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
path = "/health"
}
lifecycle {
create_before_destroy = true
}
}
Pattern 3: Kubernetes-Managed Resources
resource "aws_eks_node_group" "workers" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "workers"
instance_types = ["t3.medium"]
scaling_config {
desired_size = 3
min_size = 2
max_size = 10
}
lifecycle {
ignore_changes = [scaling_config[0].desired_size]
}
}
Common Errors
“Instance cannot be destroyed”
You set prevent_destroy = true but are trying to remove the resource. Options:
- Set
prevent_destroy = false, apply, then remove - Use
terraform state rmto remove from state without destroying
“create_before_destroy” naming conflict
Error: A resource with the ID "my-security-group" already exists
Use name_prefix instead of name:
resource "aws_security_group" "web" {
name_prefix = "web-" # Not name = "web-sg"
lifecycle {
create_before_destroy = true
}
}
ignore_changes not working with maps
For map/object attributes, reference the whole attribute or specific keys:
# Ignore all tags
ignore_changes = [tags]
# Ignore specific tag key
ignore_changes = [tags["ManagedBy"]]
Hands-On Courses
Learn by doing with interactive courses on CopyPasteLearn:
Conclusion
Lifecycle rules are essential for production Terraform. Use prevent_destroy to protect databases and critical resources, create_before_destroy for zero-downtime deployments, ignore_changes to coexist with auto-scaling and external tools, and replace_triggered_by to chain resource replacements. Most production resources should have at least one lifecycle rule configured.




