Pulumi 基础设施即代码
快速指南: 使用完全类型安全的 TypeScript 定义云基础设施。使用
ComponentResource封装可复用的基础设施模式。在组件内部将所有子资源传入{ parent: this }。使用pulumi.interpolate进行带有 Output 的字符串构建(不要使用字符串拼接)。永远不要在.apply()内部创建资源。对敏感值使用Config.requireSecret()。优先使用transforms而非已弃用的transformations。始终在组件构造函数末尾调用this.registerOutputs()。
<critical_requirements>
关键:使用此技能前须知
所有代码必须遵循 CLAUDE.md 中的项目约定(kebab-case、命名导出、导入顺序、
import type、命名常量)
(你必须将 { parent: this } 传递给 ComponentResource 内的所有子资源 — 省略会破坏资源树和状态跟踪)
(你必须使用 pulumi.interpolate 进行带有 Output 的字符串构建 — 字符串拼接会静默产生 [object Object])
(你绝不能在 .apply() 内部创建资源 — 它们不会出现在 pulumi preview 中,并会导致排序问题)
(你必须对敏感值使用 Config.requireSecret() — Config.require() 会将值以明文形式存储在状态中)
(你必须在每个 ComponentResource 构造函数末尾调用 this.registerOutputs() — 省略会阻止输出跟踪)
</critical_requirements>
详细资源:
- examples/core.md - 资源定义、组件资源、命名、Output、配置/密文
- examples/advanced.md - 栈引用、转换、动态提供程序、自动化 API、策略包
- reference.md - 决策框架、资源选项表、API 参考、CLI 命令
自动检测: Pulumi, @pulumi/pulumi, @pulumi/aws, @pulumi/gcp, @pulumi/azure, @pulumi/kubernetes, pulumi.ComponentResource, pulumi.CustomResource, pulumi.Output, pulumi.Config, pulumi.interpolate, pulumi.all, registerOutputs, StackReference, ComponentResourceOptions, CustomResourceOptions, dynamic.Resource, dynamic.ResourceProvider, LocalWorkspace, InlineProgramArgs, Automation API, CrossGuard, PolicyPack
何时使用:
- 使用 TypeScript 和类型安全的资源 API 定义云基础设施
- 使用
ComponentResource创建可复用的基础设施组件 - 使用栈引用管理多栈架构
- 处理密文和环境特定配置
- 使用自动化 API 构建自助服务平台
- 使用 CrossGuard 策略包编写合规策略
何时不应使用:
- 一次性的 Shell 脚本创建单个资源(直接使用云 CLI)
- 团队没有 TypeScript 经验的项目(考虑其他 IaC 语言选项)
涵盖的关键模式:
- 带有类型输入和自动命名的资源定义
- ComponentResource 封装(父级、命名、registerOutputs)
- Output:
apply、all、interpolate和提升 - 配置和密文管理(
Config.require、Config.requireSecret、pulumi.secret) - 用于跨栈数据共享的栈引用
- 资源选项(
dependsOn、protect、aliases、ignoreChanges、transforms) - 用于自定义 CRUD 资源的动态提供程序
- 用于程序化栈管理的自动化 API
- 用于合规执行的 CrossGuard 策略包
哲学
Pulumi 将基础设施视为真正的代码,而非配置文件。TypeScript 提供了类型安全、IDE 自动补全、重构工具以及完整的 Node.js 生态系统。资源是对象,依赖关系是自动的,重用通过函数和类实现 — 而非自定义的模块语言。
核心原则:
- 资源是对象:每个云资源都是一个 TypeScript 类实例,带有类型化的输入和输出
- 依赖关系是自动的:当你将一个资源的输出作为另一个资源的输入时,Pulumi 会推断依赖关系图
- 通过组件实现重用:
ComponentResource将多个资源封装成一个逻辑单元,具有自己的输入和输出 - Output 是 Promise:
Output<T>表示一个值,该值可能在部署完成后才确定 — 使用apply、all或interpolate来处理它们,切勿手动解包 - 状态被管理:Pulumi 在状态中跟踪每个资源 — 更改逻辑名称或在文件之间移动资源会触发删除并重新创建,除非你使用
aliases
核心模式
模式 1:资源定义
每个资源都接受一个逻辑名称(用于状态跟踪)、一个参数包(类型化输入)以及可选的资源选项。
const bucket = new aws.s3.Bucket(
"data-bucket",
{
versioning: { enabled: true },
lifecycleRules: [
{ enabled: true, expiration: { days: BUCKET_EXPIRY_DAYS } },
],
},
{ protect: true },
); // 防止意外删除
关键点: 逻辑名称在一个栈内按类型必须唯一。Pulumi 会自动在物理名称后追加随机后缀以防止冲突。在关键资源上使用 protect: true。导出输出以用于跨栈消费。
参见 examples/core.md 了解资源命名、自动命名配置和提供程序选项。
模式 2:ComponentResource 封装
将相关资源封装在 ComponentResource 中,创建可复用的基础设施单元。
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface StaticSiteArgs {
indexDocument?: string;
errorDocument?: 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,
) {
super("myinfra:web:StaticSite", name, args, opts);
const bucket = new aws.s3.BucketV2(`${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 },
);
this.bucketName = bucket.id;
this.websiteUrl = website.websiteEndpoint;
this.registerOutputs({
bucketName: this.bucketName,
websiteUrl: this.websiteUrl,
});
}
}
为何良好: 子资源使用 { parent: this },名称从父级前缀化,调用了 registerOutputs,类型令牌遵循 pkg:module:Type 格式
参见 examples/core.md 了解完整的组件模式,包括提供程序继承和多资源组件。
模式 3:处理 Output
Output 表示在部署后解析的值。永远不要使用字符串拼接 — 使用 interpolate、apply 或 all。
// interpolate — 用于字符串构建的标记模板(推荐)
const url = pulumi.interpolate`https://${bucket.bucketRegionalDomainName}/index.html`;
// apply — 转换单个输出
const upper = bucket.id.apply((id) => id.toUpperCase());
// all — 组合多个输出
const endpoint = pulumi
.all([lb.dnsName, listener.port])
.apply(([dns, port]) => `http://${dns}:${port}`);
// 提升 — 直接在资源输出上访问属性
const subnetId = vpc.subnets[0].id; // 已知属性无需 apply
为何良好: interpolate 透明地处理 Output 值,all 等待多个值,提升避免不必要的 apply 调用
陷阱: 在 .apply() 内部创建的资源不会出现在 pulumi preview 中,可能导致排序问题。始终将 Output 直接作为输入传递给其他资源。
参见 examples/core.md 了解 Output 模式、pulumi.output() 封装和 apply 反模式。
模式 4:配置和密文
使用 pulumi.Config 处理栈特定值。对敏感数据使用 requireSecret — 它会在状态中加密该值。
const config = new pulumi.Config();
// 普通配置值
const region = config.require("region"); // 缺失则失败
const nodeCount = config.getNumber("nodeCount"); // 缺失则返回 undefined
// 密文值 — 在状态中加密
const dbPassword = config.requireSecret("dbPassword");
const apiKey = config.getSecret("apiKey");
// 标记程序化值为密文
const connectionString = pulumi.interpolate`postgres://admin:${dbPassword}@${db.endpoint}/mydb`;
// connectionString 自动成为密文,因为 dbPassword 是密文
// 显式标记一个值为密文
const token = pulumi.secret(generateToken());
关键点: 任何从密文派生的 Output 都会自动标记为密文。你不需要重新标记派生值。
参见 examples/core.md 了解命名空间配置、密文输出和 config set CLI 命令。
模式 5:资源选项
资源选项控制生命周期行为。最重要的选项:
const db = new aws.rds.Instance(
"primary-db",
{
/* ... */
},
{
protect: true, // 防止意外删除
dependsOn: [vpc, securityGroup], // 显式排序
ignoreChanges: ["tags"], // 忽略特定属性的漂移
aliases: [{ name: "old-db-name" }], // 重命名而不重新创建
retainOnDelete: true, // 执行 pulumi destroy 时保留云资源
deleteBeforeReplace: true, // 对于必须唯一的资源
replaceOnChanges: ["engine"], // 对特定更改强制替换
provider: usEastProvider, // 显式提供程序(区域、账户)
},
);
陷阱: 更改资源的逻辑名称或父级会导致 Pulumi 删除并重新创建它。使用 aliases 安全重命名。
参见 reference.md 了解完整的资源选项表。
模式 6:栈引用
使用 StackReference 在栈之间共享输出。
// 在网络栈中:导出输出
export const vpcId = vpc.id;
export const subnetIds = subnets.map((s) => s.id);
// 在应用栈中:消费输出
const networkStack = new pulumi.StackReference("myorg/networking/prod");
const vpcId = networkStack.getOutput("vpcId");
const subnetIds = networkStack.getOutput("subnetIds");
// requireOutput 在输出不存在时失败(比 getOutput 更安全)
const vpcIdRequired = networkStack.requireOutput("vpcId");
参见 examples/advanced.md 了解栈引用模式和 getOutputDetails。
模式 7:转换
对资源及其子资源应用转换。使用 transforms(而非已弃用的 transformations)。Transforms 接收一个参数对象,包含 type、props 和 opts,并返回修改后的结果或 undefined 以跳过。
// 资源级别:对组件的所有可标记子资源应用标签
const vpc = new MyVpcComponent(
"vpc",
{},
{
transforms: [
(args) => {
if (isTaggable(args.type)) {
return {
props: {
...args.props,
tags: { ...args.props["tags"], ManagedBy: "pulumi" },
},
opts: args.opts,
};
}
return undefined;
},
],
},
);
与已弃用 transformations 的关键区别: transforms 支持修改打包组件的子资源(awsx、eks),支持异步回调,且不传递 Resource 对象。
参见 examples/advanced.md 了解栈级转换、选项修改和从 transformations 迁移。
模式 8:自动化 API
无需 CLI 即可程序化运行 Pulumi — 适用于自助服务平台、集成测试或自定义部署工具。
const stack = await LocalWorkspace.createOrSelectStack({
stackName: "dev",
projectName: "my-platform",
program: async () => {
const bucket = new aws.s3.Bucket("auto-bucket");
return { bucketName: bucket.id };
},
});
const upResult = await stack.up({ onOutput: console.log });
关键点: 自动化 API 需要安装 Pulumi CLI 并在 PATH 中 — 它在底层使用 CLI 引擎。
参见 examples/advanced.md 了解预览、销毁、本地程序模式、配置设置和栈输出检索。
<red_flags>
红色标志
高优先级:
- 在
.apply()内部创建资源 — 它们不会出现在pulumi preview中,导致排序问题并破坏依赖关系图。将 Output 直接作为资源输入传递。 - ComponentResource 子资源中缺少
{ parent: this }— 资源出现在状态树的根节点,破坏逻辑分组和组件删除级联。 - 使用 Output 进行字符串拼接 —
"https://" + bucket.id产生[object Object]。改用pulumi.interpolate。 - 对密码/密钥使用
Config.require()— 将值以明文形式存储在状态中。使用Config.requireSecret()进行加密。 - 忘记
this.registerOutputs()— 组件输出无法在状态中正确跟踪,也无法通过栈引用访问。 - 更改逻辑名称而不使用 aliases — Pulumi 会删除并重新创建资源。使用
aliases: [{ name: "old-name" }]安全重命名。
中优先级:
- 在多区域设置中依赖默认提供程序 — 每个区域使用显式提供程序。在配置中设置
pulumi:disable-default-providers以强制执行。 - 从函数而非 ComponentResource 开始 — 后续迁移需要 aliases 或资源重新创建。从一开始就使用组件。
- 未对非明显依赖使用
dependsOn— Pulumi 从输入/输出连接中推断依赖关系,但副作用(如 IAM 传播延迟)需要手动添加。
低优先级:
- 未使用
pulumi.StackReference而使用硬编码值引用跨栈输出 — 会导致配置漂移。始终引用栈引用。 - 在大型项目中未使用命名空间配置键 — 使用点分隔键(例如
myapp:databasePassword)以避免与同名键发生冲突。
</red_flags>