Skip to main content

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:

StackPurposeInstance TypeDatabaseBackups
devDevelopment; fast iterationt3.microSingle-AZNone
stagingPre-production testingt3.smallMulti-AZDaily
prodProduction; high availabilityt3.largeMulti-AZ + read replicasHourly

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:

  1. data-infra: Creates the database (separate stack)
  2. 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:

  1. Develop locally in the dev stack.
  2. Test in staging with production-like configuration.
  3. Merge to main (in Git).
  4. 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 set to store stack-specific values; read them with config.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.

Further Reading