pulumi-component

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Authoring Pulumi Components

Pulumi 组件编写指南

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.
ComponentResource 可将相关基础设施资源分组为一个可复用的逻辑单元。组件让基础设施更易于理解、复用和维护。在
pulumi preview
/
pulumi up
输出以及 Pulumi Cloud 控制台中,组件会显示为一个单独的节点,其下嵌套子资源。
本指南涵盖组件编写的完整生命周期。如需了解通用 Pulumi 编码模式(Output 处理、密钥、别名、预览工作流),请使用
pulumi-best-practices
技能。

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)
在以下场景中使用本指南:
  • 创建新的 ComponentResource 类
  • 设计组件的参数接口
  • 让组件支持多 Pulumi 语言调用
  • 发布或分发组件包
  • 将内联资源重构为可复用组件
  • 调试组件行为(缺失输出、创建停滞、子资源层级错误)

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
每个组件都包含四个必需元素:
  1. 继承 ComponentResource 并使用类型 URN 调用
    super()
  2. 接受标准参数:名称、参数和
    ComponentResourceOptions
  3. 为所有子资源设置
    parent: this
  4. 在构造函数末尾调用
    registerOutputs()

TypeScript

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;
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. 使用类型URN调用super():<package>:<module>:<type>
        super("myorg:index:StaticSite", name, {}, opts);

        // 2. 创建子资源并设置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. 将输出暴露为类属性
        this.bucketName = bucket.id;
        this.websiteUrl = website.websiteEndpoint;

        // 4. 注册输出 -- 始终放在最后一行
        this.registerOutputs({
            bucketName: this.bucketName,
            websiteUrl: this.websiteUrl,
        });
    }
}

// 使用示例
const site = new StaticSite("marketing", {
    indexDocument: "index.html",
});
export const url = site.websiteUrl;

Python

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)
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

类型URN格式

The first argument to
super()
is the type URN:
<package>:<module>:<type>
.
SegmentConventionExample
packageOrganization or package name
myorg
,
acme
,
pkg
moduleUsually
index
index
typePascalCase class name
StaticSite
,
VpcNetwork
Full examples:
myorg:index:StaticSite
,
acme:index:KubernetesCluster
super()
的第一个参数是类型URN:
<package>:<module>:<type>
分段约定示例
package组织或包名称
myorg
,
acme
,
pkg
module通常为
index
index
type大驼峰式类名
StaticSite
,
VpcNetwork
完整示例:
myorg:index:StaticSite
,
acme:index:KubernetesCluster

registerOutputs Is Required

registerOutputs 是必需的

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 });
    }
}
原因:如果没有
registerOutputs()
,组件在 Pulumi 控制台中会一直处于“创建中”状态,且输出不会持久化到状态中。
错误示例
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;
        // 缺少registerOutputs -- 组件会一直处于"创建中"
    }
}
正确示例
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 });

原因:硬编码子资源名称会在组件多次实例化时导致冲突。
错误示例
typescript
// 如果存在两个该组件实例,会发生冲突
const bucket = new aws.s3.Bucket("my-bucket", {}, { parent: this });
正确示例
typescript
// 每个组件实例的名称唯一
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>

使用 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
}
原因
Input<T>
既接受普通值,也接受来自其他资源的
Output<T>
。如果不使用它,使用者必须手动通过
.apply()
解析输出。
错误示例
typescript
interface WebServiceArgs {
    port: number;            // 强制使用者解析Output
    vpcId: string;           // 无法直接接受vpc.id
}
正确示例
typescript
interface WebServiceArgs {
    port: pulumi.Input<number>;     // 接受8080或某个Output
    vpcId: pulumi.Input<string>;    // 接受"vpc-123"或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> };
    };
}
避免深度嵌套的参数对象。扁平化接口更易于使用和扩展。
typescript
// 推荐扁平化
interface DatabaseArgs {
    instanceClass: pulumi.Input<string>;
    storageGb: pulumi.Input<number>;
    enableBackups?: pulumi.Input<boolean>;
    backupRetentionDays?: pulumi.Input<number>;
}

// 避免深度嵌套
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 separate optional properties:
typescript
interface StorageArgs {
    sizeGb?: pulumi.Input<number>;      // Specify size in GB
    sizeMb?: pulumi.Input<number>;      // Or specify size in MB
}
联合类型会破坏多语言SDK生成。Python、Go和C#无法表示
string | number
错误示例
typescript
interface MyArgs {
    port: pulumi.Input<string | number>;  // 在Python、Go、C#中会失败
}
正确示例
typescript
interface MyArgs {
    port: pulumi.Input<number>;  // 单一类型,适用于所有语言
}
如果需要接受多种形式,请使用单独的可选属性:
typescript
interface StorageArgs {
    sizeGb?: pulumi.Input<number>;      // 以GB为单位指定大小
    sizeMb?: pulumi.Input<number>;      // 或以MB为单位指定大小
}

No Functions or Callbacks

不要使用函数或回调

Functions cannot be serialized across language boundaries.
Wrong:
typescript
interface MyArgs {
    nameTransform: (name: string) => string;  // Cannot serialize
}
Right:
typescript
interface MyArgs {
    namePrefix?: pulumi.Input<string>;   // Configuration instead of callback
    nameSuffix?: pulumi.Input<string>;
}
函数无法跨语言边界序列化。
错误示例
typescript
interface MyArgs {
    nameTransform: (name: string) => string;  // 无法序列化
}
正确示例
typescript
interface MyArgs {
    namePrefix?: pulumi.Input<string>;   // 使用配置而非回调
    nameSuffix?: pulumi.Input<string>;
}

Use Defaults for Optional Properties

为可选属性设置默认值

Set sensible defaults inside the constructor so consumers only configure what they need:
typescript
interface SecureBucketArgs {
    enableVersioning?: pulumi.Input<boolean>;   // Defaults to true
    enableEncryption?: pulumi.Input<boolean>;   // Defaults to true
    blockPublicAccess?: pulumi.Input<boolean>;  // Defaults to true
}

class SecureBucket extends pulumi.ComponentResource {
    constructor(name: string, args: SecureBucketArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:SecureBucket", name, {}, opts);

        const enableVersioning = args.enableVersioning ?? true;
        const enableEncryption = args.enableEncryption ?? true;
        const blockPublicAccess = args.blockPublicAccess ?? true;

        // Apply defaults...
    }
}

// Consumer only overrides what they need
const bucket = new SecureBucket("data", { enableVersioning: false });

在构造函数中设置合理的默认值,这样使用者只需配置他们需要的内容:
typescript
interface SecureBucketArgs {
    enableVersioning?: pulumi.Input<boolean>;   // 默认值为true
    enableEncryption?: pulumi.Input<boolean>;   // 默认值为true
    blockPublicAccess?: pulumi.Input<boolean>;  // 默认值为true
}

class SecureBucket extends pulumi.ComponentResource {
    constructor(name: string, args: SecureBucketArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:SecureBucket", name, {}, opts);

        const enableVersioning = args.enableVersioning ?? true;
        const enableEncryption = args.enableEncryption ?? true;
        const blockPublicAccess = args.blockPublicAccess ?? true;

        // 应用默认值...
    }
}

// 使用者只需覆盖需要修改的配置
const bucket = new SecureBucket("data", { enableVersioning: false });

Exposing Outputs

暴露输出

Expose Only What Consumers Need

仅暴露使用者需要的内容

Components often create many internal resources. Expose only the values consumers need, not every internal resource.
Wrong:
typescript
class Database extends pulumi.ComponentResource {
    // Exposes everything -- consumers see implementation details
    public readonly cluster: aws.rds.Cluster;
    public readonly primaryInstance: aws.rds.ClusterInstance;
    public readonly replicaInstance: aws.rds.ClusterInstance;
    public readonly subnetGroup: aws.rds.SubnetGroup;
    public readonly securityGroup: aws.ec2.SecurityGroup;
    public readonly parameterGroup: aws.rds.ClusterParameterGroup;
    // ...
}
Right:
typescript
class Database extends pulumi.ComponentResource {
    // Exposes only what consumers need
    public readonly endpoint: pulumi.Output<string>;
    public readonly port: pulumi.Output<number>;
    public readonly securityGroupId: pulumi.Output<string>;

    constructor(name: string, args: DatabaseArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:Database", name, {}, opts);

        const sg = new aws.ec2.SecurityGroup(`${name}-sg`, { /* ... */ }, { parent: this });
        const cluster = new aws.rds.Cluster(`${name}-cluster`, { /* ... */ }, { parent: this });

        this.endpoint = cluster.endpoint;
        this.port = cluster.port;
        this.securityGroupId = sg.id;

        this.registerOutputs({
            endpoint: this.endpoint,
            port: this.port,
            securityGroupId: this.securityGroupId,
        });
    }
}
组件通常会创建许多内部资源。仅暴露使用者需要的值,而非所有内部资源。
错误示例
typescript
class Database extends pulumi.ComponentResource {
    // 暴露所有内容 -- 使用者会看到实现细节
    public readonly cluster: aws.rds.Cluster;
    public readonly primaryInstance: aws.rds.ClusterInstance;
    public readonly replicaInstance: aws.rds.ClusterInstance;
    public readonly subnetGroup: aws.rds.SubnetGroup;
    public readonly securityGroup: aws.ec2.SecurityGroup;
    public readonly parameterGroup: aws.rds.ClusterParameterGroup;
    // ...
}
正确示例
typescript
class Database extends pulumi.ComponentResource {
    // 仅暴露使用者需要的内容
    public readonly endpoint: pulumi.Output<string>;
    public readonly port: pulumi.Output<number>;
    public readonly securityGroupId: pulumi.Output<string>;

    constructor(name: string, args: DatabaseArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:Database", name, {}, opts);

        const sg = new aws.ec2.SecurityGroup(`${name}-sg`, { /* ... */ }, { parent: this });
        const cluster = new aws.rds.Cluster(`${name}-cluster`, { /* ... */ }, { parent: this });

        this.endpoint = cluster.endpoint;
        this.port = cluster.port;
        this.securityGroupId = sg.id;

        this.registerOutputs({
            endpoint: this.endpoint,
            port: this.port,
            securityGroupId: this.securityGroupId,
        });
    }
}

Derive Composite Outputs

派生复合输出

Use
pulumi.interpolate
or
pulumi.concat
to build derived values:
typescript
this.connectionString = pulumi.interpolate`postgresql://${args.username}:${args.password}@${cluster.endpoint}:${cluster.port}/${args.databaseName}`;

this.registerOutputs({ connectionString: this.connectionString });

使用
pulumi.interpolate
pulumi.concat
构建派生值:
typescript
this.connectionString = pulumi.interpolate`postgresql://${args.username}:${args.password}@${cluster.endpoint}:${cluster.port}/${args.databaseName}`;

this.registerOutputs({ connectionString: this.connectionString });

Component Design Patterns

组件设计模式

Sensible Defaults with Override

合理默认值与可覆盖

Encode best practices as defaults. Allow consumers to override when they have specific requirements.
typescript
interface SecureBucketArgs {
    enableVersioning?: pulumi.Input<boolean>;
    enableEncryption?: pulumi.Input<boolean>;
    blockPublicAccess?: pulumi.Input<boolean>;
    tags?: pulumi.Input<Record<string, pulumi.Input<string>>>;
}

class SecureBucket extends pulumi.ComponentResource {
    public readonly bucketId: pulumi.Output<string>;
    public readonly arn: pulumi.Output<string>;

    constructor(name: string, args: SecureBucketArgs = {}, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:SecureBucket", name, {}, opts);

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

        // Versioning on by default
        if (args.enableVersioning !== false) {
            new aws.s3.BucketVersioningV2(`${name}-versioning`, {
                bucket: bucket.id,
                versioningConfiguration: { status: "Enabled" },
            }, { parent: this });
        }

        // Encryption on by default
        if (args.enableEncryption !== false) {
            new aws.s3.BucketServerSideEncryptionConfigurationV2(`${name}-encryption`, {
                bucket: bucket.id,
                rules: [{ applyServerSideEncryptionByDefault: { sseAlgorithm: "AES256" } }],
            }, { parent: this });
        }

        // Public access blocked by default
        if (args.blockPublicAccess !== false) {
            new aws.s3.BucketPublicAccessBlock(`${name}-public-access`, {
                bucket: bucket.id,
                blockPublicAcls: true,
                blockPublicPolicy: true,
                ignorePublicAcls: true,
                restrictPublicBuckets: true,
            }, { parent: this });
        }

        this.bucketId = bucket.id;
        this.arn = bucket.arn;
        this.registerOutputs({ bucketId: this.bucketId, arn: this.arn });
    }
}
将最佳实践编码为默认值。当使用者有特定需求时,允许他们覆盖默认值。
typescript
interface SecureBucketArgs {
    enableVersioning?: pulumi.Input<boolean>;
    enableEncryption?: pulumi.Input<boolean>;
    blockPublicAccess?: pulumi.Input<boolean>;
    tags?: pulumi.Input<Record<string, pulumi.Input<string>>>;
}

class SecureBucket extends pulumi.ComponentResource {
    public readonly bucketId: pulumi.Output<string>;
    public readonly arn: pulumi.Output<string>;

    constructor(name: string, args: SecureBucketArgs = {}, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:SecureBucket", name, {}, opts);

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

        // 版本控制默认开启
        if (args.enableVersioning !== false) {
            new aws.s3.BucketVersioningV2(`${name}-versioning`, {
                bucket: bucket.id,
                versioningConfiguration: { status: "Enabled" },
            }, { parent: this });
        }

        // 加密默认开启
        if (args.enableEncryption !== false) {
            new aws.s3.BucketServerSideEncryptionConfigurationV2(`${name}-encryption`, {
                bucket: bucket.id,
                rules: [{ applyServerSideEncryptionByDefault: { sseAlgorithm: "AES256" } }],
            }, { parent: this });
        }

        // 公共访问默认阻止
        if (args.blockPublicAccess !== false) {
            new aws.s3.BucketPublicAccessBlock(`${name}-public-access`, {
                bucket: bucket.id,
                blockPublicAcls: true,
                blockPublicPolicy: true,
                ignorePublicAcls: true,
                restrictPublicBuckets: true,
            }, { parent: this });
        }

        this.bucketId = bucket.id;
        this.arn = bucket.arn;
        this.registerOutputs({ bucketId: this.bucketId, arn: this.arn });
    }
}

Conditional Resource Creation

条件资源创建

Use optional args to gate creation of sub-resources:
typescript
interface WebServiceArgs {
    image: pulumi.Input<string>;
    port: pulumi.Input<number>;
    enableMonitoring?: pulumi.Input<boolean>;
    alarmEmail?: pulumi.Input<string>;
}

class WebService extends pulumi.ComponentResource {
    constructor(name: string, args: WebServiceArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:WebService", name, {}, opts);

        const service = new aws.ecs.Service(`${name}-service`, {
            // ...service config...
        }, { parent: this });

        // Only create alarm infrastructure when monitoring is enabled
        if (args.enableMonitoring) {
            const topic = new aws.sns.Topic(`${name}-alerts`, {}, { parent: this });

            if (args.alarmEmail) {
                new aws.sns.TopicSubscription(`${name}-alert-email`, {
                    topic: topic.arn,
                    protocol: "email",
                    endpoint: args.alarmEmail,
                }, { parent: this });
            }

            new aws.cloudwatch.MetricAlarm(`${name}-cpu-alarm`, {
                // ...alarm config referencing service...
                alarmActions: [topic.arn],
            }, { parent: this });
        }

        this.registerOutputs({});
    }
}
使用可选参数控制子资源的创建:
typescript
interface WebServiceArgs {
    image: pulumi.Input<string>;
    port: pulumi.Input<number>;
    enableMonitoring?: pulumi.Input<boolean>;
    alarmEmail?: pulumi.Input<string>;
}

class WebService extends pulumi.ComponentResource {
    constructor(name: string, args: WebServiceArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:WebService", name, {}, opts);

        const service = new aws.ecs.Service(`${name}-service`, {
            // ...服务配置...
        }, { parent: this });

        // 仅在启用监控时创建告警基础设施
        if (args.enableMonitoring) {
            const topic = new aws.sns.Topic(`${name}-alerts`, {}, { parent: this });

            if (args.alarmEmail) {
                new aws.sns.TopicSubscription(`${name}-alert-email`, {
                    topic: topic.arn,
                    protocol: "email",
                    endpoint: args.alarmEmail,
                }, { parent: this });
            }

            new aws.cloudwatch.MetricAlarm(`${name}-cpu-alarm`, {
                // ...引用服务的告警配置...
                alarmActions: [topic.arn],
            }, { parent: this });
        }

        this.registerOutputs({});
    }
}

Composition

组合模式

Build higher-level components from lower-level ones. Each level manages a single concern.
typescript
// Lower-level component
class VpcNetwork 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: VpcNetworkArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:VpcNetwork", name, {}, opts);
        // ...create VPC, subnets, route tables...
        this.registerOutputs({ vpcId: this.vpcId });
    }
}

// Higher-level component that uses VpcNetwork
class Platform extends pulumi.ComponentResource {
    public readonly kubeconfig: pulumi.Output<string>;

    constructor(name: string, args: PlatformArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:Platform", name, {}, opts);

        // Compose lower-level components
        const network = new VpcNetwork(`${name}-network`, {
            cidrBlock: args.cidrBlock,
        }, { parent: this });

        const cluster = new aws.eks.Cluster(`${name}-cluster`, {
            vpcConfig: {
                subnetIds: network.privateSubnetIds,
            },
        }, { parent: this });

        this.kubeconfig = cluster.kubeconfig;
        this.registerOutputs({ kubeconfig: this.kubeconfig });
    }
}
从底层组件构建高层组件。每个层级只处理单一关注点。
typescript
// 底层组件
class VpcNetwork 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: VpcNetworkArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:VpcNetwork", name, {}, opts);
        // ...创建VPC、子网、路由表...
        this.registerOutputs({ vpcId: this.vpcId });
    }
}

// 使用VpcNetwork的高层组件
class Platform extends pulumi.ComponentResource {
    public readonly kubeconfig: pulumi.Output<string>;

    constructor(name: string, args: PlatformArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:Platform", name, {}, opts);

        // 组合底层组件
        const network = new VpcNetwork(`${name}-network`, {
            cidrBlock: args.cidrBlock,
        }, { parent: this });

        const cluster = new aws.eks.Cluster(`${name}-cluster`, {
            vpcConfig: {
                subnetIds: network.privateSubnetIds,
            },
        }, { parent: this });

        this.kubeconfig = cluster.kubeconfig;
        this.registerOutputs({ kubeconfig: this.kubeconfig });
    }
}

Provider Passthrough

提供者传递

Accept explicit providers for multi-region or multi-account deployments.
ComponentResourceOptions
carries provider configuration to children automatically:
typescript
// Consumer passes a provider for a different region
const usWest = new aws.Provider("us-west", { region: "us-west-2" });
const site = new StaticSite("west-site", { indexDocument: "index.html" }, {
    providers: [usWest],
});
Children with
{ parent: this }
automatically inherit the provider. No extra code is needed inside the component.

为多区域或多账户部署显式接受提供者。
ComponentResourceOptions
会自动将提供者配置传递给子资源:
typescript
// 使用者为不同区域传递提供者
const usWest = new aws.Provider("us-west", { region: "us-west-2" });
const site = new StaticSite("west-site", { indexDocument: "index.html" }, {
    providers: [usWest],
});
设置了
{ parent: this }
的子资源会自动继承提供者。组件内部无需额外代码。

Multi-Language Components

多语言组件

If your component will be consumed from multiple Pulumi languages (TypeScript, Python, Go, C#, Java, YAML), package it as a multi-language component.
如果你的组件需要支持多种 Pulumi 语言(TypeScript、Python、Go、C#、Java、YAML),请将其打包为多语言组件。

Do You Need Multi-Language?

是否需要多语言支持?

Ask: "Will anyone consume this component from a different language than it was authored in?"
Single-language component (no packaging needed):
  • Your team uses one language and the component stays within that codebase
  • The component is internal to a single project or monorepo
  • No
    PulumiPlugin.yaml
    needed -- just import the class directly
Multi-language component (packaging required):
  • Other teams consume your component in different languages
  • Platform teams building abstractions for developers who choose their own language
  • YAML consumers need access -- even if you author in TypeScript, YAML programs require multi-language packaging to use your component
  • Building a shared component library for your organization
  • Publishing to the Pulumi private registry or public registry is a common reason, but not required for multi-language support
Common mistake: A TypeScript platform team builds components only their TypeScript users can consume. If application developers use Python or YAML, those components are invisible to them without multi-language packaging.
请自问:“是否有人会用与编写语言不同的语言调用这个组件?”
单语言组件(无需打包):
  • 你的团队只使用一种语言,且组件仅在该代码库内使用
  • 组件是单个项目或单体仓库的内部组件
  • 无需
    PulumiPlugin.yaml
    -- 直接导入类即可
多语言组件(需要打包):
  • 其他团队使用不同语言调用你的组件
  • 平台团队为选择不同语言的开发者构建抽象层
  • YAML 使用者需要访问组件 -- 即使你用 TypeScript 编写,YAML 程序也需要多语言打包才能使用你的组件
  • 为你的组织构建共享组件库
  • 发布到 Pulumi 私有注册表或公共注册表是常见需求,但并非多语言支持的必需条件
常见错误:TypeScript 平台团队构建的组件仅能被 TypeScript 用户使用。如果应用开发者使用 Python 或 YAML,这些组件在没有多语言打包的情况下对他们不可见。

Setup

配置

Create a
PulumiPlugin.yaml
in the component directory to declare the runtime:
yaml
runtime: nodejs
Or for Python:
yaml
runtime: python
在组件目录中创建
PulumiPlugin.yaml
以声明运行时:
yaml
runtime: nodejs
对于 Python:
yaml
runtime: python

Serialization Constraints

序列化约束

For multi-language compatibility, args must be serializable. These constraints apply regardless of the authoring language:
AllowedNot Allowed
string
,
number
,
boolean
Union types (
string | number
)
Input<T>
wrappers
Functions and callbacks
Arrays and maps of primitivesComplex nested generics
EnumsPlatform-specific types
为了兼容多语言,参数必须可序列化。无论使用哪种编写语言,以下约束均适用:
允许不允许
string
,
number
,
boolean
联合类型 (
string | number
)
Input<T>
包装器
函数和回调
基本类型的数组和映射复杂嵌套泛型
枚举平台特定类型

Consuming Multi-Language Components

使用多语言组件

Consumers install the component with
pulumi package add
, which automatically downloads the provider plugin, generates a local SDK in the consumer's language, and updates
Pulumi.yaml
:
bash
undefined
使用者使用
pulumi package add
安装组件,该命令会自动下载提供者插件、在使用者的语言中生成本地 SDK 并更新
Pulumi.yaml
bash
undefined

From a Git repository

从Git仓库安装

pulumi package add <git-repo-url>
pulumi package add <git-repo-url>

From a specific version tag

从特定版本标签安装

pulumi package add <git-repo-url>@v1.0.0

For fresh checkouts or CI environments, run `pulumi install` to ensure all package dependencies are available. The consumer does not need to manually generate SDKs.

Authors who publish SDKs to package managers (npm, PyPI, etc.) can optionally use `pulumi package gen-sdk` to generate language-specific SDKs for publishing. Most component authors do not need this -- `pulumi package add` handles SDK generation on the consumer side.
pulumi package add <git-repo-url>@v1.0.0

对于新检出的代码或 CI 环境,运行 `pulumi install` 以确保所有包依赖可用。使用者无需手动生成 SDK。

将 SDK 发布到包管理器(npm、PyPI 等)的作者可以选择使用 `pulumi package gen-sdk` 生成特定语言的 SDK 进行发布。大多数组件作者不需要此操作 -- `pulumi package add` 会在使用者端处理 SDK 生成。

Entry Points

入口点

Published multi-language components require an entry point that hosts the component provider process. The entry point pattern differs by language.
TypeScript (
runtime: nodejs
):
Export component classes from
index.ts
. No separate entry point file is needed. Pulumi introspects exported classes automatically.
typescript
// index.ts -- exports are the entry point
export { StaticSite, StaticSiteArgs } from "./staticSite";
export { SecureBucket, SecureBucketArgs } from "./secureBucket";
Python (
runtime: python
):
Create a
__main__.py
that calls
component_provider_host
with all component classes:
python
from pulumi.provider.experimental import component_provider_host
from static_site import StaticSite
from secure_bucket import SecureBucket

if __name__ == "__main__":
    component_provider_host(
        name="my-components",
        components=[StaticSite, SecureBucket],
    )
Go (
runtime: go
):
Create a
main.go
that builds and runs the provider:
go
package main

import (
    "context"
    "fmt"
    "os"

    "github.com/pulumi/pulumi-go-provider/infer"
)

func main() {
    p, err := infer.NewProviderBuilder().
        WithComponents(
            infer.ComponentF(NewStaticSite),
            infer.ComponentF(NewSecureBucket),
        ).
        Build()
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    if err := p.Run(context.Background(), "my-components", "0.1.0"); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
C# (
runtime: dotnet
):
Create a
Program.cs
that serves the component provider host:
csharp
using System.Threading.Tasks;

class Program
{
    public static Task Main(string[] args) =>
        Pulumi.Experimental.Provider.ComponentProviderHost.Serve(args);
}
For a complete working example across all languages, see https://github.com/mikhailshilkov/comp-as-comp.

已发布的多语言组件需要一个入口点来托管组件提供者进程。不同语言的入口点模式不同。
TypeScript
runtime: nodejs
):
index.ts
导出组件类。无需单独的入口点文件。Pulumi 会自动 introspect 导出的类。
typescript
// index.ts -- 导出内容即为入口点
export { StaticSite, StaticSiteArgs } from "./staticSite";
export { SecureBucket, SecureBucketArgs } from "./secureBucket";
Python
runtime: python
):
创建
__main__.py
,调用
component_provider_host
并传入所有组件类:
python
from pulumi.provider.experimental import component_provider_host
from static_site import StaticSite
from secure_bucket import SecureBucket

if __name__ == "__main__":
    component_provider_host(
        name="my-components",
        components=[StaticSite, SecureBucket],
    )
Go
runtime: go
):
创建
main.go
以构建并运行提供者:
go
package main

import (
    "context"
    "fmt"
    "os"

    "github.com/pulumi/pulumi-go-provider/infer"
)

func main() {
    p, err := infer.NewProviderBuilder().
        WithComponents(
            infer.ComponentF(NewStaticSite),
            infer.ComponentF(NewSecureBucket),
        ).
        Build()
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    if err := p.Run(context.Background(), "my-components", "0.1.0"); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
C#
runtime: dotnet
):
创建
Program.cs
以启动组件提供者主机:
csharp
using System.Threading.Tasks;

class Program
{
    public static Task Main(string[] args) =>
        Pulumi.Experimental.Provider.ComponentProviderHost.Serve(args);
}
如需查看所有语言的完整工作示例,请访问 https://github.com/mikhailshilkov/comp-as-comp。

Distribution

分发

Choose a distribution method based on your audience:
AudienceMethodHow
Same projectDirect importStandard language import
Same organizationPrivate registry
pulumi package publish
to Pulumi Cloud
Same organizationGit repository
pulumi package add <repo>
with version tags
Language ecosystemPackage managerPublish to npm, PyPI, NuGet, or Maven
Public communityPulumi RegistrySubmit via pulumi/registry GitHub repo
根据受众选择分发方式:
受众方式操作
同一项目直接导入标准语言导入
同一组织私有注册表使用
pulumi package publish
发布到 Pulumi Cloud
同一组织Git仓库使用带版本标签的
pulumi package add <repo>
语言生态系统包管理器发布到 npm、PyPI、NuGet 或 Maven
公共社区Pulumi 注册表通过 pulumi/registry GitHub 仓库提交

Pulumi Private Registry

Pulumi 私有注册表

The private registry is the centralized catalog for your organization's components. It provides automatic API documentation, version management, and discoverability for all teams.
Publish a component to the private registry:
bash
pulumi package publish https://github.com/myorg/my-component --publisher myorg
Version components using git tags with a
v
prefix:
bash
git tag v1.0.0
git push origin v1.0.0
A README file is required when publishing. Pulumi uses it as the component's documentation page in the registry.
Automate publishing from GitHub Actions using OIDC authentication:
yaml
name: Publish Component
on:
  push:
    tags:
      - "v*"

permissions:
  id-token: write
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    env:
      PULUMI_ORG: myorg
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: pulumi/auth-actions@v1
        with:
          organization: ${{ env.PULUMI_ORG }}
          requested-token-type: urn:pulumi:token-type:access_token:organization
      - run: pulumi package publish https://github.com/${{ github.repository }} --publisher ${{ env.PULUMI_ORG }}
Prerequisites: Configure GitHub OIDC integration with Pulumi Cloud before using this workflow.
The registry supports private GitHub and GitLab repositories. For non-OIDC setups, authenticate with
GITHUB_TOKEN
or
GITLAB_TOKEN
environment variables.
The private registry automatically generates SDK documentation for each published component. Enrich the generated docs by adding type annotations to your component's inputs and outputs (JSDoc in TypeScript, docstrings in Python,
Annotate()
methods in Go).
私有注册表是组织组件的集中目录。它为所有团队提供自动 API 文档、版本管理和可发现性。
将组件发布到私有注册表:
bash
pulumi package publish https://github.com/myorg/my-component --publisher myorg
使用带
v
前缀的 Git 标签为组件版本化:
bash
git tag v1.0.0
git push origin v1.0.0
发布时需要 README 文件。Pulumi 会将其用作注册表中组件的文档页面。
使用 OIDC 认证通过 GitHub Actions 自动化发布:
yaml
name: Publish Component
on:
  push:
    tags:
      - "v*"

permissions:
  id-token: write
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    env:
      PULUMI_ORG: myorg
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: pulumi/auth-actions@v1
        with:
          organization: ${{ env.PULUMI_ORG }}
          requested-token-type: urn:pulumi:token-type:access_token:organization
      - run: pulumi package publish https://github.com/${{ github.repository }} --publisher ${{ env.PULUMI_ORG }}
前提条件:在使用此工作流之前,配置 GitHub OIDC 与 Pulumi Cloud 的集成。
注册表支持私有 GitHub 和 GitLab 仓库。对于非 OIDC 配置,使用
GITHUB_TOKEN
GITLAB_TOKEN
环境变量进行认证。
私有注册表会自动为每个已发布的组件生成 SDK 文档。通过为组件的输入和输出添加类型注解(TypeScript 中的 JSDoc、Python 中的文档字符串、Go 中的
Annotate()
方法)来丰富生成的文档。

Git Repository Distribution

Git 仓库分发

Tag releases for consumers to pin versions:
bash
git tag v1.0.0
git push origin v1.0.0
Consumers install with:
bash
pulumi package add https://github.com/myorg/my-component@v1.0.0
为发布版本打标签,以便使用者固定版本:
bash
git tag v1.0.0
git push origin v1.0.0
使用者使用以下命令安装:
bash
pulumi package add https://github.com/myorg/my-component@v1.0.0

Package Manager Distribution

包管理器分发

Publish language-specific packages for native dependency management:
  • npm:
    npm publish
    for TypeScript/JavaScript
  • PyPI:
    twine upload
    for Python
  • NuGet:
    dotnet nuget push
    for .NET
  • Maven Central: Standard Maven publishing for Java

发布特定语言的包以支持原生依赖管理:
  • npm: 为 TypeScript/JavaScript 使用
    npm publish
  • PyPI: 为 Python 使用
    twine upload
  • NuGet: 为 .NET 使用
    dotnet nuget push
  • Maven Central: 为 Java 使用标准 Maven 发布流程

Anti-Patterns

反模式

Anti-PatternProblemFix
Resources inside
apply()
Not visible in
pulumi preview
Move resource creation outside apply (see
pulumi-best-practices
practice 1)
Missing
registerOutputs()
Component stuck "creating"Always call as last line of constructor
Missing
parent: this
Children appear at root levelPass
{ parent: this }
to all child resources
Union types in argsBreaks Python, Go, C# SDKsUse single types; separate properties for variants
Functions in argsCannot serialize across languagesUse configuration properties instead
Hardcoded child namesCollisions with multiple instancesDerive names from
${name}-suffix
Over-exposed outputsLeaks implementation detailsExport only what consumers need
Single-use componentUnnecessary abstraction overheadUse inline resources until a pattern repeats
Deeply nested argsHard to use and evolveKeep interfaces flat with optional properties

反模式问题修复方案
apply()
内创建资源
pulumi preview
中不可见
将资源创建移到 apply 外部(请参阅
pulumi-best-practices
实践 1)
缺失
registerOutputs()
组件一直处于“创建中”始终在构造函数最后一行调用
缺失
parent: this
子资源显示在根层级为所有子资源传递
{ parent: this }
参数使用联合类型破坏 Python、Go、C# SDK使用单一类型;为变体使用单独属性
参数使用函数无法跨语言序列化使用配置属性替代
子资源使用硬编码名称多实例时发生冲突
${name}-suffix
派生名称
过度暴露输出泄露实现细节仅导出使用者需要的内容
一次性组件不必要的抽象开销直到模式重复出现再使用内联资源
参数深度嵌套难以使用和扩展保持接口扁平化并使用可选属性

Quick Reference

快速参考

TopicKey Point
Type URN
<package>:<module>:<type>
, module usually
index
Constructor
super(type, name, {}, opts)
then children then
registerOutputs()
Child resourcesAlways
{ parent: this }
, derive name from
${name}-suffix
Args interfaceWrap in
Input<T>
, no unions, no functions, flat structure
OutputsPublic readonly
Output<T>
properties, expose only essentials
DefaultsUse
??
operator to apply sensible defaults in constructor
CompositionLower-level components composed into higher-level ones
Multi-language
PulumiPlugin.yaml
+ entry point; consumers use
pulumi package add
DistributionPrivate registry, git tags, package managers, or public Pulumi Registry
主题关键点
类型URN
<package>:<module>:<type>
,module通常为
index
构造函数
super(type, name, {}, opts)
然后创建子资源,最后调用
registerOutputs()
子资源始终设置
{ parent: this }
,从
${name}-suffix
派生名称
参数接口使用
Input<T>
包装,不使用联合类型,不使用函数,保持扁平化结构
输出公共只读
Output<T>
属性,仅暴露必要内容
默认值使用
??
运算符在构造函数中应用合理默认值
组合底层组件组合成高层组件
多语言
PulumiPlugin.yaml
+ 入口点;使用者使用
pulumi package add
分发私有注册表、Git标签、包管理器或公共Pulumi注册表

Related Skills

相关技能

  • pulumi-best-practices: General Pulumi patterns including Output handling, secrets, and aliases
  • pulumi-automation-api: Programmatic orchestration for integration testing and multi-stack workflows
  • pulumi-esc: Centralized secrets and configuration for component deployments
  • pulumi-best-practices: 通用 Pulumi 模式,包括 Output 处理、密钥和别名
  • pulumi-automation-api: 用于集成测试和多栈工作流的程序化编排
  • pulumi-esc: 用于组件部署的集中式密钥和配置

References

参考资料