mcpField
P
pulumi-best-practices

pulumi-best-practices

github.com/pulumiクラウド・DevOps782.5k 回インストール3 週間前に更新
リポジトリ

pulumi-best-practices について

pulumi-best-practicesは、Pulumiプログラムの作成、レビュー、リファクタリングを支援するスキルです。リソース依存関係の調査やシークレット設定にも対応し、コンポーネント構成やデプロイ前のプレビューも扱います。

主な機能

  1. 01apply()内でのリソース作成回避
  2. 02Outputを入力へ直接渡す方法
  3. 03関連リソースのコンポーネント化
  4. 04コンポーネント子リソースへのparent設定
  5. 05設定シークレットの初期暗号化
  6. 06リソースのリファクタリング時のエイリアス使用
  7. 07各デプロイ前のプレビュー実行

活用シーン

  1. 01Pulumiプログラムの正確性レビュー
  2. 02リソースIDを保つインフラのリファクタリング
  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 コンソールでの移動が難しくなり、エイリアスやリファクタリングに問題が発生することがあります。親子関係を設定することで、コンポーネントが子リソースを実際にグループ化できます。

検出の手がかり:

  • 子リソースに { parent: this } を渡していない ComponentResource クラス
  • コンポーネント内のリソースがコンソールのルートレベルに表示されている
  • コンポーネントにエイリアスを追加した際に予期しない動作が発生する

誤った例:

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 でマークされたシークレットは、状態ファイル内で暗号化され、CLI 出力ではマスクされ、変換を通じて追跡されます。平文の設定から始めて後から変換する場合は、認証情報のローテーション、参照の更新、ログや状態の履歴に漏えいした値がないかの監査が必要になります。

検出の手がかり:

  • パスワード、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 に移動する、または 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 を実行すると、作成、更新、破棄される内容を正確に確認できます。本番環境で予期しない事態が起きるのは、プレビューを省略するためです。更新を想定していたリソースに「replace」と表示された場合、破棄と再作成が差し迫っていることを意味します。

検出の手がかり:

  • 変更を確認せずに対話的に 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:Pulumi の 3 つの領域(pulumi do CLI、IaC プロジェクト、Pulumi Cloud)をエージェントに案内し、専門スキルへ誘導するエントリーポイントとなるスキルです。タスクが一般的なインフラストラクチャに関する表現で始まる場合や、複数の Pulumi 領域にまたがる場合は、最初に読み込みます。スキル pulumi-overview を使用してください。
  • pulumi-component:ComponentResource クラスの作成、引数インターフェースの設計、複数言語のサポート、テスト、配布に関する詳細ガイドです。スキル pulumi-component を使用してください。
  • pulumi-automation-api:複数のスタックをプログラムでオーケストレーションします。スキル pulumi-automation-api を使用してください。
  • pulumi-esc:シークレットと構成を一元管理します。スキル pulumi-esc を使用してください。

コメント