Producer-side stack aliasing for Pulumi using lightweight proxy stacks. Consumers use standard StackReference (zero library dependency), while producers use this library to create alias stacks that re-export outputs from canonical stacks.
When managing infrastructure across multiple environments (dev, staging, prod), consumer projects need to reference shared infrastructure stacks. Traditional approaches either:
- Force consumers to know which stack holds the real resources
- Scatter mapping logic across every consumer project
- Require complex consumer-side resolvers with library dependencies
The aliasing decision belongs with the producer (infrastructure project), not the consumer.
Alias stacks re-export outputs from canonical stacks. Consumers use standard Pulumi StackReference with no library dependency. Producers use this library to create lightweight proxy stacks.
infrastructure/shared → canonical stack, exports real resources
infrastructure/dev → proxy stack, re-exports outputs from shared
infrastructure/staging → proxy stack, re-exports outputs from shared
infrastructure/prod → canonical stack, exports real resources
Consumer uses standard Pulumi:
const stack = new pulumi.StackReference(`org/infrastructure/${pulumi.getStack()}`);
const vpcId = stack.requireOutput("vpcId");Zero consumer dependencies! The consumer has no knowledge of aliasing. When application/dev deploys, it reads infrastructure/dev, which is a proxy stack that re-exports outputs from infrastructure/shared.
Producer projects only:
npm install @egulatee/pulumi-stack-aliasConsumer projects: No installation needed! Use standard Pulumi StackReference.
Create alias stacks that re-export outputs from canonical stacks:
// infrastructure/index.ts
import { createStackAlias } from "@egulatee/pulumi-stack-alias";
import * as pulumi from "@pulumi/pulumi";
const config = new pulumi.Config();
const aliasTarget = config.get("aliasTarget");
if (aliasTarget) {
// This is an alias stack — re-export outputs from target
const alias = createStackAlias({
targetProject: "infrastructure",
targetStack: aliasTarget,
outputs: ["vpcId", "endpoint", "clusterName"],
});
export const vpcId = alias.vpcId;
export const endpoint = alias.endpoint;
export const clusterName = alias.clusterName;
} else {
// This is a canonical stack — create actual resources
const vpc = new aws.ec2.Vpc("main", {
cidrBlock: "10.0.0.0/16",
});
export const vpcId = vpc.id;
export const endpoint = pulumi.output("https://api.example.com");
export const clusterName = pulumi.output("my-cluster");
}Configure your stack files:
# infrastructure/Pulumi.shared.yaml
config:
# No aliasTarget — this is the canonical stack
# infrastructure/Pulumi.dev.yaml
config:
infrastructure:aliasTarget: shared
# infrastructure/Pulumi.staging.yaml
config:
infrastructure:aliasTarget: shared
# infrastructure/Pulumi.prod.yaml
config:
# No aliasTarget — this is a canonical stack (separate from shared)For automatic aliasing without config:
// infrastructure/index.ts
import { createConditionalAlias } from "@egulatee/pulumi-stack-alias";
const alias = createConditionalAlias({
targetProject: "infrastructure",
patterns: [
{ pattern: "*/prod", target: "prod" },
{ pattern: "*/staging", target: "shared" },
{ pattern: "*/dev", target: "shared" },
{ pattern: "*/*-ephemeral", target: "shared" },
],
defaultTarget: "shared",
outputs: ["vpcId", "endpoint", "clusterName"],
});
export const vpcId = alias.vpcId;
export const endpoint = alias.endpoint;
export const clusterName = alias.clusterName;Simplified API for common cases:
import { createSimpleAlias } from "@egulatee/pulumi-stack-alias";
const alias = createSimpleAlias("infrastructure", "shared", ["vpcId"]);
export const vpcId = alias.vpcId;Use standard Pulumi StackReference — no library dependency:
// application/index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Standard Pulumi StackReference — no special library needed!
const infraStack = new pulumi.StackReference(
`${pulumi.getOrganization()}/infrastructure/${pulumi.getStack()}`
);
const vpcId = infraStack.requireOutput("vpcId");
const endpoint = infraStack.requireOutput("endpoint");
const subnet = new aws.ec2.Subnet("app-subnet", {
vpcId: vpcId,
cidrBlock: "10.0.1.0/24",
});The consumer has zero knowledge of aliasing. When application/dev deploys:
- Reads
infrastructure/dev(a proxy stack) - Gets outputs (re-exported from
infrastructure/shared) - No library dependency required!
# Deploy canonical stacks (creates real resources)
pulumi up --stack shared
pulumi up --stack prod
# Deploy alias stacks (creates proxy outputs)
pulumi up --stack dev
pulumi up --stack staging
# Consumer deployments — no alias awareness needed
cd application && pulumi up --stack devAlias stacks capture outputs at deploy time. When canonical stack outputs change, redeploy aliases:
# Update canonical stack
pulumi up --stack shared
# Sync alias stacks (fast — no real resources)
pulumi up --stack dev
pulumi up --stack stagingAutomate alias synchronization in CI/CD:
# .github/workflows/deploy.yml
jobs:
deploy-canonical:
runs-on: ubuntu-latest
steps:
- name: Deploy shared stack
run: pulumi up --stack shared --yes
sync-aliases:
needs: deploy-canonical
runs-on: ubuntu-latest
strategy:
matrix:
stack: [dev, staging]
steps:
- name: Sync alias stack
run: pulumi up --stack ${{ matrix.stack }} --yesAlias deployments are fast (seconds) since they create no real resources — just re-export outputs.
Creates a stack alias that re-exports outputs from a target stack.
Parameters:
config.targetProject(string) - Target project nameconfig.targetStack(string) - Target stack nameconfig.targetOrg(optional string) - Target organization (defaults to current org)config.outputs(string[]) - List of output names to re-export
Returns: AliasExports - Record of Pulumi Outputs
Example:
const alias = createStackAlias({
targetProject: "infrastructure",
targetStack: "shared",
outputs: ["vpcId", "endpoint"],
});
export const vpcId = alias.vpcId;
export const endpoint = alias.endpoint;Creates a conditional alias based on pattern matching.
Parameters:
config.targetProject(string) - Target project nameconfig.patterns(PatternRule[]) - Pattern matching rules (evaluated in order, first match wins)config.defaultTarget(optional string) - Default target if no pattern matchesconfig.targetOrg(optional string) - Target organization (defaults to current org)config.outputs(string[]) - List of output names to re-export
Returns: AliasExports - Record of Pulumi Outputs
Example:
const alias = createConditionalAlias({
targetProject: "infrastructure",
patterns: [
{ pattern: "*/prod", target: "prod" },
{ pattern: "*/dev", target: "shared" },
],
defaultTarget: "shared",
outputs: ["vpcId"],
});Simplified API for creating aliases.
Parameters:
targetProject(string) - Target project nametargetStack(string) - Target stack nameoutputs(string[]) - List of output names to re-export
Returns: AliasExports - Record of Pulumi Outputs
Example:
const alias = createSimpleAlias("infrastructure", "shared", ["vpcId"]);
export const vpcId = alias.vpcId;Pattern matching with wildcard support.
Wildcard rules:
*matches any value*-suffixmatches strings ending with-suffixprefix-*matches strings starting withprefix-exactmatches exactly
Example:
import { matchesPattern } from "@egulatee/pulumi-stack-alias";
matchesPattern("*/dev", "myproject", "dev") // true
matchesPattern("*/*-ephemeral", "app", "pr-123-ephemeral") // true
matchesPattern("app-*/prod-*", "app-api", "prod-us") // truePatterns follow the format: "projectPattern/stackPattern"
Examples:
"*/dev"- Any project, stack must be "dev""myproject/*"- Project must be "myproject", any stack"*/*-ephemeral"- Any project, stack must end with "-ephemeral""app-*/prod-*"- Project must start with "app-", stack must start with "prod-"
| Concern | Producer Redirect (v0.1.0) | Producer Proxy (v0.2.0) | Consumer Config |
|---|---|---|---|
| Who owns mapping | Producer ✓ | Producer ✓ | Consumer |
| Consumer library dependency | Yes | None ✓ | Varies |
| Alias stack weight | Trivial (one pointer) | Light (output refs) | N/A |
| Output freshness | Always live ✓ | Stale until redeployed* | Always live ✓ |
| Consumer code | resolveStackRef(...) |
new StackReference(...) ✓ |
Custom |
| CI/CD orchestration | Not needed | Recommended | Not needed |
* Mitigated with CI/CD automation — alias deployments are fast (seconds)
✅ Zero consumer dependencies - Consumers use standard Pulumi StackReference
✅ Producer-controlled - Infrastructure projects own aliasing decisions
✅ Type-safe - Full TypeScript support
✅ Flexible patterns - Wildcard pattern matching for conditional aliasing
✅ CI/CD friendly - Fast alias deployments (no real resources)
✅ Simple API - Three functions cover all use cases
Pros:
- Zero consumer library dependency
- Standard Pulumi patterns
- Simple mental model
Cons:
- Alias stacks need redeployment when canonical outputs change (mitigated with CI/CD)
- Slightly more operational overhead than redirect pattern (but eliminates consumer dependencies)
Deploy infrastructure once, route dev/staging to it:
const alias = createConditionalAlias({
targetProject: "infrastructure",
patterns: [
{ pattern: "*/prod", target: "prod" },
],
defaultTarget: "shared",
outputs: ["vpcId", "endpoint"],
});Route ephemeral stacks to shared infrastructure:
const alias = createConditionalAlias({
targetProject: "infrastructure",
patterns: [
{ pattern: "*/*-ephemeral", target: "shared" },
{ pattern: "*/prod", target: "prod" },
],
defaultTarget: "shared",
outputs: ["vpcId"],
});Route regional stacks to regional infrastructure:
const alias = createConditionalAlias({
targetProject: "infrastructure",
patterns: [
{ pattern: "*/us-*", target: "us-east" },
{ pattern: "*/eu-*", target: "eu-west" },
],
outputs: ["vpcId"],
});See the examples directory for complete implementations:
- Simple Alias - Basic proxy stack
- Conditional Alias - Pattern-based aliasing
This is a breaking change — complete API rewrite.
Before (v0.1.0):
// infrastructure/index.ts
const config = new pulumi.Config();
const aliasTarget = config.get("aliasTarget");
if (aliasTarget) {
export const _canonicalStack = aliasTarget;
} else {
export const vpcId = vpc.id;
}After (v0.2.0):
// infrastructure/index.ts
import { createStackAlias } from "@egulatee/pulumi-stack-alias";
const config = new pulumi.Config();
const aliasTarget = config.get("aliasTarget");
if (aliasTarget) {
const alias = createStackAlias({
targetProject: "infrastructure",
targetStack: aliasTarget,
outputs: ["vpcId", "endpoint"],
});
export const vpcId = alias.vpcId;
export const endpoint = alias.endpoint;
} else {
export const vpcId = vpc.id;
export const endpoint = /* ... */;
}Before (v0.1.0):
import { resolveStackRef } from "@egulatee/pulumi-stack-alias";
const infraStack = resolveStackRef("infrastructure");
const vpcId = infraStack.apply(ref => ref.requireOutput("vpcId"));After (v0.2.0):
// NO LIBRARY IMPORT NEEDED!
const infraStack = new pulumi.StackReference(
`${pulumi.getOrganization()}/infrastructure/${pulumi.getStack()}`
);
const vpcId = infraStack.requireOutput("vpcId");- Update package.json: Bump to
@egulatee/pulumi-stack-alias@^0.2.0 - Update producer code: Replace
_canonicalStackexports withcreateStackAlias()calls - Update consumer code: Replace
resolveStackRef()with standardnew StackReference() - Remove consumer dependency: Uninstall
@egulatee/pulumi-stack-aliasfrom consumer projects - Redeploy all stacks: Alias stacks need one-time redeployment to export new outputs
- Set up CI/CD: Add alias sync jobs to keep outputs fresh
Contributions are welcome! Please see the GitHub issues for planned features and enhancements.
MIT © Eric Gulatee