关于 pulumi-best-practices
pulumi-best-practices 是帮助编写、审查和重构 Pulumi 程序的 Skill,也适用于排查资源依赖和配置机密。内容聚焦资源依赖关系、组件组织、机密保护、重构与部署预览。
主要功能
- 01避免在 apply() 中创建资源
- 02将 Output 直接传递为输入
- 03用组件组织相关资源
- 04在组件子资源中设置 parent
- 05从一开始加密机密配置
- 06重构时使用资源别名
- 07每次部署前运行预览
适用场景
- 01审查 Pulumi 程序的正确性
- 02重构基础设施资源而保留身份
- 03排查资源依赖或部署顺序问题
- 04配置和保护密码、令牌等机密
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ pulumi/ agent-skills --skill pulumi-best-practices安装后在 Claude Code 中输入/pulumi-best-practices确认已加载。
附带文件agents/openai.yamluse_cases.yaml
详细介绍
Pulumi 最佳实践
何时使用此技能
在以下情况下调用此技能:
- 编写新的 Pulumi 程序或组件
- 检查 Pulumi 代码的正确性
- 重构现有的 Pulumi 基础设施
- 调试资源依赖问题
- 设置配置和密钥
实践
1. 切勿在 apply() 中创建资源
原因:在 apply() 中创建的资源不会显示在 pulumi preview 中,因此变更难以预测。Pulumi 无法正确跟踪依赖关系,可能导致竞态条件和部署失败。
检测信号:
.apply()回调中出现new aws.或其他资源构造函数- 在
pulumi.all([...]).apply()中创建资源 - 在 apply 中于运行时确定资源数量
错误示例:
const bucket = new aws.s3.Bucket("bucket");
bucket.id.apply(bucketId => {
// WRONG: This resource won't appear in preview
new aws.s3.BucketObject("object", {
bucket: bucketId,
content: "hello",
});
});
正确示例:
const bucket = new aws.s3.Bucket("bucket");
// Pass the output directly - Pulumi handles the dependency
const object = new aws.s3.BucketObject("object", {
bucket: bucket.id, // Output<string> works here
content: "hello",
});
适合使用 apply 的情况:
- 转换输出值,以便用于标签、名称或计算所得的字符串
- 记录日志或调试(而非创建资源)
- 影响资源属性而非资源是否存在的条件逻辑
参考:https://www.pulumi.com/docs/concepts/inputs-outputs/
2. 将输出直接作为输入传递
原因:Pulumi 根据输入与输出之间的关系构建有向无环图(DAG)。直接传递输出可确保资源按正确顺序创建。手动解包值会破坏依赖链,导致资源部署顺序错误,或引用尚不存在的值。
检测信号:
- 从
.apply()中提取的变量随后被用作资源输入 - 在 apply 外对输出值使用
await - 使用字符串拼接处理输出,而不是使用
pulumi.interpolate
错误示例:
const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" });
// WRONG: Extracting the value breaks the dependency chain
let vpcId: string;
vpc.id.apply(id => { vpcId = id; });
const subnet = new aws.ec2.Subnet("subnet", {
vpcId: vpcId, // May be undefined, no tracked dependency
cidrBlock: "10.0.1.0/24",
});
正确示例:
const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" });
const subnet = new aws.ec2.Subnet("subnet", {
vpcId: vpc.id, // Pass the Output directly
cidrBlock: "10.0.1.0/24",
});
字符串插值:
// WRONG
const name = bucket.id.apply(id => `prefix-${id}-suffix`);
// RIGHT - use pulumi.interpolate for template literals
const name = pulumi.interpolate`prefix-${bucket.id}-suffix`;
// RIGHT - use pulumi.concat for simple concatenation
const name = pulumi.concat("prefix-", bucket.id, "-suffix");
参考:https://www.pulumi.com/docs/concepts/inputs-outputs/
3. 使用组件管理相关资源
原因:ComponentResource 类可将相关资源归组为可复用的逻辑单元。若不使用组件,资源图会变成扁平结构,难以理解资源间的归属关系、在不同堆栈间复用模式,或从更高层次分析基础设施。
检测信号:
- 多个相关资源在顶层创建,未进行分组
- 不同堆栈中存在应当抽象出来的重复资源模式
- 在 Pulumi 控制台中难以理解资源间的关系
错误示例:
// Flat structure - no logical grouping, hard to reuse
const bucket = new aws.s3.Bucket("app-bucket");
const bucketPolicy = new aws.s3.BucketPolicy("app-bucket-policy", {
bucket: bucket.id,
policy: policyDoc,
});
const originAccessIdentity = new aws.cloudfront.OriginAccessIdentity("app-oai");
const distribution = new aws.cloudfront.Distribution("app-cdn", { /* ... */ });
正确示例:
interface StaticSiteArgs {
domain: string;
content: pulumi.asset.AssetArchive;
}
class StaticSite extends pulumi.ComponentResource {
public readonly url: pulumi.Output<string>;
constructor(name: string, args: StaticSiteArgs, opts?: pulumi.ComponentResourceOptions) {
super("myorg:components:StaticSite", name, args, opts);
// Resources created here - see practice 4 for parent setup
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
// ...
this.url = distribution.domainName;
this.registerOutputs({ url: this.url });
}
}
// Reusable across stacks
const site = new StaticSite("marketing", {
domain: "marketing.example.com",
content: new pulumi.asset.FileArchive("./dist"),
});
组件最佳实践:
- 使用一致的类型 URN 模式:
organization:module:ComponentName - 在构造函数末尾调用
registerOutputs() - 将输出作为类属性公开,供使用者调用
- 接受
ComponentResourceOptions,以便调用方设置提供程序、别名等
有关组件编写的深入指导(参数设计、多语言支持、测试、分发),请使用技能 pulumi-component。
参考:https://www.pulumi.com/docs/concepts/resources/components/
4. 在组件中始终设置 parent: this
原因:在 ComponentResource 中创建资源时,如果未设置 parent: this,这些资源会出现在堆栈状态的根级别。这会破坏逻辑层级结构,使 Pulumi 控制台难以导航,并可能导致别名和重构问题。父级关系才能让组件真正归组其子资源。
检测信号:
- ComponentResource 类未向子资源传递
{ parent: this } - 组件内的资源在控制台中出现在根级别
- 为组件添加别名时出现意外行为
错误示例:
class MyComponent extends pulumi.ComponentResource {
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("myorg:components:MyComponent", name, {}, opts);
// WRONG: No parent set - this bucket appears at root level
const bucket = new aws.s3.Bucket(`${name}-bucket`);
}
}
正确示例:
class MyComponent extends pulumi.ComponentResource {
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("myorg:components:MyComponent", name, {}, opts);
// RIGHT: Parent establishes hierarchy
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, {
parent: this
});
const policy = new aws.s3.BucketPolicy(`${name}-policy`, {
bucket: bucket.id,
policy: policyDoc,
}, {
parent: this
});
}
}
parent: this 的作用:
- 资源会在 Pulumi 控制台中嵌套显示在组件之下
- 删除组件时会删除所有子资源
- 组件上的别名会自动应用于子资源
- 在状态文件中明确所有权
参考:https://www.pulumi.com/docs/concepts/resources/components/
5. 从第一天起加密机密
原因:使用 --secret 标记的机密会在状态文件中加密、在命令行输出中屏蔽,并在转换过程中持续跟踪。先使用明文配置,之后再转换需要轮换凭据、更新引用,并审查日志和状态历史记录中泄露的值。
检测信号:
- 密码、API 密钥、令牌以明文配置存储
- 连接字符串中嵌入凭据
- 私钥或证书以明文存储
错误做法:
# Plaintext - will be visible in state and logs
pulumi config set databasePassword hunter2
pulumi config set apiKey sk-1234567890
正确做法:
# Encrypted from the start
pulumi config set --secret databasePassword hunter2
pulumi config set --secret apiKey sk-1234567890
在代码中:
const config = new pulumi.Config();
// This retrieves a secret - the value stays encrypted
const dbPassword = config.requireSecret("databasePassword");
// Creating outputs from secrets preserves secrecy
const connectionString = pulumi.interpolate`postgres://user:${dbPassword}@host/db`;
// connectionString is also a secret Output
// Explicitly mark values as secret
const computed = pulumi.secret(someValue);
使用 Pulumi ESC 集中管理机密:
# Pulumi.yaml
environment:
- production-secrets # Pull from ESC environment
# ESC manages secrets centrally across stacks
esc env set production-secrets db.password --secret "hunter2"
哪些内容属于机密:
- 密码和口令
- API 密钥和令牌
- 私钥和证书
- 含有凭据的连接字符串
- OAuth 客户端机密
- 加密密钥
参考资料:
6. 重构时使用别名
原因:重命名资源、将资源移入组件或更改父级,会导致 Pulumi 将其视为新资源。没有别名时,重构会销毁并重新创建资源,可能造成停机或数据丢失。别名可在重构过程中保留资源身份。
检测信号:
- 重命名资源但未添加别名
- 将资源移入或移出 ComponentResource
- 更改资源的父级
- 预览显示删除并创建,但原本只打算更新
错误做法:
// Before: resource named "my-bucket"
const bucket = new aws.s3.Bucket("my-bucket");
// After: renamed without alias - DESTROYS THE BUCKET
const bucket = new aws.s3.Bucket("application-bucket");
正确做法:
// After: renamed with alias - preserves the existing bucket
const bucket = new aws.s3.Bucket("application-bucket", {}, {
aliases: [{ name: "my-bucket" }],
});
移入组件:
// Before: top-level resource
const bucket = new aws.s3.Bucket("my-bucket");
// After: inside a component - needs alias with old parent
class MyComponent extends pulumi.ComponentResource {
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("myorg:components:MyComponent", name, {}, opts);
const bucket = new aws.s3.Bucket("bucket", {}, {
parent: this,
aliases: [{
name: "my-bucket",
parent: pulumi.rootStackResource, // Was at root
}],
});
}
}
别名类型:
// Simple name change
aliases: [{ name: "old-name" }]
// Parent change
aliases: [{ name: "resource-name", parent: oldParent }]
// Full URN (when you know the exact previous URN)
aliases: ["urn:pulumi:stack::project::aws:s3/bucket:Bucket::old-name"]
生命周期:
- 重构时添加别名
- 在所有堆栈上运行
pulumi up - 所有堆栈更新后移除别名(可选,但能让代码更整洁)
参考资料:https://www.pulumi.com/docs/iac/concepts/resources/options/aliases/
7. 每次部署前都进行预览
原因:pulumi preview 会准确显示将要创建、更新或销毁的内容。生产环境中的意外往往源于跳过预览。如果某个资源显示“替换”,而你预期的是“更新”,就意味着即将销毁并重新创建该资源。
检测信号:
- 交互式运行
pulumi up --yes,却未审查变更 - 对于给定变更,CI/CD 工作流中没有任何预览步骤
- 合并或批准部署前未审查预览输出
错误做法:
# Deploying blind
pulumi up --yes
正确做法:
# Always preview first
pulumi preview
# Review the output, then deploy
pulumi up
预览时要留意的内容:
+ create- 将创建新资源~ update- 将就地修改现有资源- delete- 将销毁资源+-replace- 将销毁并重新创建资源(可能导致停机)~+-replace- 将先更新资源,然后替换它
警示信号:
- 出现意外的
replace操作(检查是否更改了不可变属性) - 删除了不应删除的资源
- 变更数量超出代码差异所预期的范围
CI/CD 集成:
# GitHub Actions example
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Pulumi Preview
uses: pulumi/actions@v5
with:
command: preview
stack-name: production
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
deploy:
needs: preview
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Pulumi Up
uses: pulumi/actions@v5
with:
command: up
stack-name: production
PR 工作流:
- 对每个 PR 运行预览
- 将预览输出作为 PR 评论发布
- 合并前必须审查预览
- 仅在合并到 main 后部署
参考资料:
- https://www.pulumi.com/docs/cli/commands/pulumi_preview/
- https://www.pulumi.com/docs/iac/packages-and-automation/continuous-delivery/github-actions/
快速参考
| 实践 | 关键信号 | 修复方法 |
|---|---|---|
| 不要在 apply 中创建资源 | .apply() 内有 new Resource() | 将资源移到外部,直接传递 Output |
| 直接传递输出 | 将提取的值用作输入 | 使用 Output 对象、pulumi.interpolate |
| 使用组件 | 结构扁平、模式重复 | 创建 ComponentResource 类 |
| 设置 parent: this | 组件子资源位于根级别 | 为所有子资源传递 { parent: this } |
| 从第一天起保护机密 | 配置中有明文密码/密钥 | 使用 --secret 标志、ESC |
| 重构时使用别名 | 预览中出现删除并创建 | 添加包含旧名称/父级的别名 |
| 部署前进行预览 | pulumi up --yes | 始终先运行 pulumi preview |
验证清单
审查 Pulumi 代码时,请确认:
-
apply()回调中没有资源构造函数 - 将输出直接传递给依赖资源
- 将相关资源归入 ComponentResource 类
- 子资源具有
{ parent: this } - 敏感值使用
config.requireSecret()或--secret - 重构后的资源具有保留其身份的别名
- 部署流程包含预览步骤
相关技能
- pulumi-overview:入口技能,帮助 Agent 熟悉 Pulumi 的三个方面(
pulumi doCLI、IaC 项目和 Pulumi Cloud),并引导至专门技能。如果任务以一般基础设施相关表述开头,或涉及多个 Pulumi 方面,请在开始时先加载此技能。使用技能pulumi-overview。 - pulumi-component:深入指南,介绍如何编写 ComponentResource 类、设计 args 接口、支持多种语言、测试和分发。使用技能
pulumi-component。 - pulumi-automation-api:以编程方式编排多个堆栈。使用技能
pulumi-automation-api。 - pulumi-esc:集中管理机密和配置。使用技能
pulumi-esc。