Skip to main content
By default, Pulumi manages the full lifecycle of every resource: create on first apply, update in place when attributes change, destroy on removal. This is correct most of the time. But production infrastructure has exceptions. A production database should never be destroyed by accident — even if someone runs ubx destroy or removes the block. An autoscaling group’s desired_capacity is managed at runtime by AWS Auto Scaling, and Pulumi shouldn’t try to reset it on every apply. An EC2 instance that requires zero downtime should have its replacement provisioned before the old one is torn down. The lifecycle block is where you express these exceptions — three settings that override the default resource management behavior in precisely targeted ways.

What you’ll learn

  • prevent_destroy — block accidental destruction of critical resources
  • ignore_changes — suppress drift on attributes managed outside ubx
  • create_before_destroy — provision a replacement before tearing down the original

Why this matters

lifecycle settings are evaluated by Pulumi at apply time, not at compile time. They’re metadata instructions to the Pulumi engine, not ubx expressions. This means they don’t interact with the Pending<T> system and can’t reference resource outputs — they take literal values only.

The source code

# prevent_destroy — blocks ubx destroy and any plan that would delete this resource.
unit "aws_rds_instance" "primary" {
  identifier          = "myapp-${input.env}-primary"
  allocated_storage   = 20
  engine              = "postgres"
  engine_version      = "15"
  instance_class      = "db.t3.micro"
  username            = "admin"
  password            = "placeholder"
  skip_final_snapshot = false

  lifecycle {
    prevent_destroy = false  # set to true in prod to block accidental deletion
  }
}

# ignore_changes — suppress drift on attributes managed outside ubx.
# desired_capacity is managed by Auto Scaling at runtime; don't reset it.
unit "aws_autoscaling_group" "workers" {
  name             = "workers-${input.env}"
  min_size         = 1
  max_size         = 10
  desired_capacity = 2

  lifecycle {
    ignore_changes = ["desired_capacity", "tags"]
  }
}

# create_before_destroy — provision replacement before tearing down original.
# Ensures zero downtime for resources that need replacement on update.
unit "aws_instance" "api" {
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.small"

  tags = {
    Name = "api-${input.env}"
  }

  lifecycle {
    create_before_destroy = true
  }
}

How it works

1

prevent_destroy is a Pulumi engine flag

When prevent_destroy = true, Pulumi marks the resource with a destruction guard. Any plan that would destroy it — ubx destroy, a block removal, or an attribute change that forces replacement — errors before executing. The only way to remove the resource is to set prevent_destroy = false first, then apply, then remove the block.
2

ignore_changes suppresses drift detection

Attributes listed in ignore_changes are excluded from Pulumi’s diff calculation. Even if the live value differs from what’s in .iac, Pulumi won’t generate an update for those attributes. This is essential for attributes managed by external systems — autoscalers, Kubernetes controllers, manual operator changes.
3

create_before_destroy changes the replacement order

Normally Pulumi destroys then creates (to avoid naming conflicts). create_before_destroy = true reverses this: the replacement is provisioned first, then the old resource is destroyed. Required for resources where the old instance must remain available until the new one is ready.

What ubx generates

// Generated by ubx — do not edit
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const env = config.get("env") || "dev";

const primary = new aws.rds.Instance("primary", {
    identifier: `myapp-${env}-primary`,
    allocatedStorage: 20,
    engine: "postgres",
    engineVersion: "15",
    instanceClass: "db.t3.micro",
    username: "admin",
    password: "placeholder",
    skipFinalSnapshot: false,
}, { retainOnDelete: false }); // prevent_destroy maps to retainOnDelete

const workers = new aws.autoscaling.Group("workers", {
    name: `workers-${env}`,
    minSize: 1,
    maxSize: 10,
    desiredCapacity: 2,
}, { ignoreChanges: ["desiredCapacity", "tags"] });

const api = new aws.ec2.Instance("api", {
    ami: "ami-0c02fb55956c7d316",
    instanceType: "t3.small",
    tags: { Name: `api-${env}` },
}, { deleteBeforeReplace: false }); // create_before_destroy = true

export const dbIdentifier = primary.id;

Common mistakes

prevent_destroy = true in a ubx block does NOT prevent you from removing the block — it only blocks plans that would execute a destroy. To actually protect a resource, you must keep the block present with prevent_destroy = true. Removing the block and running ubx apply will still destroy the resource if prevent_destroy is not set.
Use ignore_changes sparingly — it hides drift between your .iac source and reality. If you find yourself adding many attributes to ignore_changes, it may be a sign that the resource is better managed outside ubx entirely, with a data block to read its outputs.

Run it

ubx validate
ubx plan             # see lifecycle metadata in plan output
ubx apply

What you learned

prevent_destroy = true blocks any plan that would destroy the resource
ignore_changes suppresses drift on attributes managed by external systems
create_before_destroy = true provisions the replacement before destroying the original

Next steps

Timeouts and ignore_changes

Override provider-default wait times

lifecycle reference

Full lifecycle block syntax