Terraform lifecycle: prevent_destroy, create_before_destroy, ignore_changes

· 2 min read · Terraform & OpenTofu Tutorials

Control how Terraform creates, updates and destroys a resource #

The lifecycle block is available in every resource and changes the default behavior of the plan. For a different approach to a related problem, see avoiding instance destroy in AWS.

prevent_destroy #

Makes any plan that would destroy the resource fail. Use it for databases, buckets with data and anything hard to recover:

prevent_destroy.tf
resource "aws_db_instance" "main" {
  identifier = "ditwl-pro-db"
  # ...

  lifecycle {
    prevent_destroy = true
  }
}

It does not protect against removing the resource and its lifecycle block from the code at the same time, so it is a safety net and not a security control. Use AWS deletion protection too.

create_before_destroy #

When a change forces replacement, Terraform destroys first and then creates. With create_before_destroy the new object is created first, so there is no gap:

create_before_destroy.tf
resource "aws_launch_template" "web" {
  name_prefix   = "web-"
  image_id      = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"

  lifecycle {
    create_before_destroy = true
  }
}

Both objects exist for a moment, so unique names must not collide. Use name_prefix instead of name, as in the example. It is common in auto scaling groups and load balancers.

ignore_changes #

Tells Terraform not to react to differences in some attributes. It is useful when another system changes them, for example autoscaling changing desired_capacity:

ignore_changes.tf
resource "aws_autoscaling_group" "web" {
  # ...
  desired_capacity = 2

  lifecycle {
    ignore_changes = [desired_capacity]
  }
}

Use ignore_changes = all only as a last resort, because Terraform will stop managing the resource.

replace_triggered_by #

Forces the replacement of a resource when another resource or attribute changes:

replace_triggered_by.tf
resource "aws_instance" "app" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"

  lifecycle {
    replace_triggered_by = [aws_security_group.app.id]
  }
}

precondition and postcondition #

Custom checks that fail the plan or apply with your own message:

conditions.tf
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
  }
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  lifecycle {
    precondition {
      condition     = data.aws_ami.ubuntu.architecture == "x86_64"
      error_message = "The AMI must be x86_64."
    }

    postcondition {
      condition     = self.public_ip != ""
      error_message = "The instance must have a public IP."
    }
  }
}

A precondition is evaluated before creating the resource and a postcondition after, with self pointing to the result.

Summary #

Argument Use it for
prevent_destroy Protect critical resources from accidental destruction
create_before_destroy Replace without downtime
ignore_changes Attributes changed outside of Terraform
replace_triggered_by Replace a resource when a dependency changes
precondition / postcondition Validate assumptions with clear errors

lifecycle arguments must be literal values: they cannot use variables or expressions. Related: import, moved and removed.

#Terraform #OpenTofu #AWS