npx skills add https://github.com/pulumi/agent-skills --skill pulumi-componentComponentResource 将相关的基础设施资源分组为一个可重用的逻辑单元。组件使基础设施更易于理解、重用和维护。在 pulumi preview/pulumi up 输出以及 Pulumi Cloud 控制台中,组件显示为单个节点,其子资源嵌套在下方。
本技能涵盖了完整的组件编写生命周期。关于通用的 Pulumi 编码模式(Output 处理、密钥、别名、预览工作流),请使用 pulumi-best-practices 技能。
在以下情况下调用此技能:
每个组件都有四个必需元素:
super()ComponentResourceOptions广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
parent: thisregisterOutputs()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;
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)
super() 的第一个参数是类型 URN:<package>:<module>:<type>。
| 段 | 约定 | 示例 |
|---|---|---|
| package | 组织或包名 | myorg, acme, pkg |
| module | 通常为 index | index |
| type | PascalCase 类名 | StaticSite, VpcNetwork |
完整示例:myorg:index:StaticSite, acme:index:KubernetesCluster
原因:没有 registerOutputs(),组件在 Pulumi 控制台中会显示为卡在“正在创建”状态,并且输出不会持久化到状态中。
错误示例:
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 -- 组件卡在“正在创建”状态
}
}
正确示例:
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 });
}
}
原因:硬编码的子资源名称在组件被多次实例化时会导致冲突。
错误示例:
// 如果存在此组件的两个实例,会发生冲突
const bucket = new aws.s3.Bucket("my-bucket", {}, { parent: this });
正确示例:
// 每个组件实例唯一
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
参数接口是最具影响力的设计决策。它定义了用户可以配置什么以及组件的可组合性如何。
原因:Input<T> 既接受普通值,也接受来自其他资源的 Output<T>。没有它,用户必须使用 .apply() 手动解包输出。
错误示例:
interface WebServiceArgs {
port: number; // 强制用户解包 Outputs
vpcId: string; // 无法直接接受 vpc.id
}
正确示例:
interface WebServiceArgs {
port: pulumi.Input<number>; // 接受 8080 或 someOutput
vpcId: pulumi.Input<string>; // 接受 "vpc-123" 或 vpc.id
}
避免深度嵌套的参数对象。扁平接口更易于使用和演进。
// 推荐扁平化
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> };
};
}
联合类型会破坏多语言 SDK 生成。Python、Go 和 C# 无法表示 string | number。
错误示例:
interface MyArgs {
port: pulumi.Input<string | number>; // 在 Python、Go、C# 中失败
}
正确示例:
interface MyArgs {
port: pulumi.Input<number>; // 单一类型,适用于所有语言
}
如果需要接受多种形式,请使用单独的可选属性:
interface StorageArgs {
sizeGb?: pulumi.Input<number>; // 以 GB 指定大小
sizeMb?: pulumi.Input<number>; // 或以 MB 指定大小
}
函数无法跨语言边界序列化。
错误示例:
interface MyArgs {
nameTransform: (name: string) => string; // 无法序列化
}
正确示例:
interface MyArgs {
namePrefix?: pulumi.Input<string>; // 使用配置而非回调
nameSuffix?: pulumi.Input<string>;
}
在构造函数内设置合理的默认值,以便用户只需配置他们需要的内容:
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 });
组件通常会创建许多内部资源。只暴露用户需要的值,而不是每个内部资源。
错误示例:
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;
// ...
}
正确示例:
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,
});
}
}
使用 pulumi.interpolate 或 pulumi.concat 构建派生值:
this.connectionString = pulumi.interpolate`postgresql://${args.username}:${args.password}@${cluster.endpoint}:${cluster.port}/${args.databaseName}`;
this.registerOutputs({ connectionString: this.connectionString });
将最佳实践编码为默认值。允许用户在具有特定需求时进行覆盖。
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 });
}
}
使用可选参数来控制子资源的创建:
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({});
}
}
从低层组件构建高层组件。每一层管理一个单一关注点。
// 低层组件
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 });
}
}
为多区域或多账户部署接受显式提供者。ComponentResourceOptions 会自动将提供者配置传递给子资源:
// 用户为不同区域传递一个提供者
const usWest = new aws.Provider("us-west", { region: "us-west-2" });
const site = new StaticSite("west-site", { indexDocument: "index.html" }, {
providers: [usWest],
});
带有 { parent: this } 的子资源会自动继承提供者。组件内部不需要额外的代码。
如果你的组件将被多种 Pulumi 语言(TypeScript、Python、Go、C#、Java、YAML)使用,请将其打包为多语言组件。
问:“是否有人会使用不同于编写语言的其他语言来使用此组件?”
单语言组件(无需打包):
PulumiPlugin.yaml -- 直接导入类即可多语言组件(需要打包):
常见错误:TypeScript 平台团队构建的组件只能被他们的 TypeScript 用户使用。如果应用程序开发者使用 Python 或 YAML,没有多语言打包,这些组件对他们来说是不可见的。
在组件目录中创建一个 PulumiPlugin.yaml 来声明运行时:
runtime: nodejs
或者对于 Python:
runtime: python
为了多语言兼容性,参数必须是可序列化的。无论使用何种编写语言,这些约束都适用:
| 允许 | 不允许 |
|---|---|
string, number, boolean | 联合类型 (`string |
Input<T> 包装器 | 函数和回调 |
| 基本类型的数组和映射 | 复杂的嵌套泛型 |
| 枚举 | 平台特定类型 |
用户使用 pulumi package add 安装组件,这会自动下载提供者插件,在用户的语言中生成本地 SDK,并更新 Pulumi.yaml:
# 从 Git 仓库
pulumi package add <git-repo-url>
# 从特定版本标签
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 生成。
已发布的多语言组件需要一个托管组件提供者进程的入口点。入口点模式因语言而异。
TypeScript (runtime: nodejs):
从 index.ts 导出组件类。不需要单独的入口点文件。Pulumi 会自动检查导出的类。
// index.ts -- 导出就是入口点
export { StaticSite, StaticSiteArgs } from "./staticSite";
export { SecureBucket, SecureBucketArgs } from "./secureBucket";
Python (runtime: python):
创建一个 __main__.py,调用 component_provider_host 并传入所有组件类:
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 来构建和运行提供者:
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 来服务组件提供者主机:
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。
根据你的受众选择分发方法:
| 受众 | 方法 | 方式 |
|---|---|---|
| 同一项目 | 直接导入 | 标准语言导入 |
| 同一组织 | 私有注册表 | pulumi package publish 到 Pulumi Cloud |
| 同一组织 | Git 仓库 | pulumi package add <repo> 配合版本标签 |
| 语言生态系统 | 包管理器 | 发布到 npm、PyPI、NuGet 或 Maven |
| 公共社区 | Pulumi Registry | 通过 pulumi/registry GitHub 仓库提交 |
私有注册表是你组织组件的集中目录。它为所有团队提供自动 API 文档、版本管理和可发现性。
将组件发布到私有注册表:
pulumi package publish https://github.com/myorg/my-component --publisher myorg
使用带有 v 前缀的 git 标签对组件进行版本控制:
git tag v1.0.0
git push origin v1.0.0
发布时需要 README 文件。Pulumi 将其用作注册表中组件的文档页面。
使用 OIDC 身份验证从 GitHub Actions 自动化发布:
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 tag v1.0.0
git push origin v1.0.0
用户安装方式:
pulumi package add https://github.com/myorg/my-component@v1.0.0
发布语言特定的包以进行本地依赖管理:
npm publish 用于 TypeScript/JavaScripttwine upload 用于 Pythondotnet nuget push 用于 .NET| 反模式 | 问题 | 修复方法 |
|---|---|---|
在 apply() 内部创建资源 | 在 pulumi preview 中不可见 | 将资源创建移到 apply 外部(参见 pulumi-best-practices 实践 1) |
缺少 registerOutputs() | 组件卡在“正在创建”状态 | 始终在构造函数最后一行调用 |
缺少 parent: this | 子资源出现在根层级 | 对所有子资源传递 { parent: this } |
| 参数中的联合类型 | 破坏 Python、Go、C# SDK | 使用单一类型;为变体使用单独属性 |
| 参数中的函数 | 无法跨语言序列化 | 使用配置属性替代 |
| 硬编码的子资源名称 | 多个实例时发生冲突 | 从 ${name}-suffix 派生名称 |
| 过度暴露输出 | 泄露实现细节 | 只导出用户需要的内容 |
| 单次使用的组件 | 不必要的抽象开销 | 使用内联资源直到模式重复出现 |
| 深度嵌套的参数 | 难以使用和演进 | 保持接口扁平化,使用可选属性 |
| 主题 | 关键点 |
|---|---|
| 类型 URN | <package>:<module>:<type>,module 通常为 index |
| 构造函数 | super(type, name, {}, opts) 然后创建子资源然后 registerOutputs() |
| 子资源 | 始终使用 { parent: this },从 ${name}-suffix 派生名称 |
| 参数接口 | 包装在 Input<T> 中,不要使用联合类型,不要使用函数,保持结构扁平 |
| 输出 | 公共 readonly Output<T> 属性,只暴露必要内容 |
| 默认值 | 在构造函数中使用 ?? 运算符应用合理的默认值 |
| 组合 | 低层组件组合成高层组件 |
| 多语言 | PulumiPlugin.yaml + 入口点;用户使用 pulumi package add |
| 分发 | 私有注册表、git 标签、包管理器或公共 Pulumi Registry |
每周安装次数
368
仓库
GitHub 星标数
30
首次出现时间
2026 年 1 月 28 日
安全审计
安装于
opencode323
github-copilot320
codex319
gemini-cli310
amp303
kimi-cli301
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.
Invoke this skill when:
Every component has four required elements:
super() with a type URNComponentResourceOptionsparent: this on all child resourcesregisterOutputs() at the end of the constructorimport * 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;
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)
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
Why : Without registerOutputs(), the component appears stuck in a "creating" state in the Pulumi console and outputs are not persisted to state.
Wrong :
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 :
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 });
}
}
Why : Hardcoded child names cause collisions when the component is instantiated multiple times.
Wrong :
// Collides if two instances of this component exist
const bucket = new aws.s3.Bucket("my-bucket", {}, { parent: this });
Right :
// Unique per component instance
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
The args interface is the most impactful design decision. It defines what consumers can configure and how composable the component is.
Why : Input<T> accepts both plain values and Output<T> from other resources. Without it, consumers must unwrap outputs manually with .apply().
Wrong :
interface WebServiceArgs {
port: number; // Forces consumers to unwrap Outputs
vpcId: string; // Cannot accept vpc.id directly
}
Right :
interface WebServiceArgs {
port: pulumi.Input<number>; // Accepts 8080 or someOutput
vpcId: pulumi.Input<string>; // Accepts "vpc-123" or vpc.id
}
Avoid deeply nested arg objects. Flat interfaces are easier to use and evolve.
// 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> };
};
}
Union types break multi-language SDK generation. Python, Go, and C# cannot represent string | number.
Wrong :
interface MyArgs {
port: pulumi.Input<string | number>; // Fails in Python, Go, C#
}
Right :
interface MyArgs {
port: pulumi.Input<number>; // Single type, works everywhere
}
If you need to accept multiple forms, use separate optional properties:
interface StorageArgs {
sizeGb?: pulumi.Input<number>; // Specify size in GB
sizeMb?: pulumi.Input<number>; // Or specify size in MB
}
Functions cannot be serialized across language boundaries.
Wrong :
interface MyArgs {
nameTransform: (name: string) => string; // Cannot serialize
}
Right :
interface MyArgs {
namePrefix?: pulumi.Input<string>; // Configuration instead of callback
nameSuffix?: pulumi.Input<string>;
}
Set sensible defaults inside the constructor so consumers only configure what they need:
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 });
Components often create many internal resources. Expose only the values consumers need, not every internal resource.
Wrong :
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 :
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,
});
}
}
Use pulumi.interpolate or pulumi.concat to build derived values:
this.connectionString = pulumi.interpolate`postgresql://${args.username}:${args.password}@${cluster.endpoint}:${cluster.port}/${args.databaseName}`;
this.registerOutputs({ connectionString: this.connectionString });
Encode best practices as defaults. Allow consumers to override when they have specific requirements.
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 });
}
}
Use optional args to gate creation of sub-resources:
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({});
}
}
Build higher-level components from lower-level ones. Each level manages a single concern.
// 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 });
}
}
Accept explicit providers for multi-region or multi-account deployments. ComponentResourceOptions carries provider configuration to children automatically:
// 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.
If your component will be consumed from multiple Pulumi languages (TypeScript, Python, Go, C#, Java, YAML), package it as a multi-language component.
Ask: "Will anyone consume this component from a different language than it was authored in?"
Single-language component (no packaging needed):
PulumiPlugin.yaml needed -- just import the class directlyMulti-language component (packaging required):
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.
Create a PulumiPlugin.yaml in the component directory to declare the runtime:
runtime: nodejs
Or for Python:
runtime: python
For multi-language compatibility, args must be serializable. These constraints apply regardless of the authoring language:
| Allowed | Not Allowed |
|---|---|
string, number, boolean | Union types (`string |
Input<T> wrappers | Functions and callbacks |
| Arrays and maps of primitives | Complex nested generics |
| Enums | Platform-specific types |
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:
# From a Git repository
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.
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.
// 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:
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:
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:
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.
Reference : https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/
Choose a distribution method based on your audience:
| Audience | Method | How |
|---|---|---|
| Same project | Direct import | Standard language import |
| Same organization | Private registry | pulumi package publish to Pulumi Cloud |
| Same organization | Git repository | pulumi package add <repo> with version tags |
| Language ecosystem | Package manager | Publish to npm, PyPI, NuGet, or Maven |
| Public community | Pulumi Registry | Submit via pulumi/registry GitHub repo |
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:
pulumi package publish https://github.com/myorg/my-component --publisher myorg
Version components using git tags with a v prefix:
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:
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).
Reference : https://www.pulumi.com/docs/idp/get-started/private-registry/
Tag releases for consumers to pin versions:
git tag v1.0.0
git push origin v1.0.0
Consumers install with:
pulumi package add https://github.com/myorg/my-component@v1.0.0
Publish language-specific packages for native dependency management:
npm publish for TypeScript/JavaScripttwine upload for Pythondotnet nuget push for .NETReference : https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/
| Anti-Pattern | Problem | Fix |
|---|---|---|
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 level | Pass { parent: this } to all child resources |
| Topic | Key Point |
|---|---|
| Type URN | <package>:<module>:<type>, module usually index |
| Constructor | super(type, name, {}, opts) then children then registerOutputs() |
| Child resources | Always { parent: this }, derive name from ${name}-suffix |
| Args interface | Wrap in Input<T>, no unions, no functions, flat structure |
Weekly Installs
368
Repository
GitHub Stars
30
First Seen
Jan 28, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykFail
Installed on
opencode323
github-copilot320
codex319
gemini-cli310
amp303
kimi-cli301
阿里云通义千问VL图像理解API:多模态AI模型调用与图像分析指南
297 周安装
AI智能体构建指南:工具调用、记忆与多步推理工作流设计
297 周安装
prompt-lookup AI提示词搜索与优化工具 | prompts.chat MCP服务器 | 提升AI提示词质量
297 周安装
Firebase应用托管基础教程:部署Next.js、Angular全栈Web应用
297 周安装
智能代码探索工具smart-explore:AST解析结构化代码搜索与导航
297 周安装
GrepAI Ollama 本地安装配置教程 - 私有化代码搜索嵌入模型设置指南
297 周安装
| 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 ${name}-suffix |
| 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 |
| Outputs | Public readonly Output<T> properties, expose only essentials |
| Defaults | Use ?? operator to apply sensible defaults in constructor |
| Composition | Lower-level components composed into higher-level ones |
| Multi-language | PulumiPlugin.yaml + entry point; consumers use pulumi package add |
| Distribution | Private registry, git tags, package managers, or public Pulumi Registry |