The most surprising thing about managing Pulumi environments is that separate stacks aren’t always the most efficient way to isolate them.

Let’s see how this works with a concrete example. Imagine you’re deploying a simple web application with a database. Here’s a basic Pulumi program in TypeScript:

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const environment = config.require("environment"); // e.g., "dev", "staging", "prod"
const instanceType = config.get("instanceType") || "t2.micro";

// A simple VPC
const vpc = new aws.ec2.Vpc("app-vpc", {
    cidrBlock: "10.0.0.0/16",
    tags: {
        Name: `${environment}-vpc`,
    },
});

// A security group for the web server
const webSg = new aws.ec2.SecurityGroup("web-sg", {
    vpcId: vpc.id,
    description: "Allow HTTP and SSH access",
    ingress: [
        { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
        { protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] },
    ],
    tags: {
        Name: `${environment}-web-sg`,
    },
});

// An EC2 instance for the web server
const webServer = new aws.ec2.Instance("web-server", {
    ami: "ami-0c55b159cbfafe1f0", // Example AMI, replace with a valid one for your region
    instanceType: instanceType,
    vpcSecurityGroupIds: [webSg.id],
    tags: {
        Name: `${environment}-web-server`,
    },
});

// An RDS instance for the database
const dbSubnetGroup = new aws.rds.SubnetGroup("db-subnet-group", {
    subnetIds: [
        new aws.ec2.Subnet("db-subnet-1", {
            vpcId: vpc.id,
            cidrBlock: "10.0.1.0/24",
            availabilityZone: "us-east-1a",
        }).id,
        new aws.ec2.Subnet("db-subnet-2", {
            vpcId: vpc.id,
            cidrBlock: "10.0.2.0/24",
            availabilityZone: "us-east-1b",
        }).id,
    ],
});

const db = new aws.rds.Instance("app-db", {
    allocatedStorage: 20,
    engine: "mysql",
    engineVersion: "8.0",
    instanceClass: "db.t3.micro",
    dbSubnetGroupName: dbSubnetGroup.id,
    skipFinalSnapshot: true,
    tags: {
        Name: `${environment}-db`,
    },
});

export const webServerPublicIp = webServer.publicIp;
export const dbEndpoint = db.endpoint;

Traditionally, you might manage dev, staging, and prod using separate Pulumi stacks. For example:

pulumi stack select dev pulumi config set environment dev pulumi up

pulumi stack select staging pulumi config set environment staging pulumi up

pulumi stack select prod pulumi config set environment prod pulumi up

This approach clearly separates resources. However, it leads to a lot of duplicated configuration and management overhead. You’re essentially running the same Pulumi program multiple times, each with its own set of stack-specific configurations. This can become cumbersome as your infrastructure grows.

A more streamlined approach leverages Pulumi’s configuration system and a single stack to manage multiple environments. Instead of separate stacks for dev, staging, and prod, you can use a single stack (e.g., my-app/dev) and define environment-specific configurations within that stack.

Here’s how you’d set it up:

  1. Create a single stack: pulumi stack init dev

  2. Set the base configuration: pulumi config set environment dev pulumi config set aws:region us-east-1

  3. Define environment-specific overrides: For staging, you might want a larger instance type and a different database configuration. You can overlay these settings:

    # For staging settings, saved under a new config alias
    pulumi config set --stack dev-staging environment staging
    pulumi config set --stack dev-staging aws:region us-east-1
    pulumi config set --stack dev-staging instanceType t3.medium
    pulumi config set --stack dev-staging aws:tags '{"Project": "MyApp", "Environment": "staging"}'
    
    # For prod settings
    pulumi config set --stack dev-prod environment prod
    pulumi config set --stack dev-prod aws:region us-east-1
    pulumi config set --stack dev-prod instanceType t3.xlarge
    pulumi config set --stack dev-prod aws:tags '{"Project": "MyApp", "Environment": "prod"}'
    
  4. Deploy different environments from the same stack: Now, when you deploy, you target the specific configuration alias:

    pulumi up --stack dev (deploys the dev configuration) pulumi up --stack dev-staging (deploys the staging configuration) pulumi up --stack dev-prod (deploys the prod configuration)

In this model, the Pulumi program itself remains largely the same, but the pulumi config set --stack <alias> commands allow you to define distinct sets of configuration values that are applied when you target that specific alias with pulumi up. The environment config value in the program dynamically tailors resource names, tags, and potentially resource properties.

The key insight here is that Pulumi stacks are primarily for logical separation of deployments, not necessarily for physical isolation of configurations. By using configuration aliases, you achieve a similar level of isolation for your deployments while drastically reducing the duplication of your Pulumi code and management complexity. You can also easily share common configurations across different environment aliases by referencing them or by setting them at the project level.

The real power comes when you start parameterizing more of your infrastructure. For instance, you might have different database sizes or ingress rules based on the environment. The Pulumi program can read these values from pulumi config, making it a single source of truth that adapts to each deployment target.

What many overlook is that the pulumi config command, when used with --stack <alias>, doesn’t create a new stack in the traditional sense. Instead, it creates an alias for the current stack that points to a specific set of configuration values. When you pulumi up --stack <alias>, Pulumi loads the program and then applies the configuration associated with that alias, effectively overriding any base configuration for that deployment. This allows you to have multiple "views" or deployment targets from a single underlying stack.

The next step is to explore how to automate these multi-environment deployments using Pulumi’s automation API or CI/CD pipelines.

Want structured learning?

Take the full Pulumi course →