Lifecycle #
By default, Terraform follows a simple cycle for every resource: create it if it doesn’t exist, change it if it differs, delete it if it’s no longer configured. But this default isn’t always right for every type of resource. A production database must not be deleted because of a typo. A load balancer can’t be down for even a second during an update. Some attributes are managed by other systems and must not be overwritten by Terraform. The lifecycle block gives you control to handle all of these cases.
Terraform’s Default Lifecycle #
Before looking at how to customize the lifecycle, it’s important to understand its default behavior.
flowchart TD
subgraph "Default Replace"
D1["Old resource\nin state"] --> D2["1. DESTROY\nthe old resource"]
D2 --> D3["2. CREATE\nthe new resource"]
D3 --> D4["Resource absent\nduring the process = DOWNTIME"]
end
subgraph "create_before_destroy"
C1["Old resource\nin state"] --> C2["1. CREATE\nthe new resource"]
C2 --> C3["2. DESTROY\nthe old resource"]
C3 --> C4["An active resource\nalways exists = ZERO DOWNTIME"]
end
style D4 fill:#ffebee,stroke:#c62828
style C4 fill:#e8f5e9,stroke:#2e7d32DEFAULT LIFECYCLE FOR EVERY RESOURCE:
CREATE → If the resource isn't in state yet
UPDATE → If the resource exists but the configuration differs
(two possibilities: update in-place or replace)
DESTROY → If the resource is in state but no longer in the configuration
Order when a replace (destroy + create) is needed:
1. Destroy the old resource
2. Create the new resource
This means there's a period where the resource doesn't exist at all
— potentially causing downtime for resources actively serving traffic.
create_before_destroy #
create_before_destroy flips the order of the replace operation: create the new resource first, then delete the old one. This minimizes downtime for resources that can’t afford to go down.
# SCENARIO: Updating the AMI on an instance serving production traffic
# ANTI-PATTERN: Default behavior — downtime during replace
resource "aws_instance" "web" {
ami = var.ami_id # Changing the AMI → forces replacement
instance_type = "t3.micro"
# Default order:
# 1. Destroy the old instance → traffic dies
# 2. Create the new instance → traffic comes back
# Downtime: ~20-30 seconds
}
# CORRECT: create_before_destroy — zero downtime replace
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
# New order:
# 1. Create the new instance → running
# 2. Destroy the old instance → no downtime
# (make sure there's a mechanism to shift traffic first,
# e.g. a load balancer attachment)
}
}
# create_before_destroy is very useful for SSL certificates
resource "aws_acm_certificate" "main" {
domain_name = var.domain_name
validation_method = "DNS"
lifecycle {
create_before_destroy = true
# A new certificate is created and validated before the old one is deleted
# No period where the domain lacks a valid certificate
}
}
prevent_destroy #
prevent_destroy makes Terraform refuse to delete a resource — apply will error before the destroy operation can run. This is the last line of protection for critical resources.
flowchart TD
A["terraform apply\ndetects a destroy"] --> B{"prevent_destroy\n= true?"}
B -->|"Yes"| C["❌ ERROR\nResource cannot\nbe destroyed"]
B -->|"No"| D["✅ Destroy\nruns normally"]
style C fill:#ffebee,stroke:#c62828
style D fill:#e8f5e9,stroke:#2e7d32# A production resource that must never be accidentally deleted
resource "aws_rds_instance" "production" {
identifier = "production-db"
engine = "postgres"
engine_version = "15.3"
instance_class = "db.t3.medium"
allocated_storage = 100
storage_encrypted = true
lifecycle {
prevent_destroy = true
}
}
# What happens if someone tries terraform destroy:
# ╷
# │ Error: Instance cannot be destroyed
# │
# │ on main.tf line 1, in resource "aws_rds_instance" "production":
# │ 1: resource "aws_rds_instance" "production" {
# │
# │ Resource aws_rds_instance.production has lifecycle.prevent_destroy
# │ set to true. To allow this object to be destroyed, remove or disable
# │ this attribute.
# ╵
prevent_destroyonly protects against Terraform-initiated operations. It doesn’t prevent direct deletion from the AWS console or CLI. For full protection, also use the resource’s built-in termination protection (e.g.deletion_protection = trueon RDS) and IAM policies restricting delete access.
ignore_changes #
ignore_changes tells Terraform to ignore differences in specific attributes — it won’t trigger an update even if their values change. Useful for attributes managed outside Terraform.
flowchart TD
A["Attribute change\ndetected in the plan"] --> B{"In\nignore_changes?"}
B -->|"Yes"| C["⬜ NO\naction — ignore"]
B -->|"No"| D["Depends on\nthe attribute's nature"]
D --> E["~ Update in-place\nor -/+ Replace"]
style C fill:#e8f5e9,stroke:#2e7d32
style E fill:#e3f2fd,stroke:#1565c0resource "aws_autoscaling_group" "web" {
name = "web-asg"
min_size = 2
max_size = 10
desired_capacity = 2
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
lifecycle {
ignore_changes = [
# desired_capacity is managed by the auto scaling policy — ignore it
desired_capacity,
# the "LastDeployedAt" tag is updated by the deployment pipeline — ignore it
tag,
]
}
}
# ignore_changes = all — ignore ALL changes to the resource's attributes
# Use with great care — this means Terraform will never
# update this resource after it's created
resource "aws_instance" "immutable" {
ami = var.ami_id
instance_type = "t3.micro"
lifecycle {
ignore_changes = all
# Useful for "immutable" resources created only once
# and never changed — configuration changes only apply
# to new instances, not existing ones
}
}
replace_triggered_by #
replace_triggered_by forces a resource to be replaced when another resource or attribute changes — even if there’s no direct change to the resource itself.
# SCENARIO: The instance must be replaced whenever the launch template changes
resource "aws_launch_template" "web" {
name_prefix = "web-"
image_id = var.ami_id
instance_type = "t3.micro"
}
resource "aws_instance" "web" {
# This instance doesn't directly depend on the launch template version,
# but we want it replaced every time the launch template changes
launch_template {
id = aws_launch_template.web.id
version = aws_launch_template.web.latest_version
}
lifecycle {
replace_triggered_by = [
aws_launch_template.web # Replace the instance if the launch template changes
]
}
}
Combining Lifecycle Rules #
Lifecycle rules can be combined as needed.
resource "aws_db_instance" "main" {
identifier = "app-database"
engine = "mysql"
instance_class = "db.t3.medium"
allocated_storage = 50
storage_encrypted = true
# Password is rotated by secrets manager — don't overwrite it
password = data.aws_secretsmanager_secret_version.db_password.secret_string
lifecycle {
# Create a new DB before deleting the old one during a replace
create_before_destroy = true
# Protect against accidental destroys
prevent_destroy = true
# Ignore password changes — managed by secrets manager
ignore_changes = [password]
}
}
| Lifecycle Rule | Function | When to Use |
|---|---|---|
create_before_destroy | Create the new one first, delete the old one after | Servers, certificates, load balancers |
prevent_destroy | Refuse destroy operations | Production databases, stateful resources |
ignore_changes | Ignore changes to specific attributes | Attributes managed outside Terraform |
ignore_changes = all | Ignore all changes | Immutable resources, created only once |
replace_triggered_by | Force a replace when another resource changes | Syncing versions between resources |
Lifecycle Rules in Production #
Lifecycle rules are very important for preventing data loss in production.
# Database: prevent_destroy to prevent accidental deletion
resource "aws_db_instance" "main" {
identifier = "production-db"
engine = "postgres"
instance_class = "db.t3.medium"
lifecycle {
prevent_destroy = true
# a terraform apply that tries to destroy → ERROR
# You must remove this lifecycle (with review) before you can destroy
}
}
# Instance: create_before_destroy for zero downtime
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
lifecycle {
create_before_destroy = true
# The new instance is created FIRST, then the old one is destroyed
# Avoids downtime
}
}
flowchart TD
subgraph NORMAL["Normal Replace"]
N1["Destroy old"] --> N2["Create new"]
N3["⚠️ Downtime!"]
end
subgraph CBD["create_before_destroy"]
C1["Create new"] --> C2["Destroy old"]
C3["✅ Zero downtime"]
end
style N3 fill:#ffebee,stroke:#c62828
style C3 fill:#e8f5e9,stroke:#2e7d32Lifecycle Edge Cases #
# Edge Case 1: prevent_destroy with import
# If you import a resource that already has prevent_destroy:
terraform import aws_db_instance.main i-12345
# The import succeeds, but future destroys will be blocked
# Edge Case 2: create_before_destroy with force-new
# If an attribute changes that forces a new resource:
# 1. Terraform creates the new instance
# 2. Waits for the new instance to "finish"
# 3. Deletes the old instance
# But: dependencies on the old instance can cause problems
# Edge Case 3: overly broad ignore_changes
lifecycle {
ignore_changes = all # IGNORE ALL CHANGES
# Only use for resources Terraform's management is truly complete on
}
Lifecycle Best Practices #
# BEST PRACTICE: When to use each lifecycle rule
# prevent_destroy: Resources that must never disappear
# - Production databases
# - S3 buckets with important data
# - KMS keys (encrypted data becomes inaccessible)
# - DNS zones
resource "aws_db_instance" "production" {
lifecycle {
prevent_destroy = true
}
}
# create_before_destroy: Zero-downtime replacement
# - EC2 instances behind a load balancer
# - Lambda functions
# - CloudWatch alarms
resource "aws_instance" "web" {
lifecycle {
create_before_destroy = true
}
}
# ignore_changes: Ignore specific changes
# - Auto-scaling group desired capacity
# - External monitoring that changes tags
resource "aws_autoscaling_group" "web" {
desired_capacity = 2
lifecycle {
ignore_changes = [desired_capacity]
}
}
Summary #
create_before_destroy = truefor resources that can’t afford downtime — create the new one before deleting the old one. Essential for servers, certificates, and load balancers.prevent_destroy = truefor critical resources like production databases — Terraform will error before it can delete them.prevent_destroydoesn’t protect against deletion outside Terraform — combine it with the resource’s built-in termination protection and IAM policies.ignore_changesfor attributes managed outside Terraform —desired_capacityon ASGs, rotated passwords, pipeline-updated tags.ignore_changes = allfor truly immutable resources — use with great care, Terraform will never update this resource.replace_triggered_byto force a replace based on changes to another resource that aren’t detected automatically.