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 common patterns and best practices for building AWS infrastructure with Pulumi, including resource tagging, dependency management, and component resources.
Apply consistent tags to all resources:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const commonTags = {
Project: pulumi.getProject(),
Stack: pulumi.getStack(),
Environment: pulumi.getStack(),
ManagedBy: "pulumi",
};
const bucket = new aws.s3.Bucket("my-bucket", {
tags: commonTags,
});
const instance = new aws.ec2.Instance("my-instance", {
instanceType: "t3.micro",
ami: "ami-0c55b159cbfafe1f0",
tags: commonTags,
});Create tags based on stack configuration:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment");
const costCenter = config.require("costCenter");
const owner = config.require("owner");
function getStandardTags(resourceName: string): { [key: string]: string } {
return {
Name: resourceName,
Environment: environment,
Project: pulumi.getProject(),
Stack: pulumi.getStack(),
CostCenter: costCenter,
Owner: owner,
ManagedBy: "pulumi",
CreatedAt: new Date().toISOString(),
};
}
const vpc = new aws.ec2.Vpc("main-vpc", {
cidrBlock: "10.0.0.0/16",
tags: getStandardTags("main-vpc"),
});
const subnet = new aws.ec2.Subnet("public-subnet", {
vpcId: vpc.id,
cidrBlock: "10.0.1.0/24",
tags: {
...getStandardTags("public-subnet"),
Type: "public",
},
});Apply default tags to all resources using provider configuration:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const provider = new aws.Provider("tagged-provider", {
region: "us-west-2",
defaultTags: {
tags: {
Environment: pulumi.getStack(),
Project: pulumi.getProject(),
ManagedBy: "pulumi",
Team: "platform",
},
},
});
// These resources will automatically inherit default tags
const bucket = new aws.s3.Bucket("auto-tagged-bucket", {
tags: {
Purpose: "storage", // Merged with default tags
},
}, { provider });
const table = new aws.dynamodb.Table("auto-tagged-table", {
attributes: [{ name: "id", type: "S" }],
hashKey: "id",
billingMode: "PAY_PER_REQUEST",
tags: {
DataType: "user-data",
},
}, { provider });Use tags for resource discovery and management:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Create resources with discovery tags
const productionResources = ["api", "web", "worker"].map(service => {
return new aws.ec2.Instance(`prod-${service}`, {
instanceType: "t3.medium",
ami: "ami-0c55b159cbfafe1f0",
tags: {
Environment: "production",
Service: service,
Discoverable: "true",
AutoShutdown: "false",
},
});
});
// Query resources by tags
const discoverableInstances = aws.ec2.getInstancesOutput({
filters: [
{ name: "tag:Discoverable", values: ["true"] },
{ name: "tag:Environment", values: ["production"] },
{ name: "instance-state-name", values: ["running"] },
],
});
export const discoverableInstanceIds = discoverableInstances.ids;Use dependsOn for explicit resource ordering:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const vpc = new aws.ec2.Vpc("main-vpc", {
cidrBlock: "10.0.0.0/16",
});
const igw = new aws.ec2.InternetGateway("igw", {
vpcId: vpc.id,
});
const routeTable = new aws.ec2.RouteTable("public-rt", {
vpcId: vpc.id,
});
// Ensure IGW exists before creating route
const route = new aws.ec2.Route("public-route", {
routeTableId: routeTable.id,
destinationCidrBlock: "0.0.0.0/0",
gatewayId: igw.id,
}, {
dependsOn: [igw],
});Use parent option for logical grouping and cascade deletion:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const vpc = new aws.ec2.Vpc("app-vpc", {
cidrBlock: "10.0.0.0/16",
});
// Subnets are children of VPC
const publicSubnet = new aws.ec2.Subnet("public-subnet", {
vpcId: vpc.id,
cidrBlock: "10.0.1.0/24",
}, {
parent: vpc,
});
const privateSubnet = new aws.ec2.Subnet("private-subnet", {
vpcId: vpc.id,
cidrBlock: "10.0.2.0/24",
}, {
parent: vpc,
});
// Deleting VPC will cascade delete subnetsHandle complex dependency chains:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// 1. Create VPC
const vpc = new aws.ec2.Vpc("vpc", {
cidrBlock: "10.0.0.0/16",
});
// 2. Create subnet (depends on VPC)
const subnet = new aws.ec2.Subnet("subnet", {
vpcId: vpc.id,
cidrBlock: "10.0.1.0/24",
});
// 3. Create security group (depends on VPC)
const sg = new aws.ec2.SecurityGroup("sg", {
vpcId: vpc.id,
ingress: [{
protocol: "tcp",
fromPort: 80,
toPort: 80,
cidrBlocks: ["0.0.0.0/0"],
}],
});
// 4. Create instance (depends on subnet and security group)
const instance = new aws.ec2.Instance("instance", {
instanceType: "t3.micro",
ami: "ami-0c55b159cbfafe1f0",
subnetId: subnet.id,
vpcSecurityGroupIds: [sg.id],
});
// 5. Create Elastic IP (depends on instance)
const eip = new aws.ec2.Eip("eip", {
instance: instance.id,
vpc: true,
}, {
dependsOn: [instance],
});Ensure critical resources are not accidentally deleted:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Production database - protect from deletion
const database = new aws.rds.Instance("prod-db", {
engine: "postgres",
instanceClass: "db.t3.medium",
allocatedStorage: 100,
username: "admin",
password: pulumi.secret("super-secret-password"),
}, {
protect: true, // Requires explicit unprotect before deletion
});
// Application depends on database
const app = new aws.ecs.Service("app-service", {
// ... service configuration
}, {
dependsOn: [database],
});Transform outputs using the apply method:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("my-bucket");
// Transform bucket name to uppercase
export const bucketNameUpper = bucket.bucket.apply(name => name.toUpperCase());
// Construct ARN from bucket name
export const bucketArn = bucket.bucket.apply(name => `arn:aws:s3:::${name}`);
// Conditional transformation
export const bucketUrl = bucket.bucket.apply(name =>
name.startsWith("public-") ? `https://${name}.s3.amazonaws.com` : null
);Combine multiple outputs:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const vpc = new aws.ec2.Vpc("vpc", {
cidrBlock: "10.0.0.0/16",
});
const subnet = new aws.ec2.Subnet("subnet", {
vpcId: vpc.id,
cidrBlock: "10.0.1.0/24",
});
const instance = new aws.ec2.Instance("instance", {
instanceType: "t3.micro",
ami: "ami-0c55b159cbfafe1f0",
subnetId: subnet.id,
});
// Combine multiple outputs
export const instanceInfo = pulumi.all([
instance.id,
instance.publicIp,
instance.privateIp,
vpc.id,
]).apply(([id, publicIp, privateIp, vpcId]) => ({
instanceId: id,
publicEndpoint: `http://${publicIp}`,
privateEndpoint: `http://${privateIp}`,
vpcId: vpcId,
}));Use pulumi.interpolate for string formatting with outputs:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const cluster = new aws.ecs.Cluster("app-cluster");
const loadBalancer = new aws.lb.LoadBalancer("app-lb", {
loadBalancerType: "application",
subnets: ["subnet-1", "subnet-2"],
});
// Create connection string with interpolation
export const clusterEndpoint = pulumi.interpolate`${cluster.name}.${loadBalancer.dnsName}`;
// Create policy document with interpolation
const bucket = new aws.s3.Bucket("data-bucket");
const bucketPolicy = new aws.s3.BucketPolicy("data-bucket-policy", {
bucket: bucket.id,
policy: pulumi.interpolate`{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "${bucket.arn}/*"
}]
}`,
});Perform async operations in transformations:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const instance = new aws.ec2.Instance("web-instance", {
instanceType: "t3.micro",
ami: "ami-0c55b159cbfafe1f0",
});
// Async lookup based on instance output
export const instanceAmiDetails = instance.ami.apply(async amiId => {
const ami = await aws.ec2.getAmi({
filters: [{ name: "image-id", values: [amiId] }],
});
return {
name: ami.name,
description: ami.description,
creationDate: ami.creationDate,
};
});Reference outputs from another stack:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// In network stack (foundation/network)
const vpc = new aws.ec2.Vpc("shared-vpc", {
cidrBlock: "10.0.0.0/16",
});
export const vpcId = vpc.id;
export const publicSubnetIds = ["subnet-1", "subnet-2"];
// In application stack (app/production)
const networkStack = new pulumi.StackReference("network", {
name: "organization/foundation/network",
});
const vpcId = networkStack.requireOutput("vpcId");
const subnetIds = networkStack.requireOutput("publicSubnetIds");
const instance = new aws.ec2.Instance("app-instance", {
instanceType: "t3.micro",
ami: "ami-0c55b159cbfafe1f0",
subnetId: subnetIds.apply(ids => ids[0]),
});Add type safety to stack references:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface NetworkStackOutputs {
vpcId: pulumi.Output<string>;
publicSubnetIds: pulumi.Output<string[]>;
privateSubnetIds: pulumi.Output<string[]>;
securityGroupId: pulumi.Output<string>;
}
function getNetworkStack(): NetworkStackOutputs {
const stack = new pulumi.StackReference("network", {
name: `${pulumi.getOrganization()}/network/${pulumi.getStack()}`,
});
return {
vpcId: stack.requireOutput("vpcId"),
publicSubnetIds: stack.requireOutput("publicSubnetIds"),
privateSubnetIds: stack.requireOutput("privateSubnetIds"),
securityGroupId: stack.requireOutput("securityGroupId"),
};
}
const network = getNetworkStack();
const alb = new aws.lb.LoadBalancer("app-alb", {
loadBalancerType: "application",
subnets: network.publicSubnetIds,
securityGroups: [network.securityGroupId],
});Reference multiple stacks:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Reference network stack
const networkStack = new pulumi.StackReference("network", {
name: "org/network/prod",
});
// Reference data stack
const dataStack = new pulumi.StackReference("data", {
name: "org/data/prod",
});
// Reference security stack
const securityStack = new pulumi.StackReference("security", {
name: "org/security/prod",
});
const application = new aws.ecs.TaskDefinition("app-task", {
family: "app",
cpu: "256",
memory: "512",
containerDefinitions: pulumi.all([
dataStack.requireOutput("databaseEndpoint"),
securityStack.requireOutput("appSecretArn"),
]).apply(([dbEndpoint, secretArn]) => JSON.stringify([{
name: "app",
image: "nginx:latest",
environment: [
{ name: "DB_ENDPOINT", value: dbEndpoint },
],
secrets: [
{ name: "DB_PASSWORD", valueFrom: secretArn },
],
}])),
});Create reusable component resources:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface WebServerArgs {
instanceType: string;
keyName: string;
vpcId: pulumi.Input<string>;
subnetId: pulumi.Input<string>;
}
class WebServer extends pulumi.ComponentResource {
public readonly instance: aws.ec2.Instance;
public readonly securityGroup: aws.ec2.SecurityGroup;
public readonly publicIp: pulumi.Output<string>;
constructor(name: string, args: WebServerArgs, opts?: pulumi.ComponentResourceOptions) {
super("custom:app:WebServer", name, {}, opts);
// Create security group
this.securityGroup = new aws.ec2.SecurityGroup(`${name}-sg`, {
vpcId: args.vpcId,
ingress: [
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
{ protocol: "tcp", fromPort: 443, toPort: 443, cidrBlocks: ["0.0.0.0/0"] },
],
egress: [
{ protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] },
],
}, { parent: this });
// Create instance
this.instance = new aws.ec2.Instance(`${name}-instance`, {
instanceType: args.instanceType,
ami: "ami-0c55b159cbfafe1f0",
subnetId: args.subnetId,
keyName: args.keyName,
vpcSecurityGroupIds: [this.securityGroup.id],
userData: `#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd`,
}, { parent: this });
this.publicIp = this.instance.publicIp;
this.registerOutputs({
instance: this.instance,
securityGroup: this.securityGroup,
publicIp: this.publicIp,
});
}
}
// Use the component
const webServer = new WebServer("my-web-server", {
instanceType: "t3.small",
keyName: "my-key",
vpcId: "vpc-12345",
subnetId: "subnet-12345",
});
export const webServerIp = webServer.publicIp;Build complex components:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface StaticWebsiteArgs {
domain: string;
certificateArn?: pulumi.Input<string>;
errorDocument?: string;
indexDocument?: string;
}
class StaticWebsite extends pulumi.ComponentResource {
public readonly bucket: aws.s3.Bucket;
public readonly bucketPolicy: aws.s3.BucketPolicy;
public readonly distribution: aws.cloudfront.Distribution;
public readonly websiteUrl: pulumi.Output<string>;
constructor(name: string, args: StaticWebsiteArgs, opts?: pulumi.ComponentResourceOptions) {
super("custom:web:StaticWebsite", name, {}, opts);
// S3 bucket for website content
this.bucket = new aws.s3.Bucket(`${name}-bucket`, {
website: {
indexDocument: args.indexDocument || "index.html",
errorDocument: args.errorDocument || "404.html",
},
}, { parent: this });
// Bucket policy for public read
this.bucketPolicy = new aws.s3.BucketPolicy(`${name}-policy`, {
bucket: this.bucket.id,
policy: this.bucket.arn.apply(arn => JSON.stringify({
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Principal: "*",
Action: "s3:GetObject",
Resource: `${arn}/*`,
}],
})),
}, { parent: this });
// CloudFront distribution
this.distribution = new aws.cloudfront.Distribution(`${name}-cdn`, {
enabled: true,
aliases: [args.domain],
origins: [{
originId: this.bucket.arn,
domainName: this.bucket.websiteEndpoint,
customOriginConfig: {
originProtocolPolicy: "http-only",
httpPort: 80,
httpsPort: 443,
originSslProtocols: ["TLSv1.2"],
},
}],
defaultCacheBehavior: {
targetOriginId: this.bucket.arn,
viewerProtocolPolicy: "redirect-to-https",
allowedMethods: ["GET", "HEAD", "OPTIONS"],
cachedMethods: ["GET", "HEAD"],
forwardedValues: {
queryString: false,
cookies: { forward: "none" },
},
},
viewerCertificate: args.certificateArn ? {
acmCertificateArn: args.certificateArn,
sslSupportMethod: "sni-only",
} : {
cloudfrontDefaultCertificate: true,
},
restrictions: {
geoRestriction: {
restrictionType: "none",
},
},
}, { parent: this });
this.websiteUrl = this.distribution.domainName.apply(domain => `https://${domain}`);
this.registerOutputs({
bucket: this.bucket,
distribution: this.distribution,
websiteUrl: this.websiteUrl,
});
}
}
// Deploy static website
const website = new StaticWebsite("company-site", {
domain: "www.example.com",
certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/12345678",
});
export const websiteUrl = website.websiteUrl;Create resources conditionally based on configuration:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const enableBackups = config.getBoolean("enableBackups") || false;
const enableMonitoring = config.getBoolean("enableMonitoring") || false;
const bucket = new aws.s3.Bucket("data-bucket");
// Conditionally create backup bucket
const backupBucket = enableBackups ? new aws.s3.Bucket("backup-bucket", {
versioningEnabled: true,
lifecycleRules: [{
enabled: true,
transitions: [{
days: 30,
storageClass: "GLACIER",
}],
}],
}) : undefined;
// Conditionally create CloudWatch alarm
const alarm = enableMonitoring ? new aws.cloudwatch.MetricAlarm("bucket-alarm", {
comparisonOperator: "GreaterThanThreshold",
evaluationPeriods: 2,
metricName: "NumberOfObjects",
namespace: "AWS/S3",
period: 300,
statistic: "Average",
threshold: 1000,
dimensions: {
BucketName: bucket.bucket,
},
}) : undefined;
export const hasBackups = enableBackups;
export const hasMonitoring = enableMonitoring;Different resources for different stacks:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const stack = pulumi.getStack();
const isProduction = stack === "production";
// Production gets larger instances
const instanceType = isProduction ? "t3.large" : "t3.micro";
const instanceCount = isProduction ? 3 : 1;
// Production gets Multi-AZ database
const database = new aws.rds.Instance("app-db", {
engine: "postgres",
instanceClass: isProduction ? "db.t3.large" : "db.t3.micro",
allocatedStorage: isProduction ? 100 : 20,
multiAz: isProduction,
backupRetentionPeriod: isProduction ? 7 : 1,
username: "admin",
password: pulumi.secret("db-password"),
});
// Create multiple instances for production
const instances = Array.from({ length: instanceCount }, (_, i) => {
return new aws.ec2.Instance(`app-instance-${i}`, {
instanceType: instanceType,
ami: "ami-0c55b159cbfafe1f0",
tags: {
Name: `app-instance-${i}`,
Environment: stack,
},
});
});
// Production gets ALB, dev gets single instance
const loadBalancer = isProduction ? new aws.lb.LoadBalancer("app-alb", {
loadBalancerType: "application",
subnets: ["subnet-1", "subnet-2"],
}) : undefined;
export const endpoint = isProduction
? loadBalancer!.dnsName
: instances[0].publicIp;Use feature flags for gradual rollouts:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
interface FeatureFlags {
enableNewApi: boolean;
enableCache: boolean;
enableMetrics: boolean;
enableWaf: boolean;
}
const features: FeatureFlags = {
enableNewApi: config.getBoolean("feature:newApi") || false,
enableCache: config.getBoolean("feature:cache") || false,
enableMetrics: config.getBoolean("feature:metrics") || false,
enableWaf: config.getBoolean("feature:waf") || false,
};
// Conditionally create API Gateway v2
const api = features.enableNewApi
? new aws.apigatewayv2.Api("api-v2", {
protocolType: "HTTP",
})
: new aws.apigateway.RestApi("api-v1", {
description: "Legacy API",
});
// Conditionally create ElastiCache
const cache = features.enableCache ? new aws.elasticache.Cluster("cache", {
engine: "redis",
nodeType: "cache.t3.micro",
numCacheNodes: 1,
}) : undefined;
// Conditionally enable detailed monitoring
const instance = new aws.ec2.Instance("app", {
instanceType: "t3.micro",
ami: "ami-0c55b159cbfafe1f0",
monitoring: features.enableMetrics,
});
// Conditionally add WAF
const waf = features.enableWaf ? new aws.wafv2.WebAcl("waf", {
scope: "REGIONAL",
defaultAction: { allow: {} },
rules: [{
name: "rate-limit",
priority: 1,
statement: {
rateBasedStatement: {
limit: 2000,
aggregateKeyType: "IP",
},
},
action: { block: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudwatchMetricsEnabled: true,
metricName: "rate-limit",
},
}],
visibilityConfig: {
sampledRequestsEnabled: true,
cloudwatchMetricsEnabled: true,
metricName: "waf",
},
}) : undefined;
export const activeFeatures = features;Install with Tessl CLI
npx tessl i tessl/npm-pulumi--aws@7.16.0