Lifecycle Rules: Controlling Resource Behavior
Scenario
Your production S3 bucket must never be accidentally deleted by terraform destroy. When renaming a bucket, you want the new one created before the old one is removed (zero-downtime). And your ops team sometimes adds tags manually via the console — you don't want Terraform to remove those tags on the next apply.
Lifecycle rules give you fine-grained control over how Terraform handles resource creation, destruction, and updates.
- Use create_before_destroy = true to ensure a replacement resource is created before the old one is destroyed.
- Use prevent_destroy = true to block accidental deletion.
- Use ignore_changes to prevent Terraform from reverting manual tag changes.
- Use replace_triggered_by to force replacement when an unrelated value changes.
Additional Context
Lifecycle rules live inside a lifecycle {} block within a resource. They modify Terraform's default behavior but don't change the cloud resource's actual configuration.
ignore_changes accepts a list of attribute names. ignore_changes = all tells Terraform to never update the resource after creation — useful for resources managed partially by other tools.
main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" { region = "us-east-1" }
data "aws_caller_identity" "current" {}
variable "environment" { default = "dev" }
variable "trigger_val" { default = "v1" }
# ─── create_before_destroy ───
# When bucket name changes, the NEW bucket is created first,
# then the OLD one is destroyed. Prevents downtime.
resource "aws_s3_bucket" "app" {
bucket = "lifecycle-demo-${var.environment}-${data.aws_caller_identity.current.account_id}"
tags = { Name = "lifecycle-demo", ManagedBy = "terraform" }
lifecycle {
create_before_destroy = true
}
}
# ─── prevent_destroy ───
# Terraform will ERROR if you try to destroy this resource.
# Remove prevent_destroy before running terraform destroy.
resource "aws_s3_bucket" "critical" {
bucket = "critical-data-${data.aws_caller_identity.current.account_id}"
tags = { Name = "critical-data", ManagedBy = "terraform" }
lifecycle {
prevent_destroy = true
}
}
# ─── ignore_changes ───
# Terraform will NOT revert manual tag changes made outside Terraform.
resource "aws_s3_bucket" "shared" {
bucket = "shared-bucket-${data.aws_caller_identity.current.account_id}"
tags = { Name = "shared-bucket", ManagedBy = "terraform" }
lifecycle {
# If someone adds/changes tags via the console, Terraform ignores it
ignore_changes = [tags]
}
}
# ─── replace_triggered_by ───
# Force-replaces this bucket whenever trigger_val changes,
# even though trigger_val isn't used in the resource config.
resource "aws_s3_bucket" "cache" {
bucket = "cache-bucket-${data.aws_caller_identity.current.account_id}"
tags = { Name = "cache-bucket", ManagedBy = "terraform" }
lifecycle {
replace_triggered_by = [terraform_data.trigger.output]
}
}
resource "terraform_data" "trigger" {
input = var.trigger_val
}outputs.tf
output "app_bucket" { value = aws_s3_bucket.app.id }
output "critical_bucket" { value = aws_s3_bucket.critical.id }
output "shared_bucket" { value = aws_s3_bucket.shared.id }
output "cache_bucket" { value = aws_s3_bucket.cache.id }Workflow Commands
terraform init && terraform apply
# Test prevent_destroy — this will ERROR
terraform destroy
# Error: Instance cannot be destroyed. Resource aws_s3_bucket.critical has prevent_destroy set.
# Test ignore_changes — add a tag via AWS CLI, then re-apply
aws s3api put-bucket-tagging --bucket "shared-bucket-..." \
--tagging 'TagSet=[{Key=ManualTag,Value=added-by-ops}]'
terraform plan # No changes — Terraform ignores the tag difference
# Test replace_triggered_by — change the trigger value
terraform apply -var="trigger_val=v2"
# cache_bucket is destroyed and recreated
# To actually destroy: comment out prevent_destroy first
terraform destroy│ Error: Instance cannot be destroyed
│
│ on main.tf line 29:
│ 29: resource "aws_s3_bucket" "critical" {
│
│ Resource aws_s3_bucket.critical has lifecycle.prevent_destroy
│ set, but the plan calls for this resource to be destroyed.
