Pulumi Stack References

Medium20 min read

What Are Stack References?

Why Stack References Matter

The Problem: Large infrastructure projects become unwieldy in a single stack. Teams need to split infrastructure into logical units while still sharing data between them.

The Solution: Stack references let one Pulumi stack read the outputs of another stack, enabling modular infrastructure with clear boundaries and dependencies.

Real Impact: Organizations can decompose monolithic infrastructure into micro-stacks managed by different teams, each deploying independently while staying connected.

Real-World Analogy

Think of stack references like building a city:

  • Network Stack = The city's road system and utilities (built first)
  • Database Stack = The data centers (needs road addresses from network)
  • App Stack = The office buildings (needs roads and data center connections)
  • Stack Outputs = The published addresses and connection points
  • Stack References = Looking up addresses in the city directory

Stack Reference Benefits

Team Autonomy

Different teams can own and deploy their stacks independently without affecting other stacks.

Blast Radius

A failed deployment only affects one stack, not the entire infrastructure.

Faster Deployments

Smaller stacks deploy faster because Pulumi processes fewer resources.

Clear Contracts

Stack outputs define clear interfaces between infrastructure components.

Creating Outputs

network-stack/index.ts
import * as aws from "@pulumi/aws";

// Create networking infrastructure
const vpc = new aws.ec2.Vpc("main", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
});

const publicSubnet = new aws.ec2.Subnet("public", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    mapPublicIpOnLaunch: true,
});

const privateSubnet = new aws.ec2.Subnet("private", {
    vpcId: vpc.id,
    cidrBlock: "10.0.2.0/24",
});

// Export values for other stacks to consume
export const vpcId = vpc.id;
export const publicSubnetId = publicSubnet.id;
export const privateSubnetId = privateSubnet.id;

Consuming Stack References

Stack Reference Flow
Network Stack export vpcId = vpc.id export subnetId = subnet.id export sgId = sg.id StackReference getOutput() App Stack const vpcId = ref.getOutput("vpcId") new aws.ecs.Service({ vpcId })
app-stack/index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// Reference the network stack
const stack = pulumi.getStack();  // "dev", "prod", etc.
const networkRef = new pulumi.StackReference(`org/network-stack/${stack}`);

// Read outputs from the network stack
const vpcId = networkRef.getOutput("vpcId");
const subnetId = networkRef.getOutput("privateSubnetId");

// Use the outputs as inputs to new resources
const securityGroup = new aws.ec2.SecurityGroup("app-sg", {
    vpcId: vpcId,
    ingress: [{
        protocol: "tcp",
        fromPort: 8080,
        toPort: 8080,
        cidrBlocks: ["10.0.0.0/16"],
    }],
});

// Read a secret output (remains encrypted)
const dbPassword = networkRef.getOutput("dbPassword");

Cross-Stack Patterns

Common Micro-Stack Architecture

  • Layer 1 - Network: VPC, subnets, NAT gateways, route tables
  • Layer 2 - Security: IAM roles, security groups, KMS keys
  • Layer 3 - Data: RDS databases, ElastiCache, S3 buckets
  • Layer 4 - Compute: ECS/EKS clusters, Lambda functions, load balancers
  • Layer 5 - App: Application services, API Gateway, CloudFront

Organizational Patterns

environment-aware-refs.ts
import * as pulumi from "@pulumi/pulumi";

const stack = pulumi.getStack();
const org = pulumi.getOrganization();
const config = new pulumi.Config();

// Pattern: Same environment across stacks
const networkRef = new pulumi.StackReference(
    `${org}/network/${stack}`
);
const dataRef = new pulumi.StackReference(
    `${org}/database/${stack}`
);

// Pattern: Shared services stack (always prod)
const sharedRef = new pulumi.StackReference(
    `${org}/shared-services/prod`
);

// Use typed getOutput with requireOutput
const vpcId = networkRef.requireOutput("vpcId");
const dbEndpoint = dataRef.requireOutput("endpoint");
const sharedBucketArn = sharedRef.requireOutput("logBucketArn");

Common Pitfall

Problem: Circular dependencies between stacks (Stack A references Stack B, which references Stack A).

Solution: Design your stack architecture as a directed acyclic graph (DAG). Lower-level stacks (network) should never reference higher-level stacks (app). If two stacks need to share data, extract the shared part into a lower-level stack.

Quick Reference

MethodDescriptionExample
new StackReference()Create a stack referencenew pulumi.StackReference("org/project/stack")
.getOutput()Get an output (optional)ref.getOutput("vpcId")
.requireOutput()Get required output (throws)ref.requireOutput("vpcId")
.getOutputDetails()Get output with secret inforef.getOutputDetails("password")
export constExport value for other stacksexport const vpcId = vpc.id
pulumi.getStack()Get current stack nameconst env = pulumi.getStack()
pulumi.getOrganization()Get current organizationconst org = pulumi.getOrganization()