What Are Components?
Why Component Resources Matter
The Problem: Infrastructure code becomes repetitive when similar resource patterns (VPC + subnets + route tables) are used across multiple projects and stacks.
The Solution: Component resources let you encapsulate multiple related resources into a single reusable abstraction with a clean API, just like classes in object-oriented programming.
Real Impact: Teams can create internal infrastructure libraries, enforce standards, and reduce hundreds of lines into a single component instantiation.
Real-World Analogy
Think of component resources like prefabricated building modules:
- Component Class = The module blueprint (bathroom module, kitchen module)
- Args Interface = The customization options (size, color, fixtures)
- Child Resources = The individual parts inside (pipes, tiles, wiring)
- registerOutputs = The inspection certificate listing what was built
- Instantiation = Ordering and installing a module in your building
Component vs Custom Resources
Encapsulation
Group related resources (VPC, subnets, gateways) into a single logical unit with a clean interface.
Reusability
Use the same component across projects, teams, and stacks without copy-pasting resource definitions.
Abstraction
Hide implementation complexity behind a simple API. Users don't need to know the internals.
Organization
Child resources appear nested under the component in the Pulumi console and CLI output.
Creating Components
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Define the component's input arguments
interface VpcArgs {
cidrBlock: string;
numAvailabilityZones: number;
enableNatGateway?: boolean;
tags?: Record<string, string>;
}
// Define the component resource class
class VpcComponent extends pulumi.ComponentResource {
public readonly vpcId: pulumi.Output<string>;
public readonly publicSubnetIds: pulumi.Output<string>[];
public readonly privateSubnetIds: pulumi.Output<string>[];
constructor(name: string, args: VpcArgs, opts?: pulumi.ComponentResourceOptions) {
// Register this component with Pulumi
super("custom:network:VpcComponent", name, {}, opts);
// Create the VPC as a child resource
const vpc = new aws.ec2.Vpc(`${name}-vpc`, {
cidrBlock: args.cidrBlock,
enableDnsHostnames: true,
enableDnsSupport: true,
tags: { ...args.tags, Name: `${name}-vpc` },
}, { parent: this }); // parent: this is key!
this.vpcId = vpc.id;
this.publicSubnetIds = [];
this.privateSubnetIds = [];
// Register outputs
this.registerOutputs({
vpcId: this.vpcId,
});
}
}
// Use the component
const network = new VpcComponent("main", {
cidrBlock: "10.0.0.0/16",
numAvailabilityZones: 3,
enableNatGateway: true,
tags: { Environment: "prod" },
});
export const vpcId = network.vpcId;
Component Inputs & Outputs
// Best practice: define clear input interfaces
interface StaticWebsiteArgs {
// Required inputs
domainName: string;
contentPath: string;
// Optional inputs with defaults
indexDocument?: string; // default: "index.html"
errorDocument?: string; // default: "404.html"
enableCdn?: boolean; // default: true
priceClass?: string; // default: "PriceClass_100"
}
// Component exposes clear outputs
class StaticWebsite extends pulumi.ComponentResource {
public readonly bucketName: pulumi.Output<string>;
public readonly websiteUrl: pulumi.Output<string>;
public readonly cdnDomain: pulumi.Output<string>;
// ... constructor creates S3, CloudFront, Route53 ...
}
Registering Child Resources
Key Rules for Child Resources
- Always pass
{ parent: this }: This establishes the parent-child relationship in the resource tree - Call
registerOutputs(): Signal that all child resources have been created - Use component name as prefix: Ensure child resource names are unique across instances
- Expose outputs as readonly properties: Users access component data through typed properties
Packaging Components
# Project structure for a component package
my-components/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # Re-exports all components
│ ├── vpc.ts # VPC component
│ ├── database.ts # Database component
│ └── staticSite.ts # Static website component
└── README.md
# Publish to npm
npm publish
# Use in another project
npm install @myorg/pulumi-components
import { VpcComponent, StaticWebsite } from "@myorg/pulumi-components";
// Create infrastructure with one line per component
const network = new VpcComponent("prod", {
cidrBlock: "10.0.0.0/16",
numAvailabilityZones: 3,
});
const site = new StaticWebsite("marketing", {
domainName: "www.example.com",
contentPath: "./dist",
});
export const url = site.websiteUrl;
Common Pitfall
Problem: Forgetting to pass { parent: this } to child resources causes them to appear as top-level resources, not nested under the component.
Solution: Always include { parent: this } in the options (third argument) of every resource created inside a component constructor. This is what makes the component tree work.
Quick Reference
| Concept | Description | Example |
|---|---|---|
ComponentResource | Base class for components | extends pulumi.ComponentResource |
super() | Register component type | super("custom:mod:Name", name, {}, opts) |
{ parent: this } | Set parent for child resources | new aws.s3.Bucket("b", {}, { parent: this }) |
registerOutputs() | Signal component is complete | this.registerOutputs({ id: this.id }) |
Args interface | Define component inputs | interface MyArgs { size: string; } |
readonly outputs | Expose component outputs | public readonly id: Output<string> |
ComponentResourceOptions | Options type for components | opts?: pulumi.ComponentResourceOptions |