Pulumi Component Resources

Medium25 min read

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

Component Resource Containing Child Resources
VpcComponent (ComponentResource) aws.ec2.Vpc Public Subnet Private Subnet NAT Gateway Public Route Table Private Route Table
vpc-component.ts
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

component-interface.ts
// 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

packaging.sh
# 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
using-packaged.ts
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

ConceptDescriptionExample
ComponentResourceBase class for componentsextends pulumi.ComponentResource
super()Register component typesuper("custom:mod:Name", name, {}, opts)
{ parent: this }Set parent for child resourcesnew aws.s3.Bucket("b", {}, { parent: this })
registerOutputs()Signal component is completethis.registerOutputs({ id: this.id })
Args interfaceDefine component inputsinterface MyArgs { size: string; }
readonly outputsExpose component outputspublic readonly id: Output<string>
ComponentResourceOptionsOptions type for componentsopts?: pulumi.ComponentResourceOptions