| name | pulumi-stacks |
| description | Use when managing multiple environments with Pulumi stacks for development, staging, and production deployments. |
| allowed-tools | Bash, Read |
Pulumi Stacks
Manage multiple environments and configurations with Pulumi stacks for consistent infrastructure across development, staging, and production.
Overview
Pulumi stacks are isolated, independently configurable instances of a Pulumi program. Each stack has its own state, configuration, and resources, enabling you to deploy the same infrastructure code to multiple environments.
Stack Basics
Creating and Selecting Stacks
# Initialize a new project
pulumi new aws-typescript
# Create a new stack
pulumi stack init dev
# List all stacks
pulumi stack ls
# Select a stack
pulumi stack select dev
# Show current stack
pulumi stack
# Remove a stack
pulumi stack rm dev
Stack Configuration
# Set configuration values
pulumi config set aws:region us-east-1
pulumi config set instanceType t3.micro
# Set secret values (encrypted)
pulumi config set --secret dbPassword mySecurePassword123
# Get configuration values
pulumi config get aws:region
# List all configuration
pulumi config
# Remove configuration
pulumi config rm instanceType
Stack Configuration Files
Pulumi.yaml (Project File)
name: my-infrastructure
runtime: nodejs
description: Multi-environment infrastructure
config:
aws:region:
description: AWS region for deployment
default: us-east-1
instanceType:
description: EC2 instance type
default: t3.micro
environment:
description: Environment name
Pulumi.dev.yaml (Stack Config)
config:
aws:region: us-east-1
my-infrastructure:instanceType: t3.micro
my-infrastructure:environment: development
my-infrastructure:minSize: "1"
my-infrastructure:maxSize: "3"
my-infrastructure:enableMonitoring: "false"
Pulumi.staging.yaml
config:
aws:region: us-east-1
my-infrastructure:instanceType: t3.small
my-infrastructure:environment: staging
my-infrastructure:minSize: "2"
my-infrastructure:maxSize: "5"
my-infrastructure:enableMonitoring: "true"
Pulumi.prod.yaml
config:
aws:region: us-west-2
my-infrastructure:instanceType: t3.medium
my-infrastructure:environment: production
my-infrastructure:minSize: "3"
my-infrastructure:maxSize: "10"
my-infrastructure:enableMonitoring: "true"
my-infrastructure:backupRetention: "30"
Reading Configuration in Code
TypeScript Configuration
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Get configuration
const config = new pulumi.Config();
const instanceType = config.get("instanceType") || "t3.micro";
const environment = config.require("environment");
const minSize = config.getNumber("minSize") || 1;
const maxSize = config.getNumber("maxSize") || 3;
const enableMonitoring = config.getBoolean("enableMonitoring") || false;
// Get secret
const dbPassword = config.requireSecret("dbPassword");
// Use configuration
const instance = new aws.ec2.Instance("web-server", {
instanceType: instanceType,
ami: "ami-0c55b159cbfafe1f0",
tags: {
Name: `web-server-${environment}`,
Environment: environment,
},
monitoring: enableMonitoring,
});
// Export stack name
export const stackName = pulumi.getStack();
export const instanceId = instance.id;
Python Configuration
import pulumi
import pulumi_aws as aws
# Get configuration
config = pulumi.Config()
instance_type = config.get("instanceType") or "t3.micro"
environment = config.require("environment")
min_size = config.get_int("minSize") or 1
max_size = config.get_int("maxSize") or 3
enable_monitoring = config.get_bool("enableMonitoring") or False
# Get secret
db_password = config.require_secret("dbPassword")
# Use configuration
instance = aws.ec2.Instance(
"web-server",
instance_type=instance_type,
ami="ami-0c55b159cbfafe1f0",
tags={
"Name": f"web-server-{environment}",
"Environment": environment,
},
monitoring=enable_monitoring,
)
# Export outputs
pulumi.export("stack_name", pulumi.get_stack())
pulumi.export("instance_id", instance.id)
Environment-Specific Resources
Conditional Resource Creation
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment");
const enableHighAvailability = config.getBoolean("enableHA") || false;
// Create VPC
const vpc = new aws.ec2.Vpc("main", {
cidrBlock: "10.0.0.0/16",
tags: {
Name: `vpc-${environment}`,
Environment: environment,
},
});
// Production gets multiple availability zones
const azCount = environment === "production" ? 3 : 1;
const subnets: aws.ec2.Subnet[] = [];
for (let i = 0; i < azCount; i++) {
const subnet = new aws.ec2.Subnet(`subnet-${i}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${i}.0/24`,
availabilityZone: `us-east-1${String.fromCharCode(97 + i)}`,
tags: {
Name: `subnet-${environment}-${i}`,
Environment: environment,
},
});
subnets.push(subnet);
}
// Only create NAT gateway in production
let natGateway: aws.ec2.NatGateway | undefined;
if (environment === "production") {
const eip = new aws.ec2.Eip("nat-eip", {
vpc: true,
});
natGateway = new aws.ec2.NatGateway("nat", {
allocationId: eip.id,
subnetId: subnets[0].id,
tags: {
Name: `nat-${environment}`,
Environment: environment,
},
});
}
// Create RDS with multi-AZ only in production
const db = new aws.rds.Instance("database", {
engine: "postgres",
engineVersion: "14.7",
instanceClass: environment === "production" ? "db.t3.medium" : "db.t3.micro",
allocatedStorage: environment === "production" ? 100 : 20,
dbName: "myapp",
username: "admin",
password: config.requireSecret("dbPassword"),
multiAz: environment === "production",
backupRetentionPeriod: environment === "production" ? 30 : 7,
skipFinalSnapshot: environment !== "production",
tags: {
Name: `db-${environment}`,
Environment: environment,
},
});
export const vpcId = vpc.id;
export const subnetIds = subnets.map(s => s.id);
export const dbEndpoint = db.endpoint;
Stack References
Cross-Stack References
// Infrastructure stack (infra/index.ts)
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const vpc = new aws.ec2.Vpc("shared-vpc", {
cidrBlock: "10.0.0.0/16",
tags: {
Name: "shared-vpc",
},
});
const subnet = new aws.ec2.Subnet("shared-subnet", {
vpcId: vpc.id,
cidrBlock: "10.0.1.0/24",
tags: {
Name: "shared-subnet",
},
});
// Export for other stacks
export const vpcId = vpc.id;
export const subnetId = subnet.id;
export const vpcCidr = vpc.cidrBlock;
// Application stack (app/index.ts)
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Reference infrastructure stack
const infraStack = new pulumi.StackReference("myorg/infra/prod");
// Get outputs from infrastructure stack
const vpcId = infraStack.getOutput("vpcId");
const subnetId = infraStack.getOutput("subnetId");
// Use referenced values
const securityGroup = new aws.ec2.SecurityGroup("app-sg", {
vpcId: vpcId,
description: "Security group for application",
ingress: [{
protocol: "tcp",
fromPort: 80,
toPort: 80,
cidrBlocks: ["0.0.0.0/0"],
}],
});
const instance = new aws.ec2.Instance("app-server", {
instanceType: "t3.micro",
ami: "ami-0c55b159cbfafe1f0",
subnetId: subnetId,
vpcSecurityGroupIds: [securityGroup.id],
tags: {
Name: "app-server",
},
});
export const instanceIp = instance.publicIp;
Stack Reference Commands
# Deploy infrastructure stack first
cd infra
pulumi stack select prod
pulumi up
# Then deploy application stack
cd ../app
pulumi stack select prod
pulumi up
# View outputs from referenced stack
pulumi stack output --stack myorg/infra/prod
Stack Outputs
Exporting Stack Outputs
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment");
// Create resources
const vpc = new aws.ec2.Vpc("main", {
cidrBlock: "10.0.0.0/16",
});
const bucket = new aws.s3.Bucket("app-bucket", {
bucket: `myapp-${environment}-bucket`,
});
const db = new aws.rds.Instance("database", {
engine: "postgres",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
dbName: "myapp",
username: "admin",
password: config.requireSecret("dbPassword"),
skipFinalSnapshot: true,
});
// Export outputs
export const vpcId = vpc.id;
export const vpcCidr = vpc.cidrBlock;
export const bucketName = bucket.id;
export const bucketArn = bucket.arn;
export const dbEndpoint = db.endpoint;
export const dbPort = db.port;
// Export computed values
export const dbConnectionString = pulumi.interpolate`postgresql://admin@${db.endpoint}/myapp`;
// Export stack metadata
export const stackName = pulumi.getStack();
export const projectName = pulumi.getProject();
export const region = aws.getRegion().then(r => r.name);
Accessing Stack Outputs
# View all outputs
pulumi stack output
# Get specific output
pulumi stack output vpcId
# Get output as JSON
pulumi stack output --json
# Use in shell scripts
VPC_ID=$(pulumi stack output vpcId)
echo "VPC ID: $VPC_ID"
# Export to environment variables
export $(pulumi stack output --json | jq -r 'to_entries[] | "\(.key)=\(.value)"')
Stack Transformations
Global Resource Transformations
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment");
// Register global transformation to add tags
pulumi.runtime.registerStackTransformation((args) => {
if (args.type.startsWith("aws:")) {
args.props.tags = {
...args.props.tags,
Environment: environment,
ManagedBy: "Pulumi",
Stack: pulumi.getStack(),
};
}
return {
props: args.props,
opts: args.opts,
};
});
// All AWS resources automatically get tags
const vpc = new aws.ec2.Vpc("main", {
cidrBlock: "10.0.0.0/16",
// tags will be automatically added by transformation
});
const bucket = new aws.s3.Bucket("data", {
// tags will be automatically added by transformation
});
Resource-Specific Transformations
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment");
// Transformation to enforce encryption
const enforceEncryption = (args: pulumi.ResourceTransformationArgs) => {
if (args.type === "aws:s3/bucket:Bucket") {
args.props.serverSideEncryptionConfiguration = {
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: "AES256",
},
},
};
}
if (args.type === "aws:rds/instance:Instance") {
args.props.storageEncrypted = true;
}
return {
props: args.props,
opts: args.opts,
};
};
pulumi.runtime.registerStackTransformation(enforceEncryption);
// Resources will be automatically encrypted
const bucket = new aws.s3.Bucket("data");
const db = new aws.rds.Instance("database", {
engine: "postgres",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
});
Stack Tags and Organization
Tagging Strategy
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment");
const project = pulumi.getProject();
const stack = pulumi.getStack();
// Define common tags
const commonTags = {
Project: project,
Environment: environment,
Stack: stack,
ManagedBy: "Pulumi",
CostCenter: config.get("costCenter") || "engineering",
Owner: config.get("owner") || "platform-team",
};
// Helper function to merge tags
function mergeTags(resourceTags?: { [key: string]: string }): { [key: string]: string } {
return {
...commonTags,
...resourceTags,
};
}
// Use consistent tagging
const vpc = new aws.ec2.Vpc("main", {
cidrBlock: "10.0.0.0/16",
tags: mergeTags({
Name: `vpc-${environment}`,
Type: "network",
}),
});
const bucket = new aws.s3.Bucket("data", {
tags: mergeTags({
Name: `data-${environment}`,
Type: "storage",
Compliance: "required",
}),
});
const db = new aws.rds.Instance("database", {
engine: "postgres",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
tags: mergeTags({
Name: `db-${environment}`,
Type: "database",
BackupRequired: "true",
}),
});
Stack Import and Export
Exporting Stack State
# Export stack state to JSON
pulumi stack export > stack-state.json
# Export to file
pulumi stack export --file stack-backup.json
# Export with secrets in plaintext (use carefully!)
pulumi stack export --show-secrets > stack-with-secrets.json
Importing Stack State
# Import stack state
pulumi stack import --file stack-state.json
# Import from stdin
cat stack-state.json | pulumi stack import
Stack Migration
# Export from old stack
pulumi stack select old-stack
pulumi stack export --file old-stack.json
# Create and import to new stack
pulumi stack init new-stack
pulumi stack import --file old-stack.json
# Verify resources
pulumi preview
Multi-Region Deployments
Region-Specific Stacks
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const awsConfig = new pulumi.Config("aws");
const region = awsConfig.require("region");
const environment = config.require("environment");
// Create region-specific resources
const vpc = new aws.ec2.Vpc(`vpc-${region}`, {
cidrBlock: "10.0.0.0/16",
tags: {
Name: `vpc-${environment}-${region}`,
Region: region,
Environment: environment,
},
});
// Create CloudFront distribution in us-east-1
const usEast1Provider = new aws.Provider("us-east-1", {
region: "us-east-1",
});
const certificate = new aws.acm.Certificate("cert", {
domainName: `${environment}.example.com`,
validationMethod: "DNS",
tags: {
Name: `cert-${environment}`,
Environment: environment,
},
}, { provider: usEast1Provider });
// Export region info
export const deploymentRegion = region;
export const vpcId = vpc.id;
export const certificateArn = certificate.arn;
Multi-Region Stack Configuration
# Pulumi.us-east-1-prod.yaml
config:
aws:region: us-east-1
my-app:environment: production
my-app:isPrimaryRegion: "true"
# Pulumi.us-west-2-prod.yaml
config:
aws:region: us-west-2
my-app:environment: production
my-app:isPrimaryRegion: "false"
# Pulumi.eu-west-1-prod.yaml
config:
aws:region: eu-west-1
my-app:environment: production
my-app:isPrimaryRegion: "false"
Stack Policies
Protect Resources
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment");
// Protect production databases
const db = new aws.rds.Instance("database", {
engine: "postgres",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
tags: {
Name: `db-${environment}`,
},
}, {
protect: environment === "production",
});
// Protect production storage
const bucket = new aws.s3.Bucket("data", {
tags: {
Name: `data-${environment}`,
},
}, {
protect: environment === "production",
});
Retain Resources
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment");
// Retain production databases on stack deletion
const db = new aws.rds.Instance("database", {
engine: "postgres",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
finalSnapshotIdentifier: environment === "production"
? `final-snapshot-${Date.now()}`
: undefined,
skipFinalSnapshot: environment !== "production",
}, {
retainOnDelete: environment === "production",
});
Stack Secrets Management
Using Encrypted Secrets
# Set encrypted secrets
pulumi config set --secret dbPassword mySecurePassword123
pulumi config set --secret apiKey sk_live_abc123xyz789
# View config (secrets are encrypted)
pulumi config
# View secrets in plaintext (use carefully!)
pulumi config get dbPassword --show-secrets
Secrets in Code
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
// Get secret values
const dbPassword = config.requireSecret("dbPassword");
const apiKey = config.requireSecret("apiKey");
// Use secrets in resources
const db = new aws.rds.Instance("database", {
engine: "postgres",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
username: "admin",
password: dbPassword,
});
// Create SSM parameters from secrets
const dbPasswordParam = new aws.ssm.Parameter("db-password", {
name: "/app/database/password",
type: "SecureString",
value: dbPassword,
});
const apiKeyParam = new aws.ssm.Parameter("api-key", {
name: "/app/api/key",
type: "SecureString",
value: apiKey,
});
// Secrets are encrypted in state
export const connectionString = pulumi.secret(
pulumi.interpolate`postgresql://admin:${dbPassword}@${db.endpoint}/myapp`
);
Stack Refresh and State
Refresh Stack State
# Refresh stack to match actual cloud state
pulumi refresh
# Refresh with auto-approval
pulumi refresh --yes
# Refresh specific resources
pulumi refresh --target urn:pulumi:dev::myapp::aws:s3/bucket:Bucket::my-bucket
# Refresh and show diff
pulumi refresh --diff
Stack State Management
# View stack state
pulumi stack --show-urns
# View specific resource
pulumi stack --show-urns | grep my-bucket
# Remove resource from state (doesn't delete cloud resource)
pulumi state delete 'urn:pulumi:dev::myapp::aws:s3/bucket:Bucket::my-bucket'
# Rename resource in state
pulumi state rename 'urn:pulumi:dev::myapp::aws:s3/bucket:Bucket::old-name' \
'urn:pulumi:dev::myapp::aws:s3/bucket:Bucket::new-name'
When to Use This Skill
Use the pulumi-stacks skill when you need to:
- Deploy infrastructure to multiple environments (dev, staging, prod)
- Manage environment-specific configurations
- Create isolated instances of the same infrastructure
- Share infrastructure outputs between projects
- Implement multi-region deployments
- Separate infrastructure concerns (networking, databases, applications)
- Manage secrets per environment
- Track infrastructure state per environment
- Implement progressive deployment strategies
- Organize complex infrastructure into manageable units
- Apply environment-specific policies and protections
- Maintain consistent infrastructure across environments
Best Practices
- Naming Convention: Use consistent stack naming like
<env>or<region>-<env>(e.g.,prod,us-east-1-prod) - Configuration Files: Keep stack config files in version control (except secrets)
- Environment Isolation: Never share state between environments; each environment gets its own stack
- Stack References: Use stack references instead of duplicating infrastructure code
- Secrets Management: Always use
--secretflag for sensitive values - Progressive Deployment: Deploy to dev first, then staging, finally production
- State Backups: Regularly export stack state for disaster recovery
- Resource Protection: Enable
protectoption for critical production resources - Tagging Strategy: Apply consistent tags across all environments for cost tracking
- Stack Outputs: Export all values needed by other stacks or external systems
- Configuration Validation: Validate configuration values before creating resources
- Environment Parity: Keep environments as similar as possible, differing only in scale
- Automation: Use CI/CD pipelines for stack deployments
- Documentation: Document stack dependencies and required configuration
- State Encryption: Use encrypted state backends for sensitive infrastructure
Common Pitfalls
- Hardcoded Values: Hardcoding environment-specific values instead of using configuration
- Shared State: Attempting to share stack state between environments
- Missing Config: Deploying to new stack without setting required configuration
- Unencrypted Secrets: Storing secrets as plain text in configuration
- Inconsistent Naming: Using different naming conventions across stacks
- Broken References: Stack references that point to non-existent stacks or outputs
- Missing Exports: Not exporting values needed by dependent stacks
- Config Drift: Manual changes to config files not reflected in version control
- No Resource Protection: Forgetting to protect critical production resources
- Stack Sprawl: Creating too many stacks without clear organization
- Missing Validation: Not validating configuration before deployment
- Circular Dependencies: Creating circular stack references
- No Backup Strategy: Not exporting stack state for disaster recovery
- Environment Differences: Production significantly different from other environments
- Poor Secret Management: Checking encrypted secrets into public repositories without proper key management