Skip to main content

Pulumi Ep 8: Cross-Stack References (Micro-Stacks)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
pulumi - This article is part of a series.
Part 8: This Article
As your company scales, the “Blast Radius” of your IaC becomes a major concern. If your core networking infrastructure (VPCs, Subnets, Transit Gateways) lives in the exact same index.ts file as your application’s Lambda functions, a junior developer editing the Lambda could accidentally take down the entire network. We must split the code into isolated projects.

1. The Monolith vs Micro-Stacks Architecture
#

In Terraform, splitting states is achieved using the terraform_remote_state data source. In Pulumi, it is achieved via Cross-Stack References.

Instead of one massive pulumi-project, you create multiple independent projects:

  1. core-network: Provisions the VPC and Subnets. Usually managed by the Platform/Network team. Updated rarely.
  2. shared-data: Provisions RDS Databases and Redis clusters.
  3. app-frontend: Provisions the React app and CDN. Updated daily by frontend devs.

Because app-frontend is completely decoupled from core-network, a failed deployment in the frontend cannot possibly affect the VPC.

But there is a catch: the Database needs to know the Subnet IDs, and the Frontend needs to know the Database connection string. How do we pass this data between isolated projects?


2. Exporting Data from the Core Network
#

Let’s look at the core-network project.

For another stack to consume data from this stack, we must explicitly export it from the index.ts file.

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

const vpc = new aws.ec2.Vpc("main-vpc", {
    cidrBlock: "10.0.0.0/16",
});

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

// CRITICAL: We must export the IDs so other stacks can read them!
export const vpcId = vpc.id;
export const publicSubnetId = publicSubnet.id;

When you run pulumi up in this directory, Pulumi saves these exported values into the Pulumi Service state file under the stack’s Fully Qualified Name (e.g., my-org/core-network/prod).


3. Consuming Data with StackReference
#

Now, let’s move to a completely different directory: app-frontend.

We need to deploy an EC2 instance, and it must be placed inside the Subnet created by the core-network stack.

To read the exported variables from another stack, we instantiate the pulumi.StackReference class.

// Inside app-frontend/index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// 1. Initialize the StackReference
// The string must match: <organization>/<project>/<stack>
const networkStack = new pulumi.StackReference("my-org/core-network/prod");

// 2. Extract the variables
// getOutput() returns a pulumi.Output<any>. It is completely asynchronous!
const vpcId = networkStack.getOutput("vpcId");
const subnetId = networkStack.getOutput("publicSubnetId");

// 3. Use the cross-stack data!
const webSg = new aws.ec2.SecurityGroup("web-sg", {
    // Passing the Output<any> directly into the Security Group
    vpcId: vpcId, 
    ingress: [{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] }]
});

const webServer = new aws.ec2.Instance("web-server", {
    ami: "ami-0fc5d935ebf8bc3bc",
    instanceType: "t3.micro",
    // Passing the Output<any> directly into the EC2 Instance
    subnetId: subnetId,
    vpcSecurityGroupIds: [webSg.id],
});

Strongly Typing Cross-Stack Data
#

Notice that getOutput() returns an Output<any>. Because TypeScript doesn’t know what is inside the remote state file, it defaults to the unsafe any type.

If you want strict type safety, you can cast the output, or use .apply():

// Safely cast to ensure TypeScript knows this is a string
const vpcId = networkStack.getOutput("vpcId") as pulumi.Output<string>;

4. The Rules of Cross-Stack References
#

Cross-Stack References are incredibly powerful, but they introduce hard dependencies between your projects. You must follow these rules:

  1. Ordering Matters: You cannot run pulumi up on the app-frontend stack if the core-network stack hasn’t been deployed yet. The getOutput() call will fail because the state file doesn’t exist.
  2. Destruction Prevention: The Pulumi Service tracks these relationships! If you try to run pulumi destroy on the core-network stack while the app-frontend stack is still active, the Pulumi Service will BLOCK the destruction and throw an error. This is a massive safety net that prevents you from deleting a VPC that currently holds active EC2 instances.
  3. Secrets are Maintained: If the core-network exported a password (which is encrypted), getOutput() will pull that password down still encrypted. Pulumi automatically flags the local variable as a Secret, ensuring end-to-end security across stack boundaries.

Troubleshooting & Common Errors
#

  1. error: unable to read stack 'org/project/stack'

    • Root Cause: The StackReference string is misspelled, or your Pulumi CLI is logged into the wrong backend (e.g., logged into local instead of the Pulumi Service).
    • Solution: Verify the Fully Qualified Name in the Pulumi Web UI.
  2. Type 'Output<any>' is not assignable to type 'string'

    • Root Cause: You tried to pass the result of getOutput() into a function that expects a raw string (like console.log).
    • Solution: Remember Episode 5. You must use .apply() to manipulate the data inside an Output.

Conclusion & Next Steps
#

By splitting your infrastructure into Micro-Stacks and binding them with StackReference, you have minimized the blast radius of deployments. Different teams can now manage their own IaC codebases independently, referencing shared resources safely.

So far, we have only used the @pulumi/aws package. But the Pulumi ecosystem is vast. What if you need to provision resources in GitHub, DataDog, and AWS simultaneously?

In Episode 9: Pulumi Packages and the Registry, we will explore how to find, install, and orchestrate third-party providers to build multi-cloud architectures.

pulumi - This article is part of a series.
Part 8: This Article