Pulumi Component Resources: When to Build Your Own Infrastructure Building Blocks
Pulumi Component Resources let platform teams package organizational standards into typed, versioned, testable building blocks. Here's how they work, a working example, and when not to build one.
25 Oct 2025, 03:12 UTC

Your platform team just reviewed the fourth pull request this month where an application team created an S3 bucket without encryption, without tags, and with a name that violates your convention. The fix isn't another wiki page. It's making the correct way the easy way — and in Pulumi, the tool for that is the Component Resource.
A Component Resource wraps several child resources behind a single logical resource with your own type name, like mycorp:platform:Database. Application teams see a clean, typed API; your organizational standards live inside the implementation. This post covers how they work, when they're worth building, and where they bite.
What a Component Resource actually is
A Component Resource is a logical grouping, not a deployable thing itself. It inherits from Pulumi's ComponentResource base class, registers a custom type token, and creates real provider resources (an RDS instance, a subnet, an IAM role) as children. Pulumi's engine tracks the parent/child relationship automatically, so pulumi preview shows a tidy tree instead of a flat list of forty resources, and you can target the whole subtree with pulumi up --target urn:pulumi:...::mycorp:platform:Database::orders-db.
Because components are written in TypeScript, Python, Go, or C#, you get real type checking on inputs, IDE autocomplete, and compile-time errors when an application team passes a string where you expect a number. Compare that with YAML-templated modules where a typo in a parameter name surfaces at deploy time — or not at all.
A worked example: a compliant bucket
Here's a minimal TypeScript component that bakes in tagging and encryption. Run this in any Pulumi TypeScript project with the AWS provider installed; no special permissions beyond your normal Pulumi/AWS credentials are needed to preview it.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
export interface SecureBucketArgs {
team: pulumi.Input<string>;
versioning?: pulumi.Input<boolean>;
}
export class SecureBucket extends pulumi.ComponentResource {
public readonly bucketName: pulumi.Output<string>;
constructor(name: string, args: SecureBucketArgs, opts?: pulumi.ComponentResourceOptions) {
super("mycorp:platform:SecureBucket", name, {}, opts);
const bucket = new aws.s3.Bucket(name, {
versioning: { enabled: args.versioning ?? true },
serverSideEncryptionConfiguration: {
rule: { applyServerSideEncryptionByDefault: { sseAlgorithm: "AES256" } },
},
tags: { team: args.team, managedBy: "platform" },
}, { parent: this });
new aws.s3.BucketPublicAccessBlock(`${name}-pab`, {
bucket: bucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
}, { parent: this });
this.bucketName = bucket.bucket;
this.registerOutputs({ bucketName: this.bucketName });
}
}Two details matter. First, every child gets { parent: this } — that's what builds the URN hierarchy. Second, registerOutputs tells the engine the component is finished constructing, which keeps previews accurate. Application teams then consume it in three lines:
const assets = new SecureBucket("assets", { team: "checkout" });
export const assetsBucket = assets.bucketName;Verify the hierarchy locally with pulumi preview — you should see the SecureBucket node with the bucket and public-access block nested beneath it. No cloud changes happen during preview.
Versioning and testing: the real payoff
Because a component is ordinary code, you publish it like ordinary code: an npm package (or PyPI/Go module) with semantic versioning. Consuming projects pin a version in package.json, lockfiles record exactly what's deployed, and Dependabot can open PRs when you ship 1.4.0. That gives you an upgrade path that copy-pasted modules never had.
Testing is similarly un-exotic. Pulumi's pulumi.runtime.setMocks() lets you run the program in-process with fake provider responses — no cloud credentials, tests in milliseconds, inside Jest or pytest. A useful test asserts that instantiating SecureBucket produces a bucket whose tags include the team name and whose encryption block exists. That catches regressions in your standards before any stack does.
The trade-offs nobody mentions in the demo
Don't abstract too early. If two stacks need similar infrastructure, a plain function is fine. Promote to a versioned Component Resource when you feel actual reuse-and-upgrade pain — otherwise you end up maintaining a framework instead of shipping product.
Breaking changes are coordination problems. Renaming an input or changing an output type forces every consuming stack to upgrade. Adopt a deprecation policy up front (for example, support the current and previous major versions, and mark old inputs with a deprecation warning before removal).
Multi-cloud abstraction leaks. A component that "works on AWS and Azure" still exposes provider-specific properties unless you deliberately normalize them — and that normalization layer becomes its own maintenance burden. Be honest about whether you're abstracting or just renaming.
Preview performance degrades with depth. Very deep component hierarchies (roughly ten or more levels) slow URN resolution during preview. Keep hierarchies shallow; compose at one or two levels where practical. If preview feels sluggish, pulumi preview --json includes timing data you can inspect to confirm where the time goes.
Where to start
Pick one resource your teams repeatedly get wrong — a bucket, a database, a queue with its DLQ. Write it as a component, publish it as 0.1.0, migrate one stack, and watch what the next review cycle looks like. If the component earns its keep, version it seriously and write the deprecation policy before you need it. The goal isn't abstraction for its own sake; it's making the compliant path the path of least resistance.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.