关于 pulumi-component
为 Pulumi ComponentResource 编写提供完整指导,面向构建和维护基础设施组件的开发者。内容涵盖组件结构、参数与输出设计,以及多语言打包。
主要功能
- 01组件结构与生命周期
- 02参数接口设计
- 03输出与资源组合
- 04多语言组件打包
适用场景
- 01创建可复用的基础设施组件
- 02将内联资源重构为组件
- 03为多语言使用者打包组件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ pulumi/ agent-skills --skill pulumi-component安装后在 Claude Code 中输入/pulumi-component确认已加载。
附带文件agents/openai.yamluse_cases.yaml
详细介绍
编写 Pulumi 组件
ComponentResource 将相关基础设施资源组合成一个可复用的逻辑单元。组件让基础设施更易于理解、复用和维护。在 pulumi preview/pulumi up 的输出和 Pulumi Cloud 控制台中,组件会显示为一个节点,其子节点嵌套在下方。
本技能涵盖组件编写的完整生命周期。如需了解通用的 Pulumi 编码模式(Output 处理、密钥、别名、预览工作流),请改用 pulumi-best-practices 技能。
何时使用此技能
在以下情况下调用此技能:
- 创建新的 ComponentResource 类
- 设计组件的 args 接口
- 让组件可供多种 Pulumi 语言使用
- 发布或分发组件包
- 将内联资源重构为可复用组件
- 调试组件行为(缺少输出、创建卡住、子资源层级错误)
组件结构
每个组件都包含四个必需元素:
- 扩展 ComponentResource,并使用类型 URN 调用
super() - 接受标准参数:name、args 和
ComponentResourceOptions - 为所有子资源设置
parent: this - 在构造函数末尾调用
registerOutputs()
TypeScript
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface StaticSiteArgs {
indexDocument?: pulumi.Input<string>;
errorDocument?: pulumi.Input<string>;
}
class StaticSite extends pulumi.ComponentResource {
public readonly bucketName: pulumi.Output<string>;
public readonly websiteUrl: pulumi.Output<string>;
constructor(name: string, args: StaticSiteArgs, opts?: pulumi.ComponentResourceOptions) {
// 1. Call super with type URN: <package>:<module>:<type>
super("myorg:index:StaticSite", name, {}, opts);
// 2. Create child resources with parent: this
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
const website = new aws.s3.BucketWebsiteConfigurationV2(`${name}-website`, {
bucket: bucket.id,
indexDocument: { suffix: args.indexDocument ?? "index.html" },
errorDocument: { key: args.errorDocument ?? "error.html" },
}, { parent: this });
// 3. Expose outputs as class properties
this.bucketName = bucket.id;
this.websiteUrl = website.websiteEndpoint;
// 4. Register outputs -- always the last line
this.registerOutputs({
bucketName: this.bucketName,
websiteUrl: this.websiteUrl,
});
}
}
// Usage
const site = new StaticSite("marketing", {
indexDocument: "index.html",
});
export const url = site.websiteUrl;
Python
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)
类型 URN 格式
super() 的第一个参数是类型 URN:<package>:<module>:<type>。
| 部分 | 约定 | 示例 |
|---|---|---|
| package | 组织或包名称 | myorg、acme、pkg |
| module | 通常为 index | index |
| type | PascalCase 类名 | StaticSite、VpcNetwork |
完整示例:myorg:index:StaticSite、acme:index:KubernetesCluster
必须调用 registerOutputs
原因:如果不调用 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;
// Missing registerOutputs -- component stuck "creating"
}
}
正确示例:
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 });
}
}
根据组件名称派生子资源名称
原因:硬编码的子资源名称会在多次实例化组件时造成冲突。
错误示例:
// Collides if two instances of this component exist
const bucket = new aws.s3.Bucket("my-bucket", {}, { parent: this });
正确示例:
// Unique per component instance
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
设计 args 接口
args 接口是影响最大的设计决策。它决定了使用者可以配置哪些内容,以及组件的可组合性。
将属性包装在 Input 中
原因:Input<T> 同时接受普通值和来自其他资源的 Output<T>。如果不使用它,使用者就必须通过 .apply() 手动解包输出。
错误示例:
interface WebServiceArgs {
port: number; // Forces consumers to unwrap Outputs
vpcId: string; // Cannot accept vpc.id directly
}
正确示例:
interface WebServiceArgs {
port: pulumi.Input<number>; // Accepts 8080 or someOutput
vpcId: pulumi.Input<string>; // Accepts "vpc-123" or vpc.id
}
保持结构扁平
避免使用深层嵌套的 arg 对象。扁平接口更易于使用和扩展。
// 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> };
};
}
不使用联合类型
联合类型会破坏多语言 SDK 生成。Python、Go 和 C# 无法表示 string | number。
错误示例:
interface MyArgs {
port: pulumi.Input<string | number>; // Fails in Python, Go, C#
}
正确示例:
interface MyArgs {
port: pulumi.Input<number>; // Single type, works everywhere
}
如果需要接受多种形式,请使用单独的可选属性:
interface StorageArgs {
sizeGb?: pulumi.Input<number>; // Specify size in GB
sizeMb?: pulumi.Input<number>; // Or specify size in MB
}
不使用函数或回调
函数无法跨语言边界序列化。
错误示例:
interface MyArgs {
nameTransform: (name: string) => string; // Cannot serialize
}
正确示例:
interface MyArgs {
namePrefix?: pulumi.Input<string>; // Configuration instead of callback
nameSuffix?: pulumi.Input<string>;
}
为可选属性设置默认值
在构造函数中设置合理的默认值,让使用者只需配置所需内容:
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 });
暴露输出
只暴露使用者需要的内容
组件通常会创建许多内部资源。只暴露使用者需要的值,而不是每个内部资源。
错误示例:
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;
// ...
}
正确示例:
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,
});
}
}
派生组合输出
使用 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 });
// 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 });
}
}
有条件地创建资源
使用可选 args 控制是否创建子资源:
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({});
}
}
组合
使用较低层级的组件构建较高层级的组件。每个层级只负责一个方面。
// 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 });
}
}
提供程序透传
为多区域或多账户部署接受显式提供程序。ComponentResourceOptions 会自动将提供程序配置传递给子资源:
// 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],
});
使用 { parent: this } 的子资源会自动继承提供程序。组件内部无需额外编写代码。
多语言组件
如果你的组件会由多种 Pulumi 语言(TypeScript、Python、Go、C#、Java、YAML)使用,请将其打包为多语言组件。
是否需要多语言?
请思考:“是否有人会使用与编写此组件不同的语言来使用它?”
单语言组件(无需打包):
- 你的团队只使用一种语言,且组件仅在该代码库中使用
- 组件仅供单个项目或 monorepo 内部使用
- 无需
PulumiPlugin.yaml——只需直接导入类
多语言组件(必须打包):
- 其他团队会使用不同语言来使用你的组件
- 平台团队为可自行选择语言的开发者构建抽象
- YAML 用户需要访问——即使你使用 TypeScript 编写,YAML 程序也需要多语言打包才能使用你的组件
- 为组织构建共享组件库
- 发布到 Pulumi 私有注册表或公共注册表是常见原因,但并非支持多语言所必需
常见错误:TypeScript 平台团队构建的组件只有 TypeScript 用户可以使用。如果应用开发者使用 Python 或 YAML,那么没有多语言打包的话,他们就无法使用这些组件。
设置
在组件目录中创建 PulumiPlugin.yaml,以声明运行时:
runtime: nodejs
或者使用 Python:
runtime: python
序列化限制
为确保多语言兼容性,参数必须可序列化。无论使用哪种语言编写,都须遵守以下限制:
| 允许 | 不允许 |
|---|---|
string、number、boolean | 联合类型(string | number) |
Input<T> 包装器 | 函数和回调 |
| 基本类型的数组和映射 | 复杂的嵌套泛型 |
| 枚举 | 特定于平台的类型 |
使用多语言组件
使用者通过 pulumi package add 安装组件,该命令会自动下载提供程序插件、生成使用者所用语言的本地 SDK,并更新 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
对于全新检出或 CI 环境,请运行 pulumi install,确保所有软件包依赖项均可用。使用者无需手动生成 SDK。
将 SDK 发布到软件包管理器(npm、PyPI 等)的作者可以选择使用 pulumi package gen-sdk 生成特定语言的 SDK 以供发布。大多数组件作者不需要这样做——pulumi package add 会在使用者端处理 SDK 生成。
入口点
已发布的多语言组件需要一个入口点来承载组件提供程序进程。不同语言的入口点模式各不相同。
TypeScript(runtime: nodejs):
从 index.ts 导出组件类。无需单独的入口点文件。Pulumi 会自动检查导出的类。
// index.ts -- exports are the entry point
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。
参考:https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/
分发
根据受众选择分发方式:
| 受众 | 方式 | 操作 |
|---|---|---|
| 同一项目 | 直接导入 | 使用标准语言导入 |
| 同一组织 | 私有注册表 | 使用 pulumi package publish 发布到 Pulumi Cloud |
| 同一组织 | Git 仓库 | 使用 pulumi package add <repo> 并添加版本标签 |
| 语言生态系统 | 包管理器 | 发布到 npm、PyPI、NuGet 或 Maven |
| 公共社区 | Pulumi Registry | 通过 pulumi/registry GitHub 仓库提交 |
Pulumi 私有注册表
私有注册表是组织组件的集中式目录。它为所有团队提供自动 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() 方法)。
参考:https://www.pulumi.com/docs/idp/get-started/private-registry/
Git 仓库分发
为版本发布添加标签,方便使用者固定版本:
git tag v1.0.0
git push origin v1.0.0
使用者通过以下方式安装:
pulumi package add https://github.com/myorg/[email protected]
包管理器分发
发布特定语言的包,以便使用原生依赖管理:
- npm:使用
npm publish发布 TypeScript/JavaScript 包 - PyPI:使用
twine upload发布 Python 包 - NuGet:使用
dotnet nuget push发布 .NET 包 - Maven Central:使用标准 Maven 发布流程发布 Java 包
参考:https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/
反模式
| 反模式 | 问题 | 修正方法 |
|---|---|---|
在 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> 包装;不使用联合类型或函数;保持结构扁平 |
| 输出 | 使用公开的只读 Output<T> 属性,仅暴露必要内容 |
| 默认值 | 在构造函数中使用 ?? 运算符设置合理的默认值 |
| 组合 | 将较低层级的组件组合成较高层级的组件 |
| 多语言 | 使用 PulumiPlugin.yaml 和入口点;使用者通过 pulumi package add 添加包 |
| 分发 | 私有注册表、git 标签、包管理器或公共 Pulumi Registry |
相关技能
- pulumi-best-practices:涵盖 Output 处理、机密和别名等 Pulumi 通用模式
- pulumi-automation-api:用于集成测试和多堆栈工作流的编程式编排
- pulumi-esc:集中管理组件部署所需的机密和配置
参考资料
- 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/