Encapsulate Repeated Pulumi Infrastructure with Component Resources
Stop copying VPC and subnet boilerplate across stacks. Build, publish, and consume a Pulumi ComponentResource in TypeScript.
08 Mar 2026, 04:19 UTC

Your application team needs a new environment. Someone copies the VPC, subnet, and NAT gateway code from the staging stack, changes the CIDR block, and forgets to copy the cost-allocation tags. Six months later, production and staging have different routing rules, and an incident reveals that one subnet lacks a route to the NAT gateway. The problem is not a missing cloud skill; it is repeated infrastructure code without a shared contract.
The useful takeaway: define the pattern once as a Pulumi component resource, publish it as a versioned package, and let teams instantiate it with strongly typed inputs. A component resource is a logical grouping construct in Pulumi that registers as a single resource in the state graph while managing multiple child resources underneath. It preserves dependency tracking through Output properties, so downstream resources wait for the component's children to be ready before they are created.
What a component resource changes in your graph
When you extend pulumi.ComponentResource, you call super('pkg:index:ComponentName', name, args, opts) in the constructor. The first argument is a type token that identifies the component in Pulumi's resource graph. Child resources created inside the constructor are registered with the component as their parent, so pulumi preview shows one logical node with expandable children instead of a flat list of unrelated resources. pulumi stack export includes the full expanded tree, which helps with drift detection and auditing.
Worked example: a network stack component
This TypeScript example defines a NetworkStack component that provisions a VPC, public and private subnets, a NAT gateway, and route table associations. It accepts a CIDR block and availability zone count, then exposes the VPC ID and subnet IDs as outputs.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
export interface NetworkStackArgs {
cidrBlock: string;
availabilityZoneCount: number;
environment: string;
}
export class NetworkStack extends pulumi.ComponentResource {
public readonly vpcId: pulumi.Output;
public readonly privateSubnetIds: pulumi.Output;
constructor(
name: string,
args: NetworkStackArgs,
opts?: pulumi.ComponentResourceOptions
) {
super("myorg:index:NetworkStack", name, args, opts);
const vpc = new aws.ec2.Vpc(`${name}-vpc`, {
cidrBlock: args.cidrBlock,
enableDnsHostnames: true,
enableDnsSupport: true,
tags: {
Environment: args.environment,
ManagedBy: "pulumi-component",
},
}, { parent: this });
// Subnets, NAT gateway, and route tables omitted for brevity.
// Each child resource should pass { parent: this }.
this.vpcId = vpc.id;
this.privateSubnetIds = pulumi.output([]);
}
}
Run this code in a Pulumi project directory where @pulumi/pulumi and @pulumi/aws are installed. You need the Pulumi CLI and provider credentials configured for the stack. pulumi preview shows planned changes without applying them. The expected check is that preview output lists myorg:index:NetworkStack as a single resource with child resources nested beneath it.
Publish and consume the component
Package the component in an internal npm registry so other teams can install it like any library. From the component project directory:
npm pack
npm publish --registry
Run npm publish only if you have write access to the target registry; publishing to the public npm registry by accident is a common exposure risk. In a consuming stack, install the package and instantiate the component:
npm install @myorg/pulumi-aws-network@1.0.0
import { NetworkStack } from "@myorg/pulumi-aws-network";
const network = new NetworkStack("prod-network", {
cidrBlock: "10.0.0.0/16",
availabilityZoneCount: 3,
environment: "production",
});
export const vpcId = network.vpcId;
TypeScript consumers get compile-time checking on cidrBlock and availabilityZoneCount. If you change the args interface in a breaking way, consuming stacks fail to compile before they reach pulumi up.
Limitations and how to verify
Components add an abstraction layer. Operators troubleshooting an incident still need to understand the underlying VPC, subnet, and NAT gateway behavior; the component hides but does not remove that complexity. Breaking changes to args or outputs require semantic versioning and coordinated upgrades, because Pulumi does not enforce compatibility automatically. Deeply nested components can enlarge the resource graph and slow down pulumi preview and pulumi stack export.
To verify a component before sharing it, run pulumi preview and confirm the component appears as one node with expandable children. Then run pulumi stack export and inspect the JSON for the component URN and all descendant URNs. For offline testing, use @pulumi/pulumi/testing with a mock provider to assert child resource counts and property values without deploying to AWS.
Start with one repeated pattern
Pick the infrastructure your teams copy most often—often a VPC, database cluster, or Kubernetes ingress setup—and wrap it in a component resource. Publish it internally, require consumers to pin a version, and treat breaking args changes as major releases. The goal is less copied code and more predictable stacks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.