ð pulumi-stacks
Use when managing multiple environments with Pulumi stacks for development, staging, and production deployments.
Overview
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