skip to content

What is a Pulumi ComponentResource, and what must a ComponentResource subclass do in its constructor for the component to behave correctly?

level: middleimportance: must knowfreq 50%

answer

  1. a node in the resource tree
  2. no cloud API of its own
  3. three-part type token
  4. children need parent: this
  5. finish with registerOutputs

basics

~20 s

A ComponentResource is a logical grouping of other resources exposed as one reusable class — Pulumi's module equivalent. Its constructor must call super with a type token and name, create every child with the parent option set to itself, and finish with registerOutputs.

solid answer

~50 s

A ComponentResource has no cloud API behind it. It is a node in the resource tree that owns children, which is how Pulumi packages reusable abstractions — the counterpart to a Terraform module, except it is just a class you can publish on npm or PyPI. Three obligations in the constructor: call `super("pkg:module:Name", name, {}, opts)` with a three-part type token and the logical name; pass `{ parent: this }` in the options of every child resource so they are nested under the component rather than floating at the stack root; and call `this.registerOutputs({...})` at the end, which signals that the component is fully constructed and publishes its outputs. Skip the parent option and you get a class that is only cosmetically a component — children show up unparented, the resource tree is flat, and options that should flow down, like a provider or a transformation, do not reach them.

code

typescript · 21 lines
typescript
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

export class StaticSite extends pulumi.ComponentResource {
    public readonly bucketName: pulumi.Output<string>;

    constructor(name: string, args: { indexDocument: string },
                opts?: pulumi.ComponentResourceOptions) {
        super("mycorp:web:StaticSite", name, {}, opts);

        const bucket = new aws.s3.Bucket(`${name}-bucket`, {
            website: { indexDocument: args.indexDocument },
        }, { parent: this });

        this.bucketName = bucket.bucket;
        this.registerOutputs({ bucketName: this.bucketName });
    }
}

const site = new StaticSite("marketing", { indexDocument: "index.html" });
export const name = site.bucketName;

go deeper

for a junior

Know that a ComponentResource groups several resources into one reusable class and that you subclass it, calling super with a type token and the name.

for a middle

Explain all three constructor obligations — the three-part type token, parent: this on every child, and registerOutputs at the end — and what each one buys you.

for a senior

Show judgment on where component boundaries belong and what it costs to move one later, since every boundary is a set of URNs that needs aliases to migrate without replacement.

for a principal

Own the platform question: components are versioned library code in your language's registry, so decide how they are published, versioned and consumed across teams, and where the typed interface earns more than a shared module directory would.

## The problem it solves A plain function that creates five resources and returns some of them works fine — until you want the five to be treated as a unit. Pulumi's resource model is a tree, and a function creates no node in it. `ComponentResource` is the class you subclass to create that node. What you get from being a real node: the resources are nested under one URN in `pulumi stack` output and in the Pulumi Cloud resource view; deletion and options propagate down the parent chain; and the component can be published as a normal package in your language's ecosystem for others to `new` up. This last part is the substantive difference from a Terraform module, which is a directory of files consumed by a source address — a Pulumi component is a class, versioned and distributed like any other library code. ## The three constructor obligations ```typescript export class StaticSite extends pulumi.ComponentResource { public readonly bucketName: pulumi.Output<string>; constructor(name: string, args: { indexDocument: string }, opts?: pulumi.ComponentResourceOptions) { super("mycorp:web:StaticSite", name, {}, opts); const bucket = new aws.s3.Bucket(`${name}-bucket`, { website: { indexDocument: args.indexDocument }, }, { parent: this }); this.bucketName = bucket.bucket; this.registerOutputs({ bucketName: this.bucketName }); } } ``` **1. `super(type, name, props, opts)`.** The type token is a three-part string, conventionally `package:module:Type`. It is not decorative — it becomes part of every child's URN and is how the engine identifies the component's kind. Pick it once and do not change it casually, since changing it changes URNs. Passing `opts` through is what lets a caller set a parent, a provider or an alias on the component. **2. `{ parent: this }` on every child.** This is the obligation people forget. Without it, the children register at the stack root, and: - The tree is flat, so nothing visually or structurally connects them to the component. - Options that propagate down the parent chain — the provider, resource transformations, `protect` — do not reach them. - Child URNs are not scoped by the component, so two instances of the component can collide on names unless you manually prefix every one. Prefixing child names with the component's `name` argument, as above, is the standard convention that keeps two instances distinct in a readable way. **3. `this.registerOutputs({...})`.** Call it last. It tells the engine the component is done constructing and records the component's output properties. Omitting it is not always fatal but leaves the component's outputs unrecorded and the engine without a completion signal; treat it as mandatory. ## Exposing outputs A component's public surface is ordinary class fields typed as `Output<T>`. Callers consume them exactly as they would a real resource's attributes — pass them into other resources' args, apply over them, export them from the stack. That is a genuine ergonomic gain over a module system where the interface is a separate declaration file: the type checker enforces the interface, your editor autocompletes it, and a wrong field is a compile error rather than a plan-time one. ## ComponentResourceOptions vs CustomResourceOptions A child resource created by a provider takes `CustomResourceOptions`. A component takes `ComponentResourceOptions`, which adds `providers` — a map letting a caller supply, say, a specific AWS provider for everything inside the component. The distinction matters when you write the constructor signature: type it as `ComponentResourceOptions` so callers can pass a provider set down. ## When not to reach for one A component is worth the ceremony when the group has a real identity — a service, a network, a site. For a two-line helper that computes a tag map or builds an ARN, a plain function is better: no URN, no tree node, nothing to alias later. Over-componentising has a real cost, because every component boundary is a set of URNs you must migrate with aliases if you later change your mind about the structure. ## Remote components, briefly Components written in one language can be packaged as *multi-language* components so a Python program can consume a component authored in TypeScript. That is a packaging concern layered on the same class; the constructor obligations above are unchanged.

  • What actually goes wrong if you forget `{ parent: this }` on the children?
    The children register at the stack root instead of under the component, so the resource tree is flat, their URNs are not scoped by the component, and options that propagate down the parent chain — the provider, transformations, protect — never reach them. Two instances of the component can then also collide on child names.
  • When is a plain function that returns resources the better choice?
    When the group has no identity worth naming — a helper that builds a tag map or assembles an ARN. A component adds URNs and a tree node you must later migrate with aliases if the structure changes, so pay that cost only when the grouping is a real thing like a service or a network.
  • How does a component's interface differ from a Terraform module's?
    A component's interface is a constructor signature and typed Output fields, checked by the compiler and autocompleted in the editor, and it is distributed as a normal package in the language's registry. A module's interface is declared variables and outputs, consumed by a source address, and validated when the tool runs.

It is a folder rather than a file: it holds nothing itself, but everything inside it inherits its location and travels with it.

saying these in an interview costs you the question

  • Thinks a ComponentResource creates cloud infrastructure itself
  • Omits parent: this and calls the result a component
  • Uses an arbitrary one-word string as the type token
  • Skips registerOutputs as optional boilerplate
  • Wraps every two-line helper in a component

context