Building Reusable Infrastructure with Pulumi ComponentResource in TypeScript
Learn how to encapsulate cloud resources into reusable, versioned Pulumi ComponentResources in TypeScript, with a concrete VPC example, trade‑offs, and steps to verify the component in your stack.
18 Feb 2026, 05:40 UTC

The duplication problem
When every stack re‑creates the same VPC, security groups, and IAM roles, the code base grows fragile. A change in one stack forces a copy‑paste edit in all the others, and the risk of drift rises with each new environment.
Pulumi’s ComponentResource lets you package a group of cloud resources into a single logical unit with a stable URN. Consumers treat the component like any other resource—declare inputs, await outputs, and compose it into larger architectures—while the implementation details stay hidden.
Anatomy of a ComponentResource
A component is a TypeScript class that extends pulumi.ComponentResource. The constructor receives:
- type token – a unique string like
myorg:network:Vpcthat becomes part of the URN. - name – the logical name used by callers.
- args – an interface describing the inputs the component accepts.
- opts? – optional
ComponentResourceOptions(protect, deleteBeforeReplace, additionalSecretOutputs, providers, …).
Inside the constructor you create child resources (e.g., aws.ec2.Vpc, aws.ec2.Subnet) passing { parent: this } so Pulumi nests them under the component’s URN. Finally you call this.registerOutputs({ … }) to expose values that downstream resources can depend on.
Wiring inputs and outputs
Inputs are plain TypeScript properties on the args interface. Because they are part of the resource’s property bag, Pulumi’s diff engine can detect changes and decide whether child resources need replacement.
Outputs are registered once, typically at the end of the constructor. They become available to callers as Output<T> properties on the component instance. Note that the outputs are not readable until registerOutputs finishes; any imperative code that runs before that point will see undefined.
Worked example: a reusable VPC component
The following snippet shows a minimal, version‑ready component that creates a VPC with two public subnets and exports the VPC ID and subnet IDs.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
export interface VpcArgs {
cidrBlock: string;
availabilityZones: string[]; // must have at least two entries
tags?: { [key: string]: string };
}
export class Vpc extends pulumi.ComponentResource {
public readonly vpcId: pulumi.Output<string>;
public readonly publicSubnetIds: pulumi.Output<string[]>;
constructor(name: string, args: VpcArgs, opts?: pulumi.ComponentResourceOptions) {
super("myorg:network:Vpc", name, {}, opts);
const vpc = new aws.ec2.Vpc(`${name}-vpc`, {
cidrBlock: args.cidrBlock,
enableDnsHostnames: true,
enableDnsSupport: true,
tags: { Name: name, ...args.tags },
}, { parent: this });
const subnets = args.availabilityZones.slice(0, 2).map((az, i) =>
new aws.ec2.Subnet(`${name}-public-${i}`, {
vpcId: vpc.id,
cidrBlock: pulumi.interpolate`${args.cidrBlock.split(".").slice(0,2).join(".")}.${i * 64 + 16}.0/20`,
availabilityZone: az,
mapPublicIpOnLaunch: true,
tags: { Name: `${name}-public-${i}`, ...args.tags },
}, { parent: this })
);
this.vpcId = vpc.id;
this.publicSubnetIds = pulumi.all(subnets.map(s => s.id)).apply(ids => ids);
this.registerOutputs({
vpcId: this.vpcId,
publicSubnetIds: this.publicSubnetIds,
});
}
}
Consumers use it like any resource:
import { Vpc, VpcArgs } from "./vpc";
const prodVpc = new Vpc("prod", {
cidrBlock: "10.10.0.0/16",
availabilityZones: ["us-east-1a", "us-east-1b"],
tags: { Environment: "production" },
});
export const vpcId = prodVpc.vpcId;
export const subnetIds = prodVpc.publicSubnetIds;
Trade‑offs and limitations
- Replacement cascades – Changing an input that the underlying provider treats as immutable (e.g.,
cidrBlockonaws.ec2.Vpc) forces Pulumi to replace the VPC and all child subnets. The component cannot shield you from provider‑level immutability. - Output timing – Outputs are only visible after
registerOutputsruns. If you write imperative logic that readscomponent.vpcIdbefore the constructor returns, you’ll get a pendingOutputthat resolves later. - Over‑encapsulation – Wrapping a single resource in a component adds indirection without benefit. Reserve components for groups that have a stable interface and are reused across stacks.
- Versioning – Publish the component as an npm package (or a private registry) and bump the package version when you make breaking changes to the
argsshape or output names. Consumers can then lock to a specific version.
Actionable next steps
- Run
pulumi new typescriptin a fresh directory to scaffold a project. - Place the
Vpcclass invpc.tsand export it. - Reference the component from
index.tsas shown above. - Execute
pulumi up(requires cloud credentials) and verify that the preview shows a single top‑level resourcemyorg:network:Vpcwith nestedaws:ec2/vpc:Vpcandaws:ec2/subnet:Subnetchildren. - Run
pulumi stack exportand confirm the component’s URN appears once and thatvpcIdandpublicSubnetIdsare present in the stack outputs. - Publish the directory as an npm package (or push to a private registry) and add it as a dependency in other repositories.
By extracting repeated infrastructure into a ComponentResource, you gain a single source of truth, type‑safe inputs, and predictable outputs—making multi‑environment deployments far easier to maintain.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.