Claude
Skills
Sign in
Back

pulumi-component

Included with Lifetime
$97 forever

Guide for authoring Pulumi ComponentResource classes. Use when creating reusable infrastructure components, designing component interfaces, setting up multi-language support, or distributing component packages.

Design

What this skill does


# Authoring Pulumi Components

A ComponentResource groups related infrastructure resources into a reusable, logical unit. Components make infrastructure easier to understand, reuse, and maintain. Components appear as a single node with children nested underneath in `pulumi preview`/`pulumi up` output and in the Pulumi Cloud console.

This skill covers the full component authoring lifecycle. For general Pulumi coding patterns (Output handling, secrets, aliases, preview workflows), use the `pulumi-best-practices` skill instead.

## When to Use This Skill

Invoke this skill when:

- Creating a new ComponentResource class
- Designing the args interface for a component
- Making a component consumable from multiple Pulumi languages
- Publishing or distributing a component package
- Refactoring inline resources into a reusable component
- Debugging component behavior (missing outputs, stuck creating, children at wrong level)

## Component Anatomy

Every component has four required elements:

1. **Extend ComponentResource** and call `super()` with a type URN
2. **Accept standard parameters**: name, args, and `ComponentResourceOptions`
3. **Set `parent: this`** on all child resources
4. **Call `registerOutputs()`** at the end of the constructor

### TypeScript

```typescript
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

interface StaticSiteArgs {
    indexDocument?: pulumi.Input<string>;
    errorDocument?: pulumi.Input<string>;
}

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

    constructor(name: string, args: StaticSiteArgs, opts?: pulumi.ComponentResourceOptions) {
        // 1. Call super with type URN: <package>:<module>:<type>
        super("myorg:index:StaticSite", name, {}, opts);

        // 2. Create child resources with parent: this
        const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });

        const website = new aws.s3.BucketWebsiteConfigurationV2(`${name}-website`, {
            bucket: bucket.id,
            indexDocument: { suffix: args.indexDocument ?? "index.html" },
            errorDocument: { key: args.errorDocument ?? "error.html" },
        }, { parent: this });

        // 3. Expose outputs as class properties
        this.bucketName = bucket.id;
        this.websiteUrl = website.websiteEndpoint;

        // 4. Register outputs -- always the last line
        this.registerOutputs({
            bucketName: this.bucketName,
            websiteUrl: this.websiteUrl,
        });
    }
}

// Usage
const site = new StaticSite("marketing", {
    indexDocument: "index.html",
});
export const url = site.websiteUrl;
```

### Python

```python
import pulumi
import pulumi_aws as aws

class StaticSiteArgs:
    def __init__(self,
                 index_document: pulumi.Input[str] = "index.html",
                 error_document: pulumi.Input[str] = "error.html"):
        self.index_document = index_document
        self.error_document = error_document

class StaticSite(pulumi.ComponentResource):
    bucket_name: pulumi.Output[str]
    website_url: pulumi.Output[str]

    def __init__(self, name: str, args: StaticSiteArgs,
                 opts: pulumi.ResourceOptions = None):
        super().__init__("myorg:index:StaticSite", name, None, opts)

        bucket = aws.s3.Bucket(f"{name}-bucket",
            opts=pulumi.ResourceOptions(parent=self))

        website = aws.s3.BucketWebsiteConfigurationV2(f"{name}-website",
            bucket=bucket.id,
            index_document=aws.s3.BucketWebsiteConfigurationV2IndexDocumentArgs(
                suffix=args.index_document,
            ),
            error_document=aws.s3.BucketWebsiteConfigurationV2ErrorDocumentArgs(
                key=args.error_document,
            ),
            opts=pulumi.ResourceOptions(parent=self))

        self.bucket_name = bucket.id
        self.website_url = website.website_endpoint

        self.register_outputs({
            "bucket_name": self.bucket_name,
            "website_url": self.website_url,
        })

site = StaticSite("marketing", StaticSiteArgs())
pulumi.export("url", site.website_url)
```

### Type URN Format

The first argument to `super()` is the type URN: `<package>:<module>:<type>`.

| Segment | Convention | Example |
|---------|-----------|---------|
| package | Organization or package name | `myorg`, `acme`, `pkg` |
| module  | Usually `index` | `index` |
| type    | PascalCase class name | `StaticSite`, `VpcNetwork` |

Full examples: `myorg:index:StaticSite`, `acme:index:KubernetesCluster`

### registerOutputs Is Required

**Why**: Without `registerOutputs()`, the component appears stuck in a "creating" state in the Pulumi console and outputs are not persisted to state.

**Wrong**:

```typescript
class MyComponent extends pulumi.ComponentResource {
    public readonly url: pulumi.Output<string>;

    constructor(name: string, args: MyArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:MyComponent", name, {}, opts);
        const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
        this.url = bucket.bucketRegionalDomainName;
        // Missing registerOutputs -- component stuck "creating"
    }
}
```

**Right**:

```typescript
class MyComponent extends pulumi.ComponentResource {
    public readonly url: pulumi.Output<string>;

    constructor(name: string, args: MyArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:MyComponent", name, {}, opts);
        const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
        this.url = bucket.bucketRegionalDomainName;

        this.registerOutputs({ url: this.url });
    }
}
```

### Derive Child Names from the Component Name

**Why**: Hardcoded child names cause collisions when the component is instantiated multiple times.

**Wrong**:

```typescript
// Collides if two instances of this component exist
const bucket = new aws.s3.Bucket("my-bucket", {}, { parent: this });
```

**Right**:

```typescript
// Unique per component instance
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
```

---

## Designing the Args Interface

The args interface is the most impactful design decision. It defines what consumers can configure and how composable the component is.

### Wrap Properties in Input<T>

**Why**: `Input<T>` accepts both plain values and `Output<T>` from other resources. Without it, consumers must unwrap outputs manually with `.apply()`.

**Wrong**:

```typescript
interface WebServiceArgs {
    port: number;            // Forces consumers to unwrap Outputs
    vpcId: string;           // Cannot accept vpc.id directly
}
```

**Right**:

```typescript
interface WebServiceArgs {
    port: pulumi.Input<number>;     // Accepts 8080 or someOutput
    vpcId: pulumi.Input<string>;    // Accepts "vpc-123" or vpc.id
}
```

### Keep Structures Flat

Avoid deeply nested arg objects. Flat interfaces are easier to use and evolve.

```typescript
// Prefer flat
interface DatabaseArgs {
    instanceClass: pulumi.Input<string>;
    storageGb: pulumi.Input<number>;
    enableBackups?: pulumi.Input<boolean>;
    backupRetentionDays?: pulumi.Input<number>;
}

// Avoid deep nesting
interface DatabaseArgs {
    instance: {
        compute: { class: pulumi.Input<string> };
        storage: { sizeGb: pulumi.Input<number> };
    };
    backup: {
        config: { enabled: pulumi.Input<boolean>; retention: pulumi.Input<number> };
    };
}
```

### No Union Types

Union types break multi-language SDK generation. Python, Go, and C# cannot represent `string | number`.

**Wrong**:

```typescript
interface MyArgs {
    port: pulumi.Input<string | number>;  // Fails in Python, Go, C#
}
```

**Right**:

```typescript
interface MyArgs {
    port: pulumi.Input<number>;  // Single type, works everywhere
}
```

If you need to accept multiple forms, use separa
Files: 3
Size: 27.9 KB
Complexity: 39/100
Category: Design

Related in Design