pulumi-best-practices
Pulumi の TypeScript/Python プログラムの作成・レビュー・デバッグ時、`Output<T>` や `apply()` の使い方、`ComponentResource` クラスの作成、エイリアスを使ったリソースの再構成、シークレット・設定管理、CI での `pulumi preview/up` ワークフロー構築などに関する質問をトリガーとして読み込みます。リソースの依存関係の順序、親子リソースの関係、`pulumi.interpolate` に関する質問にも対応します。
description の原文を見る
Load when the user is writing, reviewing, or debugging Pulumi TypeScript/Python programs; asks about Output<T> or apply() usage; wants to create ComponentResource classes; needs to refactor resources without destroying them (aliases); is setting up secrets or config; or is configuring a pulumi preview/up CI workflow. Also load for questions about resource dependency order, parent/child resource relationships, or pulumi.interpolate.
SKILL.md 本文
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 => {
// 間違い: このリソースは preview に表示されない
new aws.s3.BucketObject("object", {
bucket: bucketId,
content: "hello",
});
});
正しい例:
const bucket = new aws.s3.Bucket("bucket");
// Output を直接渡す - Pulumi が依存性を処理する
const object = new aws.s3.BucketObject("object", {
bucket: bucket.id, // Output<string> がここで機能する
content: "hello",
});
apply が適切な場合:
- タグ、名前、計算文字列で使用する出力値の変換
- ロギングまたはデバッグ(リソース作成ではない)
- リソースの存在ではなく、リソースプロパティに影響する条件付きロジック
参考: https://www.pulumi.com/docs/concepts/inputs-outputs/
2. Output を入力として直接渡す
理由: Pulumi は入出力関係に基づいて有向非環グラフ(DAG)を構築します。Output を直接渡すことで、正しい作成順序が保証されます。値を手動でアンラップすると依存性チェーンが破され、リソースが誤った順序でデプロイされたり、まだ存在しない値を参照したりします。
検出シグナル:
.apply()から抽出された変数がリソース入力として後で使用される- apply の外で出力値に対する
awaitの使用 pulumi.interpolateではなく、Output との文字列連結
間違い:
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");
参考: https://www.pulumi.com/docs/concepts/inputs-outputs/
3. 関連リソースにはコンポーネントを使用する
理由: ComponentResource クラスは関連リソースを再利用可能な論理単位にグループ化します。コンポーネントがなければ、リソースグラフはフラットになり、どのリソースが一緒に属しているのか理解しづらく、スタック全体でパターンを再利用しにくく、インフラストラクチャを高レベルで推論するのが困難になります。
検出シグナル:
- グループ化なしにトップレベルで作成される複数の関連リソース
- スタック全体で繰り返されるべきリソースパターン
- Pulumi コンソールからリソース関係を理解するのが難しい
間違い:
// フラット構造 - 論理的グループがなく、再利用しにくい
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);
// ここで作成されるリソース - セットアップの親については practice 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"),
});
コンポーネントのベストプラクティス:
- 一貫した URN パターンを使用:
organization:module:ComponentName - コンストラクタの最後で
registerOutputs()を呼び出す - コンシューマーのためにクラスプロパティとして出力を公開
- 呼び出し元がプロバイダー、エイリアスなどを設定できるようにするため
ComponentResourceOptionsを受け取る
コンポーネント作成に関する詳細なガイダンス(引数設計、多言語サポート、テスト、配布)については、スキル pulumi-component を使用してください。
参考: https://www.pulumi.com/docs/concepts/resources/components/
4. ComponentResource 内で常に parent: this を設定する
理由: parent: this を設定せずに ComponentResource 内でリソースを作成すると、それらのリソースはスタックの状態のルートレベルに表示されます。これにより論理階層が破れ、Pulumi コンソールのナビゲーションが困難になり、エイリアスとリファクタリングで問題が生じる可能性があります。親の関係こそが、コンポーネントが実際に子をグループ化するものです。
検出シグナル:
- 子リソースに
{ parent: this }を渡さない ComponentResource クラス - コンポーネント内のリソースがコンソールのルートレベルに表示される
- コンポーネントにエイリアスを追加する際の予期しない動作
間違い:
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 がもたらすもの:
- リソースが Pulumi コンソールでコンポーネント下にネストされて表示される
- コンポーネントを削除するとすべての子が削除される
- コンポーネント上のエイリアスが自動的に子に適用される
- 状態ファイルで所有権が明確である
参考: https://www.pulumi.com/docs/concepts/resources/components/
5. 初日からシークレットを暗号化する
理由: --secret でマークされたシークレットは状態ファイルで暗号化され、CLI 出力でマスクされ、変換を通じて追跡されます。プレーンテキスト設定で開始して後で変換すると、認証情報のローテーション、参照の更新、ログと状態履歴でのリークした値の監査が必要になります。
検出シグナル:
- パスワード、API キー、トークンがプレーンテキスト設定として保存される
- 埋め込み認証情報を含む接続文字列
- プレーンテキストの秘密鍵または証明書
間違い:
# プレーンテキスト - 状態とログに表示される
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"
シークレットの条件:
- パスワードとパスフレーズ
- API キーとトークン
- 秘密鍵と証明書
- 認証情報を含む接続文字列
- OAuth クライアント シークレット
- 暗号化キー
参考:
6. リファクタリング時にエイリアスを使用する
理由: リソースの名前変更、コンポーネントへの移動、または親の変更により、Pulumi はそれらを新しいリソースとして認識します。エイリアスなしにリファクタリングするとリソースが破棄および再作成され、ダウンタイムやデータ損失の可能性があります。エイリアスはリファクタリングを通じてリソースアイデンティティを保持します。
検出シグナル:
- エイリアスなしのリソース名変更
- ComponentResource の内外へのリソース移動
- リソースの親の変更
- Preview が更新を意図していた場合の delete+create の表示
間違い:
// Before: "my-bucket" という名前のリソース
const bucket = new aws.s3.Bucket("my-bucket");
// After: エイリアスなしで名前変更 - バケットを破棄する
const bucket = new aws.s3.Bucket("application-bucket");
正しい例:
// After: エイリアスで名前変更 - 既存バケットを保持
const bucket = new aws.s3.Bucket("application-bucket", {}, {
aliases: [{ name: "my-bucket" }],
});
コンポーネントへの移動:
// Before: トップレベルのリソース
const bucket = new aws.s3.Bucket("my-bucket");
// After: コンポーネント内 - 古い親とのエイリアスが必要
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を実行 - すべてのスタック更新後、エイリアスを削除(オプション、ただしコードをきれいに保つ)
参考: https://www.pulumi.com/docs/iac/concepts/resources/options/aliases/
7. すべてのデプロイ前に Preview を実行する
理由: pulumi preview は作成、更新、または破棄される内容を正確に表示します。プレビューをスキップすることで本番環境での予期しない事態が発生します。リソースが「更新」を予想していた場合に「置換」を表示しているということは、破棄と再作成が近いということです。
検出シグナル:
- 変更を確認せずに対話的に
pulumi up --yesを実行 - 特定の変更に対してCI/CD ワークフロー内のどこにもプレビューステップがない
- マージまたはデプロイ承認前にプレビュー出力がレビューされていない
間違い:
# 盲目的なデプロイ
pulumi up --yes
正しい例:
# 常に最初にプレビュー
pulumi preview
# 出力をレビューしてからデプロイ
pulumi up
Preview 出力で確認すべき項目:
+ 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 ワークフロー:
- すべての PR でプレビューを実行
- PR コメントとしてプレビュー出力を投稿
- マージ前にプレビューレビューを必須にする
- メインへのマージ後のみデプロイ
参考:
- 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 を直接渡す | 抽出された値が入力として使用される | Output オブジェクト、pulumi.interpolate を使用 |
| コンポーネントを使用 | フラット構造、繰り返されるパターン | ComponentResource クラスを作成 |
| parent: this を設定 | コンポーネントの子がルートレベルにある | すべての子リソースに { parent: this } を渡す |
| 初日からシークレット | config 内のプレーンテキストパスワード/キー | --secret フラグ、ESC を使用 |
| リファクタリング時にエイリアス | Preview での delete+create | 古い名前/親を使用してエイリアスを追加 |
| デプロイ前に Preview | pulumi up --yes | 常に最初に pulumi preview を実行 |
検証チェックリスト
Pulumi コードをレビューする際、以下を確認してください:
-
apply()コールバック内にリソース コンストラクタがない - Output が依存リソースに直接渡される
- 関連リソースが ComponentResource クラスでグループ化されている
- 子リソースに
{ parent: this }がある - 機密値が
config.requireSecret()または--secretを使用している - リファクタリングされたリソースがアイデンティティを保持するエイリアスを持っている
- デプロイプロセスにプレビューステップが含まれている
関連スキル
- pulumi-component: ComponentResource クラスの作成、引数インターフェースの設計、多言語サポート、テスト、配布に関する詳細なガイド。スキル
pulumi-componentを使用してください。 - pulumi-automation-api: 複数スタックのプログラマティック調整。スキル
pulumi-automation-apiを使用してください。 - pulumi-esc: 集中管理されたシークレットと設定管理。スキル
pulumi-escを使用してください。
ライセンス: Apache-2.0(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- pulumi
- リポジトリ
- pulumi/agent-skills
- ライセンス
- Apache-2.0
- 最終更新
- 不明
Source: https://github.com/pulumi/agent-skills / ライセンス: Apache-2.0
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。