Pulumi Protect and Retain are guardrails that prevent you from accidentally deleting cloud resources.

Let’s see this in action. Imagine you have a critical AWS S3 bucket. You want to make sure no one, not even you, can pulumi destroy it by mistake.

# Pulumi.yaml
name: my-secure-app
runtime: nodejs
description: A secure app with protected resources.

# Pulumi.<stack-name>.yaml
config:
  aws:region: us-east-1

Now, define your S3 bucket in TypeScript:

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// Protect this bucket from accidental deletion
const protectedBucket = new aws.s3.Bucket("protected-bucket", {
    // ... other bucket configurations
}, { protect: true });

// This bucket can be deleted normally
const normalBucket = new aws.s3.Bucket("normal-bucket", {
    // ... other bucket configurations
});

export const protectedBucketName = protectedBucket.id;
export const normalBucketName = normalBucket.id;

If you run pulumi up, both buckets are created.

Now, try to delete the protected bucket. You’ll see an error:

$ pulumi destroy
Previewing destroy (dev)

     Type                 Name             Plan
-    pulumi:pulumi:Stack  my-secure-app-dev  delete
-    aws:s3:Bucket        protected-bucket   delete
-    aws:s3:Bucket        normal-bucket      delete

Resources:
    - 3 to delete

Do you really want to destroy all resources? [y/N] y
Destroying (dev)

...

 pulumi:pulumi:Stack: (dev)
  error: 1 error(s) occurred:
    * aws:s3:Bucket (protected-bucket):
        cannot delete resource "arn:aws:s3:::protected-bucket-...", it is protected.

The protect: true option on the aws.s3.Bucket resource explicitly tells Pulumi that this resource should not be deleted. This protection applies to pulumi destroy operations. When Pulumi detects a destroy operation targeting a protected resource, it aborts the operation for that specific resource, preventing accidental data loss or service interruption.

You can also achieve similar protection using the retainOnDelete option, which is often used for resources that shouldn’t be deleted but might be replaced as part of an update. While protect: true is a hard stop for deletion, retainOnDelete: true allows the resource to be replaced during an update (meaning Pulumi deletes the old one and creates a new one), but it will not be deleted if the entire stack is destroyed. This is a subtle but important distinction.

Let’s look at the retainOnDelete option with an IAM role:

const retainedRole = new aws.iam.Role("retained-role", {
    assumeRolePolicy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
            Action: "sts:AssumeRole",
            Effect: "Allow",
            Principal: {
                Service: "ec2.amazonaws.com",
            },
        }],
    }),
}, { retainOnDelete: true });

If you run pulumi destroy with retainedRole having retainOnDelete: true, the role itself won’t be deleted. However, if you were to modify the role’s configuration and run pulumi up again, Pulumi might decide to replace it if the changes are not in-place updatable, and retainOnDelete would not prevent this replacement. The primary use case for retainOnDelete is to prevent a resource from being deleted when the entire stack is destroyed, effectively preserving it outside of Pulumi’s management for that specific deletion event.

The core problem these features solve is accidental infrastructure teardown. In complex cloud environments, a simple pulumi destroy can have cascading and devastating effects. protect: true acts as a hard lock on individual resources, ensuring they can only be removed through explicit code changes (removing the protect: true option) followed by a pulumi up to update the resource’s state, and then a pulumi destroy. This multi-step process prevents impulsive deletions.

retainOnDelete: true is more nuanced. It’s about ensuring that certain resources survive a full stack destruction, often because they are essential for recovery or have external dependencies that would be broken if the resource vanished. Think of a critical IAM user or a database snapshot. You don’t want pulumi destroy to wipe them out, but you might eventually want to update their configuration. retainOnDelete ensures they aren’t swept away by a broad destroy command.

The internal mechanism is that Pulumi tags protected resources with a special metadata flag. During a destroy operation, before attempting to delete any resource, Pulumi checks this metadata. If the flag is present, the destroy command for that resource is skipped, and an error is logged. For retainOnDelete, the resource is marked such that if the resource is being destroyed as part of a stack destruction, Pulumi will instead mark it for retention and not perform the delete operation, but it may still be replaced if the update operation necessitates it.

A common misconception is that protect: true stops all changes. This isn’t true. You can still update the configuration of a protected resource. For example, if you wanted to add tags to the protected-bucket:

const protectedBucket = new aws.s3.Bucket("protected-bucket", {
    tags: {
        "Environment": "Production",
    },
}, { protect: true });

Running pulumi up would successfully add the tags. The protect: true option only prevents the resource from being deleted.

The next hurdle you’ll likely encounter is understanding how to manage resource lifecycles across different environments, especially when some resources need to be retained and others need to be cleanly provisioned and de-provisioned.

Want structured learning?

Take the full Pulumi course →