CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-pulumi--aws

A Pulumi package for creating and managing Amazon Web Services (AWS) cloud resources with infrastructure-as-code.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

resource-lifecycle.mddocs/guides/

Resource Lifecycle Guide

This guide covers the complete lifecycle of AWS resources in Pulumi, including creation, updates, deletion, import, protection, and understanding replace vs update behavior.

Table of Contents

  • Creating Resources
  • Updating Resources
  • Deleting Resources
  • Import Existing Resources
  • Protecting Resources
  • Replace vs Update Behavior

Creating Resources

Basic Resource Creation

Create AWS resources using the Pulumi AWS SDK:

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

// Simple resource creation
const bucket = new aws.s3.Bucket("my-bucket", {
    acl: "private",
    tags: {
        Environment: "dev",
        Project: "example",
    },
});

export const bucketName = bucket.id;
export const bucketArn = bucket.arn;

Resource Creation with Dependencies

Create resources with explicit dependencies:

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

// Create VPC
const vpc = new aws.ec2.Vpc("app-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    enableDnsSupport: true,
});

// Create subnet (implicitly depends on VPC)
const subnet = new aws.ec2.Subnet("app-subnet", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    availabilityZone: "us-west-2a",
});

// Create internet gateway
const igw = new aws.ec2.InternetGateway("app-igw", {
    vpcId: vpc.id,
});

// Create route table
const routeTable = new aws.ec2.RouteTable("app-rt", {
    vpcId: vpc.id,
});

// Create route (explicit dependency on IGW)
const route = new aws.ec2.Route("internet-route", {
    routeTableId: routeTable.id,
    destinationCidrBlock: "0.0.0.0/0",
    gatewayId: igw.id,
}, {
    dependsOn: [igw],
});

// Associate route table with subnet
const routeTableAssociation = new aws.ec2.RouteTableAssociation("app-rta", {
    subnetId: subnet.id,
    routeTableId: routeTable.id,
});

Resource Creation with Transformations

Apply transformations during resource creation:

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

// Apply naming convention transformation
const bucket = new aws.s3.Bucket("data-bucket", {
    bucket: pulumi.interpolate`${pulumi.getProject()}-${pulumi.getStack()}-data`,
    acl: "private",
}, {
    transformations: [(args) => {
        // Add default tags to all resources
        if (args.type === "aws:s3/bucket:Bucket") {
            args.props.tags = {
                ...args.props.tags,
                ManagedBy: "pulumi",
                CreatedAt: new Date().toISOString(),
            };
        }
        return {
            props: args.props,
            opts: args.opts,
        };
    }],
});

Bulk Resource Creation

Create multiple similar resources:

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

// Create multiple subnets across AZs
const azs = ["us-west-2a", "us-west-2b", "us-west-2c"];
const vpc = new aws.ec2.Vpc("app-vpc", {
    cidrBlock: "10.0.0.0/16",
});

const subnets = azs.map((az, index) => {
    return new aws.ec2.Subnet(`subnet-${az}`, {
        vpcId: vpc.id,
        cidrBlock: `10.0.${index}.0/24`,
        availabilityZone: az,
        tags: {
            Name: `subnet-${az}`,
            AZ: az,
        },
    });
});

export const subnetIds = subnets.map(s => s.id);

Updating Resources

In-Place Updates

Some properties can be updated without replacing the resource:

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

// Initial creation
const bucket = new aws.s3.Bucket("my-bucket", {
    acl: "private",
    tags: {
        Environment: "dev",
    },
});

// Update tags (in-place update, no replacement)
// Just modify the code and run `pulumi up`
const bucketUpdated = new aws.s3.Bucket("my-bucket", {
    acl: "private",
    tags: {
        Environment: "dev",
        Owner: "team-a", // Added new tag
        CostCenter: "engineering",
    },
});

// Update versioning (in-place update)
const bucketWithVersioning = new aws.s3.Bucket("my-bucket", {
    acl: "private",
    versioning: {
        enabled: true, // Enable versioning
    },
    tags: {
        Environment: "dev",
    },
});

Updates Requiring Replacement

Some property changes require resource replacement:

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

// Initial EC2 instance
const instance = new aws.ec2.Instance("web-server", {
    instanceType: "t3.micro",
    ami: "ami-0c55b159cbfafe1f0",
    availabilityZone: "us-west-2a",
});

// Changing AMI requires replacement (creates new instance, deletes old)
const instanceWithNewAmi = new aws.ec2.Instance("web-server", {
    instanceType: "t3.micro",
    ami: "ami-0abcdef1234567890", // Different AMI
    availabilityZone: "us-west-2a",
});

// Changing availability zone requires replacement
const instanceInDifferentAz = new aws.ec2.Instance("web-server", {
    instanceType: "t3.micro",
    ami: "ami-0c55b159cbfafe1f0",
    availabilityZone: "us-west-2b", // Different AZ
});

Controlled Updates with Aliases

Rename resources without replacement:

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

// Original resource
// const oldBucket = new aws.s3.Bucket("old-name", { ... });

// Rename resource without replacement
const newBucket = new aws.s3.Bucket("new-name", {
    acl: "private",
}, {
    aliases: [{ name: "old-name" }],
});

// Pulumi will recognize this as a rename, not a replacement

Ignore Changes

Prevent Pulumi from updating certain properties:

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

// Ignore changes to tags (useful when tags are managed externally)
const bucket = new aws.s3.Bucket("my-bucket", {
    acl: "private",
    tags: {
        Environment: "dev",
    },
}, {
    ignoreChanges: ["tags"],
});

// Ignore desired count changes (useful for auto-scaling)
const service = new aws.ecs.Service("app-service", {
    cluster: "cluster-id",
    taskDefinition: "task-def-arn",
    desiredCount: 2,
}, {
    ignoreChanges: ["desiredCount"],
});

// Ignore multiple properties
const instance = new aws.ec2.Instance("app-instance", {
    instanceType: "t3.micro",
    ami: "ami-0c55b159cbfafe1f0",
    userData: "#!/bin/bash\necho 'Hello'",
    tags: {
        Name: "app-instance",
    },
}, {
    ignoreChanges: ["userData", "tags"],
});

Replace Triggered Updates

Force replacement when needed:

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

const config = new pulumi.Config();
const forceReplace = config.getBoolean("forceReplace") || false;

const instance = new aws.ec2.Instance("app-instance", {
    instanceType: "t3.micro",
    ami: "ami-0c55b159cbfafe1f0",
}, {
    replaceOnChanges: ["userData"],
});

// Or use pulumi up --replace urn:pulumi:stack::project::aws:ec2/instance:Instance::app-instance

Deleting Resources

Basic Deletion

Delete resources by removing them from code:

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

// Remove this resource from code
// const bucket = new aws.s3.Bucket("old-bucket");

// Run `pulumi up` to delete the resource

Deletion with Dependencies

Pulumi automatically handles deletion order:

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

// When deleting VPC, Pulumi automatically deletes in correct order:
// 1. Instances
// 2. Security groups
// 3. Subnets
// 4. Route tables
// 5. Internet gateway
// 6. VPC

// Just remove all related resources from code and run `pulumi up`

Retain on Delete

Keep resource even when removed from code:

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

// Production database - retain on delete
const database = new aws.rds.Instance("prod-db", {
    engine: "postgres",
    instanceClass: "db.t3.large",
    allocatedStorage: 100,
    username: "admin",
    password: pulumi.secret("db-password"),
}, {
    retainOnDelete: true,
});

// When removed from code, resource is removed from state but not deleted

Delete Protection

Protect critical resources from deletion:

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

// Protected resource cannot be deleted
const bucket = new aws.s3.Bucket("critical-data", {
    acl: "private",
}, {
    protect: true,
});

// To delete, must first remove protection:
// 1. Change protect to false
// 2. Run pulumi up
// 3. Remove resource from code
// 4. Run pulumi up again

Conditional Deletion

Use deleteBeforeReplace for special cases:

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

// Delete old resource before creating new one
const certificate = new aws.acm.Certificate("cert", {
    domainName: "example.com",
    validationMethod: "DNS",
}, {
    deleteBeforeReplace: true,
});

// Useful when resources have name constraints or quotas

Import Existing Resources

Basic Import

Import existing AWS resources into Pulumi:

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

// Step 1: Define the resource in code
const existingBucket = new aws.s3.Bucket("existing-bucket", {
    bucket: "my-existing-bucket-name",
    acl: "private",
}, {
    import: "my-existing-bucket-name",
});

// Step 2: Run `pulumi up` to import the resource
// Pulumi will import the bucket and manage it going forward

Import with Resource ID

Import using resource-specific identifiers:

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

// Import VPC by ID
const vpc = new aws.ec2.Vpc("imported-vpc", {
    cidrBlock: "10.0.0.0/16",
}, {
    import: "vpc-0123456789abcdef0",
});

// Import security group by ID
const sg = new aws.ec2.SecurityGroup("imported-sg", {
    vpcId: vpc.id,
    name: "existing-sg",
}, {
    import: "sg-0123456789abcdef0",
});

// Import RDS instance by identifier
const db = new aws.rds.Instance("imported-db", {
    identifier: "my-database",
    engine: "postgres",
    instanceClass: "db.t3.micro",
}, {
    import: "my-database",
});

Bulk Import

Import multiple resources:

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

// Import multiple S3 buckets
const bucketNames = [
    "bucket-1",
    "bucket-2",
    "bucket-3",
];

const buckets = bucketNames.map(name => {
    return new aws.s3.Bucket(name, {
        bucket: name,
    }, {
        import: name,
    });
});

// Import multiple EC2 instances
const instanceIds = [
    "i-0123456789abcdef0",
    "i-0123456789abcdef1",
    "i-0123456789abcdef2",
];

const instances = instanceIds.map((id, index) => {
    return new aws.ec2.Instance(`imported-instance-${index}`, {
        instanceType: "t3.micro",
        ami: "ami-0c55b159cbfafe1f0",
    }, {
        import: id,
    });
});

Import and Modify

Import existing resource and apply changes:

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

// Step 1: Import the resource
const bucket = new aws.s3.Bucket("my-bucket", {
    bucket: "existing-bucket-name",
    acl: "private",
}, {
    import: "existing-bucket-name",
});

// Step 2: After import, modify the resource
const bucketWithVersioning = new aws.s3.Bucket("my-bucket", {
    bucket: "existing-bucket-name",
    acl: "private",
    versioning: {
        enabled: true, // Add versioning
    },
    tags: {
        ManagedBy: "pulumi", // Add tags
    },
});

// Step 3: Run `pulumi up` to apply changes

Import from Terraform State

Migrate from Terraform to Pulumi:

# Use pulumi import command for each resource
pulumi import aws:s3/bucket:Bucket my-bucket my-bucket-name
pulumi import aws:ec2/vpc:Vpc my-vpc vpc-0123456789abcdef0
pulumi import aws:ec2/subnet:Subnet my-subnet subnet-0123456789abcdef0
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// After running import commands, define resources in code
const bucket = new aws.s3.Bucket("my-bucket", {
    bucket: "my-bucket-name",
    acl: "private",
});

const vpc = new aws.ec2.Vpc("my-vpc", {
    cidrBlock: "10.0.0.0/16",
});

const subnet = new aws.ec2.Subnet("my-subnet", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
});

Protecting Resources

Basic Protection

Prevent accidental deletion:

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

// Protect production database
const database = new aws.rds.Instance("prod-db", {
    engine: "postgres",
    instanceClass: "db.t3.large",
    allocatedStorage: 100,
    username: "admin",
    password: pulumi.secret("db-password"),
}, {
    protect: true, // Cannot be deleted while protected
});

// Attempting to delete will result in error
// Must first remove protection, then delete

Stack-Based Protection

Apply protection based on stack:

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

const stack = pulumi.getStack();
const isProduction = stack === "production";

// Protect resources in production only
const bucket = new aws.s3.Bucket("data-bucket", {
    acl: "private",
}, {
    protect: isProduction,
});

const database = new aws.rds.Instance("app-db", {
    engine: "postgres",
    instanceClass: "db.t3.micro",
    username: "admin",
    password: pulumi.secret("db-password"),
}, {
    protect: isProduction,
    retainOnDelete: isProduction,
});

Resource-Specific Protection

Apply AWS-level protection:

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

// RDS deletion protection
const database = new aws.rds.Instance("protected-db", {
    engine: "postgres",
    instanceClass: "db.t3.medium",
    deletionProtection: true, // AWS-level protection
    skipFinalSnapshot: false,
    finalSnapshotIdentifier: "final-snapshot",
    username: "admin",
    password: pulumi.secret("db-password"),
}, {
    protect: true, // Pulumi-level protection
});

// S3 bucket with object lock
const bucket = new aws.s3.Bucket("protected-bucket", {
    objectLockConfiguration: {
        objectLockEnabled: "Enabled",
        rule: {
            defaultRetention: {
                mode: "GOVERNANCE",
                days: 30,
            },
        },
    },
}, {
    protect: true,
});

// EC2 instance with termination protection
const instance = new aws.ec2.Instance("protected-instance", {
    instanceType: "t3.micro",
    ami: "ami-0c55b159cbfafe1f0",
    disableApiTermination: true, // Termination protection
}, {
    protect: true,
});

Conditional Protection with Config

Configure protection through config:

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

const config = new pulumi.Config();
const protectResources = config.getBoolean("protectResources") || false;

const resources = [
    new aws.s3.Bucket("bucket-1", { acl: "private" }, { protect: protectResources }),
    new aws.s3.Bucket("bucket-2", { acl: "private" }, { protect: protectResources }),
    new aws.dynamodb.Table("table-1", {
        attributes: [{ name: "id", type: "S" }],
        hashKey: "id",
        billingMode: "PAY_PER_REQUEST",
    }, { protect: protectResources }),
];

// Configure via:
// pulumi config set protectResources true

Replace vs Update Behavior

Understanding Replacement

Some property changes trigger resource replacement:

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

// Properties that trigger replacement (varies by resource):
// - EC2 Instance: AMI, availability zone, instance type (sometimes)
// - RDS Instance: engine, instance class (sometimes), storage type
// - S3 Bucket: bucket name
// - DynamoDB Table: hash key, range key

// Example: Changing instance type (may update or replace)
const instance = new aws.ec2.Instance("web", {
    instanceType: "t3.micro", // Change to t3.small may update in-place
    ami: "ami-0c55b159cbfafe1f0",
});

// Changing AMI always replaces
const instanceNewAmi = new aws.ec2.Instance("web", {
    instanceType: "t3.micro",
    ami: "ami-new-version", // Triggers replacement
});

Preview Changes

Always preview changes before applying:

# Preview changes to see if resource will be replaced
pulumi preview

# Look for these indicators:
# ~ (update in-place)
# +-replace (delete and create)
# - (delete)
# + (create)
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// Example output:
// ~ aws:ec2/instance:Instance: (update)
//   [urn=urn:pulumi:prod::app::aws:ec2/instance:Instance::web]
//   ~ tags: {
//       + Owner: "team-a"
//     }

// vs.

// +-aws:ec2/instance:Instance: (replace)
//   [urn=urn:pulumi:prod::app::aws:ec2/instance:Instance::web]
//   ~ ami: "ami-old" => "ami-new"

Force Replacement

Force resource replacement when needed:

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

// Configure properties that should trigger replacement
const instance = new aws.ec2.Instance("web", {
    instanceType: "t3.micro",
    ami: "ami-0c55b159cbfafe1f0",
    userData: "#!/bin/bash\necho 'v1'",
}, {
    replaceOnChanges: ["userData"], // Force replacement on userData change
});

// Or use CLI flag:
// pulumi up --replace urn:pulumi:prod::app::aws:ec2/instance:Instance::web

Minimize Disruption

Strategies to minimize disruption during replacement:

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

// 1. Create before delete (default for most resources)
const instance = new aws.ec2.Instance("web", {
    instanceType: "t3.micro",
    ami: "ami-0c55b159cbfafe1f0",
}); // New instance created before old one deleted

// 2. Use blue-green deployment
const blueInstance = new aws.ec2.Instance("web-blue", {
    instanceType: "t3.micro",
    ami: "ami-old-version",
});

const greenInstance = new aws.ec2.Instance("web-green", {
    instanceType: "t3.micro",
    ami: "ami-new-version",
});

// Switch traffic to green, then remove blue

// 3. Use auto-scaling groups for zero-downtime
const asg = new aws.autoscaling.Group("web-asg", {
    minSize: 2,
    maxSize: 4,
    desiredCapacity: 2,
    launchConfiguration: "lc-id",
});

// ASG automatically handles rolling updates

Update Policies

Control update behavior for specific resources:

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

// Auto Scaling Group with update policy
const asg = new aws.autoscaling.Group("app-asg", {
    minSize: 2,
    maxSize: 6,
    desiredCapacity: 4,
    launchConfiguration: "lc-id",
    tags: [{
        key: "Name",
        value: "app-instance",
        propagateAtLaunch: true,
    }],
});

// ECS Service with deployment configuration
const service = new aws.ecs.Service("app-service", {
    cluster: "cluster-id",
    taskDefinition: "task-def-arn",
    desiredCount: 3,
    deploymentConfiguration: {
        minimumHealthyPercent: 50,
        maximumPercent: 200,
    },
    deploymentCircuitBreaker: {
        enable: true,
        rollback: true,
    },
});

// Lambda with gradual deployment
const lambdaAlias = new aws.lambda.Alias("app-alias", {
    functionName: "function-name",
    functionVersion: "1",
    routingConfig: {
        additionalVersionWeights: {
            "2": 0.1, // 10% traffic to new version
        },
    },
});

Immutable Infrastructure

Embrace replacement for immutable infrastructure:

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

// Launch configuration (immutable - requires replacement)
const launchConfig = new aws.ec2.LaunchConfiguration("app-lc", {
    imageId: "ami-0c55b159cbfafe1f0",
    instanceType: "t3.micro",
    userData: "#!/bin/bash\necho 'v1'",
});

// Auto Scaling Group references launch configuration
const asg = new aws.autoscaling.Group("app-asg", {
    launchConfiguration: launchConfig.id,
    minSize: 2,
    maxSize: 4,
    desiredCapacity: 2,
});

// To update: create new launch configuration, update ASG reference
// Old instances gradually replaced by new ones

Install with Tessl CLI

npx tessl i tessl/npm-pulumi--aws

docs

index.md

quickstart.md

README.md

tile.json