Cloud  /  Terraform

IaC Terraform 50 guides · updated 2026

Infrastructure as code done right — providers, state, reusable modules, and the workflow patterns that keep multi-cloud deployments sane in 2026.

Terraform Lifecycle Meta-Arguments

The lifecycle block lets you customize how Terraform creates, updates, and destroys resources. It overrides the default behavior for specific resources — essential for zero-downtime deployments, protecting critical resources, and managing drift in resources that change outside Terraform.


create_before_destroy

By default, Terraform destroys the old resource before creating the new one. create_before_destroy reverses this — new resource first, then destroy the old. Critical for any resource that supports load — swapping an old server for a new one without downtime.

resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
lifecycle {
create_before_destroy = true
}
}
resource "aws_lb_listener_certificate" "api" {
listener_arn = aws_lb_listener.https.arn
certificate_arn = aws_acm_certificate.api.arn
lifecycle {
create_before_destroy = true # Add new cert before removing old one
}
}
# ACM certificates themselves need create_before_destroy
resource "aws_acm_certificate" "api" {
domain_name = "api.example.com"
validation_method = "DNS"
lifecycle {
create_before_destroy = true # Required for certificate rotation
}
}

prevent_destroy

Blocks terraform destroy and any plan that would destroy this resource. Use for databases, production state buckets, and other resources where accidental deletion is catastrophic.

resource "aws_db_instance" "production" {
identifier = "production-postgres"
instance_class = "db.r6g.large"
engine = "postgres"
lifecycle {
prevent_destroy = true # Any plan that would destroy this will fail
}
}
resource "aws_s3_bucket" "terraform_state" {
bucket = "mycompany-terraform-state"
lifecycle {
prevent_destroy = true
}
}

When Terraform tries to destroy a prevent_destroy resource:

Error: Instance cannot be destroyed
on main.tf line 5, in resource "aws_db_instance" "production":
5: lifecycle {
6: prevent_destroy = true
7: }
This object is protected from destroy action and cannot be destroyed.

ignore_changes

Tell Terraform to ignore changes to specific attributes — Terraform won’t try to restore them to the config value after an external change:

resource "aws_autoscaling_group" "app" {
name = "app-asg"
min_size = 2
max_size = 20
desired_capacity = 3 # Auto-scaling changes this — Terraform should ignore it
lifecycle {
ignore_changes = [desired_capacity] # Don't override auto-scaling decisions
}
}
resource "aws_ecs_service" "api" {
name = "api"
task_definition = aws_ecs_task_definition.api.arn
desired_count = 2
lifecycle {
ignore_changes = [
desired_count, # Auto-scaling manages this
task_definition, # Deployment pipeline updates this separately
]
}
}
# Ignore all changes to tags (common for resources with externally-managed tags)
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
lifecycle {
ignore_changes = [tags] # Tag management tool controls these
}
}
# Ignore all changes (not recommended — use sparingly)
resource "aws_instance" "imported_legacy" {
lifecycle {
ignore_changes = [all] # Terraform tracks but never modifies this resource
}
}

replace_triggered_by

Force replacement of a resource when another resource or attribute changes (Terraform 1.2+):

resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
lifecycle {
# Replace this instance when the launch template changes
replace_triggered_by = [
aws_launch_template.app.latest_version
]
}
}
resource "aws_apprunner_service" "api" {
service_name = "api"
lifecycle {
# Replace the App Runner service when the source image changes
replace_triggered_by = [
null_resource.image_push # A null_resource that triggers on image changes
]
}
}

precondition and postcondition (Terraform 1.2+)

Validate assumptions about the environment before and after apply:

resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
lifecycle {
precondition {
condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
error_message = "instance_type must be t3.micro, t3.small, or t3.medium."
}
precondition {
condition = data.aws_ami.ubuntu.architecture == "x86_64"
error_message = "AMI must be x86_64 architecture."
}
postcondition {
condition = self.public_ip != ""
error_message = "Instance must have a public IP — check subnet settings."
}
}
}
# Cross-resource validation
resource "aws_db_instance" "main" {
instance_class = var.db_instance_class
lifecycle {
precondition {
condition = !(var.environment == "production" && var.db_instance_class == "db.t3.micro")
error_message = "Production databases must not use db.t3.micro. Use db.r6g.large or larger."
}
}
}

Combining lifecycle Arguments

Multiple lifecycle arguments work together:

resource "aws_acm_certificate" "main" {
domain_name = var.domain_name
validation_method = "DNS"
lifecycle {
create_before_destroy = true # Safe certificate rotation
prevent_destroy = true # Don't accidentally delete a valid cert
# If the cert is re-issued by an external process, don't fight it
ignore_changes = [subject_alternative_names]
}
}