mcpField
P
pulumi-best-practices

pulumi-best-practices

github.com/pulumi云与 DevOps782.5k 次安装3 周前更新
代码仓库

关于 pulumi-best-practices

pulumi-best-practices 是帮助编写、审查和重构 Pulumi 程序的 Skill,也适用于排查资源依赖和配置机密。内容聚焦资源依赖关系、组件组织、机密保护、重构与部署预览。

主要功能

  1. 01避免在 apply() 中创建资源
  2. 02将 Output 直接传递为输入
  3. 03用组件组织相关资源
  4. 04在组件子资源中设置 parent
  5. 05从一开始加密机密配置
  6. 06重构时使用资源别名
  7. 07每次部署前运行预览

适用场景

  1. 01审查 Pulumi 程序的正确性
  2. 02重构基础设施资源而保留身份
  3. 03排查资源依赖或部署顺序问题
  4. 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"]

生命周期:

  1. 重构时添加别名
  2. 在所有堆栈上运行 pulumi up
  3. 所有堆栈更新后移除别名(可选,但能让代码更整洁)

参考资料: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 后部署

参考资料:


快速参考

实践关键信号修复方法
不要在 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 do CLI、IaC 项目和 Pulumi Cloud),并引导至专门技能。如果任务以一般基础设施相关表述开头,或涉及多个 Pulumi 方面,请在开始时先加载此技能。使用技能 pulumi-overview。
  • pulumi-component:深入指南,介绍如何编写 ComponentResource 类、设计 args 接口、支持多种语言、测试和分发。使用技能 pulumi-component。
  • pulumi-automation-api:以编程方式编排多个堆栈。使用技能 pulumi-automation-api。
  • pulumi-esc:集中管理机密和配置。使用技能 pulumi-esc。

评论

支持 Markdown