pulumi-component
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAuthoring 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 / output and in the Pulumi Cloud console.
pulumi previewpulumi upThis skill covers the full component authoring lifecycle. For general Pulumi coding patterns (Output handling, secrets, aliases, preview workflows), use the skill instead.
pulumi-best-practicesComponentResource 可将相关基础设施资源分组为一个可复用的逻辑单元。组件让基础设施更易于理解、复用和维护。在 / 输出以及 Pulumi Cloud 控制台中,组件会显示为一个单独的节点,其下嵌套子资源。
pulumi previewpulumi up本指南涵盖组件编写的完整生命周期。如需了解通用 Pulumi 编码模式(Output 处理、密钥、别名、预览工作流),请使用 技能。
pulumi-best-practicesWhen 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:
- Extend ComponentResource and call with a type URN
super() - Accept standard parameters: name, args, and
ComponentResourceOptions - Set on all child resources
parent: this - Call at the end of the constructor
registerOutputs()
每个组件都包含四个必需元素:
- 继承 ComponentResource 并使用类型 URN 调用
super() - 接受标准参数:名称、参数和
ComponentResourceOptions - 为所有子资源设置
parent: this - 在构造函数末尾调用
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 is the type URN: .
super()<package>:<module>:<type>| Segment | Convention | Example |
|---|---|---|
| package | Organization or package name | |
| module | Usually | |
| type | PascalCase class name | |
Full examples: ,
myorg:index:StaticSiteacme:index:KubernetesClustersuper()<package>:<module>:<type>| 分段 | 约定 | 示例 |
|---|---|---|
| package | 组织或包名称 | |
| module | 通常为 | |
| type | 大驼峰式类名 | |
完整示例:,
myorg:index:StaticSiteacme:index:KubernetesClusterregisterOutputs Is Required
registerOutputs 是必需的
Why: Without , the component appears stuck in a "creating" state in the Pulumi console and outputs are not persisted to state.
registerOutputs()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 });
}
}原因:如果没有 ,组件在 Pulumi 控制台中会一直处于“创建中”状态,且输出不会持久化到状态中。
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;
// 缺少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: accepts both plain values and from other resources. Without it, consumers must unwrap outputs manually with .
Input<T>Output<T>.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 | numberWrong:
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 or to build derived values:
pulumi.interpolatepulumi.concattypescript
this.connectionString = pulumi.interpolate`postgresql://${args.username}:${args.password}@${cluster.endpoint}:${cluster.port}/${args.databaseName}`;
this.registerOutputs({ connectionString: this.connectionString });使用 或 构建派生值:
pulumi.interpolatepulumi.concattypescript
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. carries provider configuration to children automatically:
ComponentResourceOptionstypescript
// 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 automatically inherit the provider. No extra code is needed inside the component.
{ parent: this }为多区域或多账户部署显式接受提供者。 会自动将提供者配置传递给子资源:
ComponentResourceOptionstypescript
// 使用者为不同区域传递提供者
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 needed -- just import the class directly
PulumiPlugin.yaml
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 in the component directory to declare the runtime:
PulumiPlugin.yamlyaml
runtime: nodejsOr for Python:
yaml
runtime: python在组件目录中创建 以声明运行时:
PulumiPlugin.yamlyaml
runtime: nodejs对于 Python:
yaml
runtime: pythonSerialization Constraints
序列化约束
For multi-language compatibility, args must be serializable. These constraints apply regardless of the authoring language:
| Allowed | Not Allowed |
|---|---|
| Union types ( |
| Functions and callbacks |
| Arrays and maps of primitives | Complex nested generics |
| Enums | Platform-specific types |
为了兼容多语言,参数必须可序列化。无论使用哪种编写语言,以下约束均适用:
| 允许 | 不允许 |
|---|---|
| 联合类型 ( |
| 函数和回调 |
| 基本类型的数组和映射 | 复杂嵌套泛型 |
| 枚举 | 平台特定类型 |
Consuming Multi-Language Components
使用多语言组件
Consumers install the component with , which automatically downloads the provider plugin, generates a local SDK in the consumer's language, and updates :
pulumi package addPulumi.yamlbash
undefined使用者使用 安装组件,该命令会自动下载提供者插件、在使用者的语言中生成本地 SDK 并更新 :
pulumi package addPulumi.yamlbash
undefinedFrom 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: nodejsExport component classes from . No separate entry point file is needed. Pulumi introspects exported classes automatically.
index.tstypescript
// index.ts -- exports are the entry point
export { StaticSite, StaticSiteArgs } from "./staticSite";
export { SecureBucket, SecureBucketArgs } from "./secureBucket";Python ():
runtime: pythonCreate a that calls with all component classes:
__main__.pycomponent_provider_hostpython
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: goCreate a that builds and runs the provider:
main.gogo
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: dotnetCreate a that serves the component provider host:
Program.cscsharp
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从 导出组件类。无需单独的入口点文件。Pulumi 会自动 introspect 导出的类。
index.tstypescript
// index.ts -- 导出内容即为入口点
export { StaticSite, StaticSiteArgs } from "./staticSite";
export { SecureBucket, SecureBucketArgs } from "./secureBucket";Python():
runtime: python创建 ,调用 并传入所有组件类:
__main__.pycomponent_provider_hostpython
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.gogo
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.cscsharp
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:
| Audience | Method | How |
|---|---|---|
| Same project | Direct import | Standard language import |
| Same organization | Private registry | |
| Same organization | Git repository | |
| Language ecosystem | Package manager | Publish to npm, PyPI, NuGet, or Maven |
| Public community | Pulumi Registry | Submit via pulumi/registry GitHub repo |
根据受众选择分发方式:
| 受众 | 方式 | 操作 |
|---|---|---|
| 同一项目 | 直接导入 | 标准语言导入 |
| 同一组织 | 私有注册表 | 使用 |
| 同一组织 | Git仓库 | 使用带版本标签的 |
| 语言生态系统 | 包管理器 | 发布到 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 myorgVersion components using git tags with a prefix:
vbash
git tag v1.0.0
git push origin v1.0.0A 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 or environment variables.
GITHUB_TOKENGITLAB_TOKENThe 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, methods in Go).
Annotate()私有注册表是组织组件的集中目录。它为所有团队提供自动 API 文档、版本管理和可发现性。
将组件发布到私有注册表:
bash
pulumi package publish https://github.com/myorg/my-component --publisher myorg使用带 前缀的 Git 标签为组件版本化:
vbash
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_TOKENGITLAB_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.0Consumers 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.0Package Manager Distribution
包管理器分发
Publish language-specific packages for native dependency management:
- npm: for TypeScript/JavaScript
npm publish - PyPI: for Python
twine upload - NuGet: for .NET
dotnet nuget push - 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-Pattern | Problem | Fix |
|---|---|---|
Resources inside | Not visible in | Move resource creation outside apply (see |
Missing | Component stuck "creating" | Always call as last line of constructor |
Missing | Children appear at root level | Pass |
| Union types in args | Breaks Python, Go, C# SDKs | Use single types; separate properties for variants |
| Functions in args | Cannot serialize across languages | Use configuration properties instead |
| Hardcoded child names | Collisions with multiple instances | Derive names from |
| Over-exposed outputs | Leaks implementation details | Export only what consumers need |
| Single-use component | Unnecessary abstraction overhead | Use inline resources until a pattern repeats |
| Deeply nested args | Hard to use and evolve | Keep interfaces flat with optional properties |
| 反模式 | 问题 | 修复方案 |
|---|---|---|
| 在 | 将资源创建移到 apply 外部(请参阅 |
缺失 | 组件一直处于“创建中” | 始终在构造函数最后一行调用 |
缺失 | 子资源显示在根层级 | 为所有子资源传递 |
| 参数使用联合类型 | 破坏 Python、Go、C# SDK | 使用单一类型;为变体使用单独属性 |
| 参数使用函数 | 无法跨语言序列化 | 使用配置属性替代 |
| 子资源使用硬编码名称 | 多实例时发生冲突 | 从 |
| 过度暴露输出 | 泄露实现细节 | 仅导出使用者需要的内容 |
| 一次性组件 | 不必要的抽象开销 | 直到模式重复出现再使用内联资源 |
| 参数深度嵌套 | 难以使用和扩展 | 保持接口扁平化并使用可选属性 |
Quick Reference
快速参考
| Topic | Key Point |
|---|---|
| Type URN | |
| Constructor | |
| Child resources | Always |
| Args interface | Wrap in |
| Outputs | Public readonly |
| Defaults | Use |
| Composition | Lower-level components composed into higher-level ones |
| Multi-language | |
| Distribution | Private registry, git tags, package managers, or public Pulumi Registry |
| 主题 | 关键点 |
|---|---|
| 类型URN | |
| 构造函数 | |
| 子资源 | 始终设置 |
| 参数接口 | 使用 |
| 输出 | 公共只读 |
| 默认值 | 使用 |
| 组合 | 底层组件组合成高层组件 |
| 多语言 | |
| 分发 | 私有注册表、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
参考资料
- https://www.pulumi.com/docs/iac/concepts/resources/components/
- https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/
- https://www.pulumi.com/docs/idp/get-started/private-registry/
- https://www.pulumi.com/docs/iac/concepts/inputs-outputs/
- https://www.pulumi.com/docs/iac/concepts/resources/options/aliases/
- https://www.pulumi.com/docs/iac/concepts/resources/components/
- https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/
- https://www.pulumi.com/docs/idp/get-started/private-registry/
- https://www.pulumi.com/docs/iac/concepts/inputs-outputs/
- https://www.pulumi.com/docs/iac/concepts/resources/options/aliases/