TFLAB18-Lifecycle Rules: Controlling Resource Behavior

Lifecycle Rules: Controlling Resource Behavior

🔧 Terraform Core ⭐ Intermediate lifecycle create_before_destroy prevent_destroy ignore_changes replace_triggered_by

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.

Your Objectives
  • 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
✓ prevent_destroy Error
│ 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.