Pulumi Stacks: Multi-Environment IaC Management
Pulumi stacks are named deployments of the same infrastructure code with different values. Use stacks to manage dev, staging, and production environments from a single codebase: the code is identical, but stack-specific config files (Pulumi.dev.yaml, Pulumi.prod.yaml) drive different behaviors. This eliminates code duplication, ensures parity between environments, and makes environment promotion (dev → staging → prod) a repeatable, low-risk process.
Designing Stacks for Multi-Environment Deployments
A typical production setup has three stacks:
| Stack | Purpose | Instance Type | Database | Backups |
|---|---|---|---|---|
| dev | Development; fast iteration | t3.micro | Single-AZ | None |
| staging | Pre-production testing | t3.small | Multi-AZ | Daily |
| prod | Production; high availability | t3.large | Multi-AZ + read replicas | Hourly |
The infrastructure code is the same in all three stacks; only the configuration varies. This approach ensures that:
- Developers test code against a staging environment that mirrors production.
- Configuration changes are reviewed (via pull requests) before promotion.
- You can promote code confidently because staging and prod run identical infrastructure.
Creating and Managing Stacks
Initialize multiple stacks at project creation:
# Create project
pulumi new aws-python --name my-infra
# Create stacks
pulumi stack init dev
pulumi stack init staging
pulumi stack init prod
Each stack gets its own config file:
my-infra/
├── __main__.py
├── Pulumi.yaml
├── Pulumi.dev.yaml
├── Pulumi.staging.yaml
├── Pulumi.prod.yaml
└── requirements.txt
Switch between stacks:
pulumi stack select dev
pulumi stack select staging
pulumi stack select prod
Stack-Specific Configuration
Configure values for each stack using pulumi config set. These values are stored in the stack's YAML file and read by your code via pulumi.Config().
# Configure dev stack
pulumi config set --stack dev instance_count 1
pulumi config set --stack dev instance_type t3.micro
pulumi config set --stack dev database_backup false
pulumi config set --stack dev region us-east-1
# Configure staging stack
pulumi config set --stack staging instance_count 2
pulumi config set --stack staging instance_type t3.small
pulumi config set --stack staging database_backup true
pulumi config set --stack staging region us-east-1
# Configure prod stack
pulumi config set --stack prod instance_count 4
pulumi config set --stack prod instance_type t3.large
pulumi config set --stack prod database_backup true
pulumi config set --stack prod region us-west-2
Your __main__.py reads these values and adjusts resources accordingly:
import pulumi
import pulumi_aws as aws
config = pulumi.Config()
# Read stack-specific config; provide defaults
instance_count = config.get_int('instance_count') or 1
instance_type = config.get('instance_type') or 't3.micro'
enable_backups = config.get_bool('database_backup') or False
region = config.get('region') or 'us-east-1'
environment = pulumi.get_stack() # 'dev', 'staging', or 'prod'
# Create security group
sg = aws.ec2.SecurityGroup(f'app-sg-{environment}',
description=f'Security group for {environment}',
ingress=[
aws.ec2.SecurityGroupIngressArgs(
protocol='tcp',
from_port=80,
to_port=80,
cidr_blocks=['0.0.0.0/0']
)
]
)
# Create RDS database with backup toggle
db = aws.rds.Instance(f'app-db-{environment}',
allocated_storage=20,
storage_type='gp2',
engine='postgres',
engine_version='14.7',
instance_class=f'db.{instance_type.replace("t3.", "t3.") or "t3.micro"}',
backup_retention_period=7 if enable_backups else 1,
skip_final_snapshot=True,
password='changeme123!', # Use Pulumi secrets in real code
username='postgres'
)
# Create ASG with stack-specific instance count
asg = aws.autoscaling.Group(f'app-asg-{environment}',
desired_capacity=instance_count,
min_size=1,
max_size=instance_count * 2,
launch_configuration_name='app-lc',
)
pulumi.export('environment', environment)
pulumi.export('instance_count', instance_count)
pulumi.export('db_endpoint', db.endpoint)
Now deploy to each stack with different values:
pulumi stack select dev
pulumi up # 1 t3.micro instance, no backups
pulumi stack select prod
pulumi up # 4 t3.large instances, hourly backups
Using Stack Outputs with Stack References
Stacks often depend on each other. For example, the application stack (EC2 instances, load balancer) needs the database endpoint from the data stack. Pulumi's stack references let you read outputs from one stack and use them in another.
Assume you have two projects:
- data-infra: Creates the database (separate stack)
- app-infra: Creates the application (depends on the database)
In app-infra/__main__.py, reference the database endpoint from data-infra:
import pulumi
import pulumi_aws as aws
# Reference the database endpoint from the data-infra stack
data_stack_ref = pulumi.StackReference(f"organization/data-infra/prod")
db_endpoint = data_stack_ref.get_output('db_endpoint')
# Create application security group allowing database access
app_sg = aws.ec2.SecurityGroup('app-sg',
description='Application security group',
ingress=[
# Allow traffic to database
aws.ec2.SecurityGroupIngressArgs(
protocol='tcp',
from_port=5432, # PostgreSQL
to_port=5432,
cidr_blocks=[db_endpoint]
)
]
)
# Export for other stacks to reference
pulumi.export('app_sg_id', app_sg.id)
Stack references work across projects and organizations. This pattern scales to hundreds of stacks.
Promoting Code and Config Across Stacks
A typical workflow promotes code from dev → staging → prod:
- Develop locally in the dev stack.
- Test in staging with production-like configuration.
- Merge to main (in Git).
- Deploy to prod with production configuration.
Example CI/CD workflow (GitHub Actions):
name: Pulumi Deploy
on:
push:
branches: [main]
jobs:
deploy-prod:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pulumi/actions@v5
with:
command: up
stack-name: prod
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
This automatically deploys to prod when main is updated. For safety, add approval gates:
deploy-prod:
environment:
name: production
# Requires manual approval in GitHub UI before deploying
runs-on: ubuntu-latest
steps:
# Deploy steps...
Best Practices for Stacks
Use descriptive stack names: dev, staging, prod are clear. Avoid test1, test2.
Keep config DRY: Don't hardcode values in code. Use config.get() for all environment-specific values.
Document stack purposes: Add a comment in your project explaining which stack is for what.
# Stack purposes:
# - dev: Fast iteration, minimal resources
# - staging: Production-like setup for testing
# - prod: High availability, backups enabled
Separate sensitive config: Use Pulumi secrets for passwords, API keys, and tokens:
pulumi config set --secret db_password 'mysecretpassword'
In code:
config = pulumi.Config()
db_password = config.require_secret('db_password')
Test stack promotion: Before promoting to prod, ensure the same code and config work in staging.
Key Takeaways
- Stacks are named deployments of the same code with different values (dev, staging, prod).
- Use
pulumi config setto store stack-specific values; read them withconfig.get(). - Deploy the same infrastructure to multiple stacks:
pulumi stack select <name>; pulumi up. - Use stack references to read outputs from one stack in another (for cross-stack dependencies).
- Promote code through stacks: develop in dev, test in staging, deploy to prod via CI/CD with approval gates.
Frequently Asked Questions
How do I safely manage secrets (passwords, API keys) in stacks?
Use pulumi config set --secret key value. Pulumi encrypts secrets in the state file and only decrypts them at deploy time. In code, read them with config.require_secret('key').
Can I have a stack-specific requirements.txt or dependencies?
Yes, but it's rare. Usually, all stacks use the same Python environment. If needed, create conditional imports in __main__.py based on pulumi.get_stack().
What happens if I accidentally delete a stack?
Use pulumi stack rm <stack_name>. This deletes the stack metadata and (optionally) the resources. If you delete prod accidentally, the resources may remain in AWS; you must clean them up manually or redeploy from stack snapshots.
How do I share stack config across a team?
Commit Pulumi.*.yaml files to Git. Secrets are encrypted, so it's safe. Team members run pulumi stack select and pulumi up with the shared config.
Can I have per-person development stacks?
Yes. Create stacks named dev-alice, dev-bob, etc. Each developer manages their own stack without interfering with others.