A Pulumi package for creating and managing Amazon Web Services (AWS) cloud resources with infrastructure-as-code.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
This guide covers the complete lifecycle of AWS resources in Pulumi, including creation, updates, deletion, import, protection, and understanding replace vs update behavior.
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;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,
});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,
};
}],
});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);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",
},
});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
});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 replacementPrevent 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"],
});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-instanceDelete 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 resourcePulumi 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`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 deletedProtect 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 againUse 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 quotasImport 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 forwardImport 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",
});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 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 changesMigrate 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-0123456789abcdef0import * 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",
});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 deleteApply 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,
});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,
});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 trueSome 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
});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 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::webStrategies 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 updatesControl 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
},
},
});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 onesInstall with Tessl CLI
npx tessl i tessl/npm-pulumi--aws@7.16.0