pulumi-best-practices by pulumi/agent-skills
npx skills add https://github.com/pulumi/agent-skills --skill pulumi-best-practices在以下情况下调用此技能:
apply() 内部创建资源原因:在 apply() 内部创建的资源不会出现在 pulumi preview 中,使得变更不可预测。Pulumi 无法正确跟踪依赖关系,导致竞态条件和部署失败。
检测信号:
.apply() 回调函数内部使用 new aws. 或其他资源构造函数pulumi.all([...]).apply() 内部创建资源广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
错误示例:
const bucket = new aws.s3.Bucket("bucket");
bucket.id.apply(bucketId => {
// 错误:此资源不会出现在预览中
new aws.s3.BucketObject("object", {
bucket: bucketId,
content: "hello",
});
});
正确示例:
const bucket = new aws.s3.Bucket("bucket");
// 直接传递输出 - Pulumi 会处理依赖关系
const object = new aws.s3.BucketObject("object", {
bucket: bucket.id, // Output<string> 在此处有效
content: "hello",
});
何时适合使用 apply:
原因:Pulumi 基于输入/输出关系构建有向无环图。直接传递输出可确保正确的创建顺序。手动解包值会破坏依赖链,导致资源以错误顺序部署或引用尚不存在的值。
检测信号:
.apply() 提取的变量稍后用作资源输入awaitpulumi.interpolate 处理输出错误示例:
const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" });
// 错误:提取值会破坏依赖链
let vpcId: string;
vpc.id.apply(id => { vpcId = id; });
const subnet = new aws.ec2.Subnet("subnet", {
vpcId: vpcId, // 可能未定义,没有跟踪的依赖关系
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, // 直接传递 Output
cidrBlock: "10.0.1.0/24",
});
字符串插值:
// 错误
const name = bucket.id.apply(id => `prefix-${id}-suffix`);
// 正确 - 对模板字面量使用 pulumi.interpolate
const name = pulumi.interpolate`prefix-${bucket.id}-suffix`;
// 正确 - 对简单拼接使用 pulumi.concat
const name = pulumi.concat("prefix-", bucket.id, "-suffix");
原因:ComponentResource 类将相关资源分组为可重用的逻辑单元。没有组件,你的资源图是扁平的,难以理解哪些资源属于一起、跨堆栈重用模式或在更高层次上推理你的基础设施。
检测信号:
错误示例:
// 扁平结构 - 无逻辑分组,难以重用
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);
// 在此处创建资源 - 关于父级设置请参见实践 4
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
// ...
this.url = distribution.domainName;
this.registerOutputs({ url: this.url });
}
}
// 可跨堆栈重用
const site = new StaticSite("marketing", {
domain: "marketing.example.com",
content: new pulumi.asset.FileArchive("./dist"),
});
组件最佳实践:
organization:module:ComponentNameregisterOutputs()ComponentResourceOptions 以允许调用者设置提供者、别名等有关深入的组件编写指南(参数设计、多语言支持、测试、分发),请使用技能 pulumi-component。
parent: this原因:当你在 ComponentResource 内部创建资源而不设置 parent: this 时,这些资源会出现在堆栈状态的根级别。这会破坏逻辑层次结构,使 Pulumi 控制台难以导航,并可能导致别名和重构问题。父级关系是使组件实际分组其子资源的关键。
检测信号:
{ parent: this } 传递给子资源错误示例:
class MyComponent extends pulumi.ComponentResource {
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("myorg:components:MyComponent", name, {}, opts);
// 错误:未设置父级 - 此存储桶出现在根级别
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);
// 正确:父级建立层次结构
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 提供的好处:
原因:使用 --secret 标记的密钥在状态文件中被加密,在 CLI 输出中被屏蔽,并通过转换进行跟踪。从明文配置开始并在稍后转换需要凭证轮换、引用更新以及对日志和状态历史中泄露值的审计。
检测信号:
错误示例:
# 明文 - 将在状态和日志中可见
pulumi config set databasePassword hunter2
pulumi config set apiKey sk-1234567890
正确示例:
# 从一开始就加密
pulumi config set --secret databasePassword hunter2
pulumi config set --secret apiKey sk-1234567890
在代码中:
const config = new pulumi.Config();
// 这检索一个密钥 - 值保持加密
const dbPassword = config.requireSecret("databasePassword");
// 从密钥创建输出会保持机密性
const connectionString = pulumi.interpolate`postgres://user:${dbPassword}@host/db`;
// connectionString 也是一个密钥 Output
// 显式将值标记为密钥
const computed = pulumi.secret(someValue);
使用 Pulumi ESC 进行集中式密钥管理:
# Pulumi.yaml
environment:
- production-secrets # 从 ESC 环境拉取
# ESC 跨堆栈集中管理密钥
esc env set production-secrets db.password --secret "hunter2"
什么属于密钥:
参考:
原因:重命名资源、将其移入组件或更改父级会导致 Pulumi 将它们视为新资源。没有别名,重构会销毁并重新创建资源,可能导致停机或数据丢失。别名在重构过程中保持资源身份。
检测信号:
错误示例:
// 之前:资源名为 "my-bucket"
const bucket = new aws.s3.Bucket("my-bucket");
// 之后:重命名未使用别名 - 销毁存储桶
const bucket = new aws.s3.Bucket("application-bucket");
正确示例:
// 之后:使用别名重命名 - 保留现有存储桶
const bucket = new aws.s3.Bucket("application-bucket", {}, {
aliases: [{ name: "my-bucket" }],
});
移入组件:
// 之前:顶层资源
const bucket = new aws.s3.Bucket("my-bucket");
// 之后:在组件内部 - 需要带有旧父级的别名
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, // 之前在根级别
}],
});
}
}
别名类型:
// 简单名称更改
aliases: [{ name: "old-name" }]
// 父级更改
aliases: [{ name: "resource-name", parent: oldParent }]
// 完整 URN(当你知道确切的前一个 URN 时)
aliases: ["urn:pulumi:stack::project::aws:s3/bucket:Bucket::old-name"]
生命周期:
pulumi up原因:pulumi preview 精确显示将创建、更新或销毁的内容。生产环境中的意外源于跳过预览。当你预期是“更新”时,资源显示“替换”意味着即将销毁和重新创建。
检测信号:
pulumi up --yes 而未审查变更错误示例:
# 盲目部署
pulumi up --yes
正确示例:
# 始终先预览
pulumi preview
# 审查输出,然后部署
pulumi up
在预览中需要关注的内容:
+ create - 将创建新资源~ update - 现有资源将就地修改- delete - 资源将被销毁+-replace - 资源将被销毁并重新创建(可能导致停机)~+-replace - 资源将先更新,然后替换警告信号:
replace 操作(检查不可变属性更改)CI/CD 集成:
# GitHub Actions 示例
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 工作流:
参考:
| 实践 | 关键信号 | 修复方法 |
|---|---|---|
| 不在 apply 中创建资源 | 在 .apply() 内部使用 new Resource() | 将资源移到外部,直接传递 Output |
| 直接传递输出 | 提取的值用作输入 | 使用 Output 对象,pulumi.interpolate |
| 使用组件 | 扁平结构,重复模式 | 创建 ComponentResource 类 |
| 设置 parent: this | 组件的子资源出现在根级别 | 向所有子资源传递 { parent: this } |
| 从一开始加密密钥 | 配置中的明文密码/密钥 | 使用 --secret 标志,ESC |
| 重构时使用别名 | 预览中显示删除+创建 | 添加带有旧名称/父级的别名 |
| 部署前预览 | pulumi up --yes | 始终先运行 pulumi preview |
审查 Pulumi 代码时,请验证:
apply() 回调函数内部{ parent: this }config.requireSecret() 或 --secretpulumi-component。pulumi-automation-api。pulumi-esc。每周安装次数
549
代码仓库
GitHub 星标数
30
首次出现
Jan 28, 2026
安全审计
安装于
opencode486
codex482
github-copilot481
gemini-cli472
amp459
kimi-cli458
Invoke this skill when:
apply()Why : Resources created inside apply() don't appear in pulumi preview, making changes unpredictable. Pulumi cannot properly track dependencies, leading to race conditions and deployment failures.
Detection signals :
new aws. or other resource constructors inside .apply() callbackspulumi.all([...]).apply()Wrong :
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",
});
});
Right :
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",
});
When apply is appropriate :
Reference : https://www.pulumi.com/docs/concepts/inputs-outputs/
Why : Pulumi builds a directed acyclic graph (DAG) based on input/output relationships. Passing outputs directly ensures correct creation order. Unwrapping values manually breaks the dependency chain, causing resources to deploy in wrong order or reference values that don't exist yet.
Detection signals :
.apply() used later as resource inputsawait on output values outside of applypulumi.interpolateWrong :
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",
});
Right :
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",
});
For string interpolation :
// 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");
Reference : https://www.pulumi.com/docs/concepts/inputs-outputs/
Why : ComponentResource classes group related resources into reusable, logical units. Without components, your resource graph is flat, making it hard to understand which resources belong together, reuse patterns across stacks, or reason about your infrastructure at a higher level.
Detection signals :
Wrong :
// 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", { /* ... */ });
Right :
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"),
});
Component best practices :
organization:module:ComponentNameregisterOutputs() at the end of the constructorComponentResourceOptions to allow callers to set providers, aliases, etc.For in-depth component authoring guidance (args design, multi-language support, testing, distribution), use skill pulumi-component.
Reference : https://www.pulumi.com/docs/concepts/resources/components/
parent: this in ComponentsWhy : When you create resources inside a ComponentResource without setting parent: this, those resources appear at the root level of your stack's state. This breaks the logical hierarchy, makes the Pulumi console hard to navigate, and can cause issues with aliases and refactoring. The parent relationship is what makes the component actually group its children.
Detection signals :
{ parent: this } to child resourcesWrong :
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`);
}
}
Right :
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
});
}
}
What parent: this provides :
Reference : https://www.pulumi.com/docs/concepts/resources/components/
Why : Secrets marked with --secret are encrypted in state files, masked in CLI output, and tracked through transformations. Starting with plaintext config and converting later requires credential rotation, reference updates, and audit of leaked values in logs and state history.
Detection signals :
Wrong :
# Plaintext - will be visible in state and logs
pulumi config set databasePassword hunter2
pulumi config set apiKey sk-1234567890
Right :
# Encrypted from the start
pulumi config set --secret databasePassword hunter2
pulumi config set --secret apiKey sk-1234567890
In code :
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);
Use Pulumi ESC for centralized secrets :
# Pulumi.yaml
environment:
- production-secrets # Pull from ESC environment
# ESC manages secrets centrally across stacks
esc env set production-secrets db.password --secret "hunter2"
What qualifies as a secret :
References :
Why : Renaming resources, moving them into components, or changing parents causes Pulumi to see them as new resources. Without aliases, refactoring destroys and recreates resources, potentially causing downtime or data loss. Aliases preserve resource identity through refactors.
Detection signals :
Wrong :
// 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");
Right :
// After: renamed with alias - preserves the existing bucket
const bucket = new aws.s3.Bucket("application-bucket", {}, {
aliases: [{ name: "my-bucket" }],
});
Moving into a component :
// 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
}],
});
}
}
Alias types :
// 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"]
Lifecycle :
pulumi up on all stacksReference : https://www.pulumi.com/docs/iac/concepts/resources/options/aliases/
Why : pulumi preview shows exactly what will be created, updated, or destroyed. Surprises in production come from skipping preview. A resource showing "replace" when you expected "update" means imminent destruction and recreation.
Detection signals :
pulumi up --yes interactively without reviewing changesWrong :
# Deploying blind
pulumi up --yes
Right :
# Always preview first
pulumi preview
# Review the output, then deploy
pulumi up
What to look for in preview :
+ create - New resource will be created~ update - Existing resource will be modified in place- delete - Resource will be destroyed+-replace - Resource will be destroyed and recreated (potential downtime)~+-replace - Resource will be updated, then replacedWarning signs :
replace operations (check for immutable property changes)CI/CD integration :
# 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 workflow :
References :
| Practice | Key Signal | Fix |
|---|---|---|
| No resources in apply | new Resource() inside .apply() | Move resource outside, pass Output directly |
| Pass outputs directly | Extracted values used as inputs | Use Output objects, pulumi.interpolate |
| Use components | Flat structure, repeated patterns | Create ComponentResource classes |
| Set parent: this | Component children at root level | Pass { parent: this } to all child resources |
| Secrets from day one | Plaintext passwords/keys in config | Use flag, ESC |
When reviewing Pulumi code, verify:
apply() callbacks{ parent: this }config.requireSecret() or --secretpulumi-component.pulumi-automation-api.pulumi-esc.Weekly Installs
549
Repository
GitHub Stars
30
First Seen
Jan 28, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode486
codex482
github-copilot481
gemini-cli472
amp459
kimi-cli458
FastMCP Python框架:快速构建MCP服务器,为Claude等大模型提供工具与资源
405 周安装
Nginx配置完全指南:生产级反向代理、负载均衡、SSL终止与性能优化
406 周安装
iOS HIG 设计指南:原生应用界面开发框架与 SwiftUI/UIKit 最佳实践
406 周安装
Aceternity UI - Next.js 13+ 动画React组件库 | Tailwind CSS & Framer Motion
406 周安装
RAG工程师技能详解:检索增强生成系统架构与最佳实践
406 周安装
survey技能:跨平台问题空间扫描与调研工具 | 智能体开发研究
406 周安装
--secret| Aliases when refactoring | Delete+create in preview | Add alias with old name/parent |
| Preview before deploy | pulumi up --yes | Always run pulumi preview first |