【Pulumi地獄からの脱出】数千リソースを従える巨大スタックの限界を突破する、極限のパフォーマンスチューニング
我々は日々、インフラをコード化(IaC)し、宣言的インフラストラクチャの美しさに酔いしれてきた。しかし、管理するリソースが数千、数万規模に膨れ上がったとき、その「美しさ」は残酷な現実へと変貌する。
`pulumi preview` を叩いてからコーヒーを淹れに行き、戻ってきてもまだグラフの走査が終わっていない。CI/CDパイプラインはタイムアウトの嵐。モジュール分割をサボった結果、たった一つのタグ変更のために全リソースのメタデータがフェッチされ、APIレートリミットに叩き落とされる――。
この地獄から抜け出すためには、公式ドキュメントの表層をなぞるだけでは無力だ。本稿では、Pulumiの内部アーキテクチャの挙動を解剖し、大規模スタックにおけるボトルネックを根絶するための極限の知見を授ける。
—
1. 敵を知る:なぜ Pulumi は巨大化すると遅くなるのか?
パフォーマンスチューニングの鉄則は、ボトルネックの定量化だ。`pulumi preview` や `pulumi up` が遅くなる原因は、主に以下の3点に集約される。
1. リソースグラフの直列化と依存関係の過剰検知
2. 言語ランタイム(Node.js / Python / Go等)と Pulumi Engine 間の IPC(プロセス間通信)のオーバーヘッド
3. プロバイダプラグインによるクラウドAPIへの過剰なリクエスト(Refreshフェーズの呪い)
特に Python や TypeScript を用いる場合、言語ランタイム側のメモリ消費とガベージコレクション(GC)の停止時間が、エンジン側の処理を大きくブロックする。まずは、この不可視の遅延を可視化することから始めよう。
診断コマンド:何が時間を食っているのか?
タイム測定には、環境変数 `PULUMI_DEBUG_GOTO` やログレベルの最大化を活用するが、最も手っ取り早いのはトレース情報の取得だ。
実行時間をプロファイルするためにログを有効化
PULUMI_LOG_LEVEL=debug PULUMI_CONFIG_PASSPHRASE=”your-passphrase” pulumi preview –logtostderr -v=9 2> pulumi_debug.log
このログから、どのプロバイダのどのリソースの `Diff` や `Check` に時間がかかっているかを特定せよ。
—
2. プロバイダ並列度(Parallelism)の極限チューニング
Pulumiのエンジンはデフォルトで並列処理を行っているが、デフォルト値(通常は `8` や `16`)は大規模インフラに対しては保守的すぎる。あるいは逆に、クラウドプロバイダ(AWS, GCP, Azure)のAPIレートリミットを自ら踏み抜き、指数バックオフによるスロットリング地獄に陥っているケースがほとんどだ。
並列度の動的制御と環境変数
スタックの性質(I/Oバウンドか、CPUバウンドか)に合わせて、`PULUMI_PARALLELISM` を調整する。数千リソースを扱う場合、これを `64` や `128` に引き上げることが有効な場合がある。
例: 並列度を 64 に強制設定してプレビューを実行
PULUMI_PARALLELISM=64 pulumi preview
【警告】 ただし、AWS CloudFormation / AWS API のようなスロットリングが厳しい環境で並列度を無闇に上げると、`Throttling: Rate exceeded` エラーが頻発する。プロバイダ側のリトライ回数とバックオフ設定をコード側で明示的にチューニングする必要がある。
// TypeScriptの例: AWSプロバイダのタイムアウト・リトライ設定の最適化
import as aws from “@pulumi/aws”;
// プロバイダインスタンスを明示的に作成し、リトライ回数を拡張
const customProvider = new aws.Provider(“custom-aws”, {
region: “us-east-1”,
maxRetries: 10, // デフォルトより多くリトライさせる
});
—
3. 依存関係の最適化:暗黙的依存(Implicit Dependencies)の排除
大規模スタックで最もやってはいけないアンチパターンが、「不要な暗黙的依存関係の連鎖」だ。
例えば、VPC内の数百個のサブネットやセキュリティグループが、単一のルートVPCオブジェクトの出力(Outputs)を直接参照していると、Pulumiエンジンは「VPCに変更がないか」を全リソースの `Diff` 計算時に走査し続ける。
解決策:`dependsOn` と `ignoreChanges` の戦略的活用
1. 明示的依存の最小化: データが本当に必要な場合を除き、文字列の補間やオブジェクト全体の参照を避ける。
2. `ignoreChanges` による不要な差分計算のスキップ:
変更頻度の高いアトリビュート(例: オートスケーリンググループの現在のインスタンス数、Kubernetesマニフェストのタイムスタンプなど)は、プレビューの計算コストを無駄に食う。
import as aws from “@pulumi/aws”;
const server = new aws.ec2.Instance(“web-server”, {
ami: “ami-12345678”,
instanceType: “t3.medium”,
// 頻繁に変動するタグやメタデータを差分計算から除外
}, {
ignoreChanges: [“tags[\”LastDeployedAt\”]”, “userData”],
});
—
4. 状態ファイル分割の設計パターン:モノリスからの脱却
数千リソースを単一の Pulumi Stack(単一の State File)で管理しているならば、今すぐそのアーキテクチャを破壊し、垂直・水平分割を行え。
単一ステートが数MB〜数十MBを超えると、バックエンド(Pulumi Service, S3, GCS等)からのロック取得、暗号化/復号、そしてステートのJSONパース自体がボトルネックになる。
設計パターン:レイヤード・スタック・アーキテクチャ
インフラストラクチャを以下の階層に完全に分離し、Stack References を介して疎結合に連携させる。
[Layer 0: Global / Network] (VPC, Subnets, DNS) -> 変更頻度: 極めて低
│
▼ (StackReference)
[Layer 1: Shared Services] (IAM, EKS Cluster, DB) -> 変更頻度: 低
│
▼ (StackReference)
[Layer 2: Workloads] (K8s Deployments, Lambdas) -> 変更頻度: 高
実装例:Stack Reference による安全な値の共有
親スタック(Network)の出力を、子スタック(Workloads)から高速かつ安全に参照する。これにより、子スタックのプレビュー時はネットワーク層の膨大なリソースグラフを走査する必要がなくなる。
// 子スタック(Workloads)側のコード
import as pulumi from “@pulumi/pulumi”;
// 別スタックの参照をロード(ローカルのステートファイルを読まないため高速)
const networkStack = new pulumi.StackReference(“org/network/production”);
const vpcId = networkStack.requireOutput(“vpcId”);
const subnetIds = networkStack.requireOutput(“privateSubnetIds”);
// この層のプレビューはVpc自体の差分計算を一切行わないため、爆速で完了する
—
5. 【極限自動化】APIとCLIを叩く独自自動化スクリプトによるパイプライン最適化
CI/CDパイプラインにおいて、毎回すべての言語ランタイムを初期化し、全プラグインをダウンロード・検証している時間は無駄だ。
Pulumi Automation API を活用し、並列プレビューの制御、キャッシュの極限利用、不要なログ出力の抑制を行う「自製ランナー」を構築せよ。
以下は、Node.jsベースのAutomation APIを用いて、複数スタックのプレビューを完全並列実行し、ボトルネックを自動検出する高信頼スクリプトの断片である。
import as pulumi from “@pulumi/pulumi/automation”;
import as fs from “fs”;
async function runOptimizedPreview(stackName: string, workDir: string) {
console.log(`[START] Stacking preview for: ${stackName}`);
const startTime = Date.now();
try {
const s = await pulumi.LocalWorkspace.selectStack({
stackName: stackName,
workDir: workDir,
});
// プレビューの実行(環境変数で並列度を強制注入)
const res = await s.preview({
onOutput: (out) => {
// 必要に応じてストリーム出力を制御
},
envVars: {
“PULUMI_PARALLELISM”: “64”,
“PULUMI_SKIP_UPDATE_CHECK”: “true”, // 更新チェックを無効化して起動を高速化
}
});
const duration = (Date.now() – startTime) / 1000;
console.log(`[SUCCESS] ${stackName} preview completed in ${duration}s. Changes: ${JSON.stringify(res.changeSummary)}`);
} catch (e) {
console.error(`[ERROR] Failed preview for ${stackName}:`, e);
process.exit(1);
}
}
// 複数スタックの並列実行制御
async function main() {
const stacks = [“prod-network”, “prod-database”, “prod-compute”];
await Promise.all(stacks.map(st => runOptimizedPreview(st, “./infra”)));
}
main();
パイプライン高速化のためのハック集
1. `PULUMI_SKIP_UPDATE_CHECK=true` の常時設定
Pulumi CLIは起動時に毎回GitHubから最新バージョンの確認を行う。これを切るだけで、数百のタスクを回すCI環境では数秒の短縮になる。
2. プラグインの事前キャッシュ(Pre-caching)
CIコンテナイメージの中に、使用する全プロバイダプラグイン(`aws`, `kubernetes`, `azure` 等)のバイナリを焼き込んでおけ。CI実行時の `pulumi plugin install` をゼロにしろ。
3. リモート評価(Remote Evaluation / Pulumi Service Backend)の活用
巨大なステートとローカルのメモリ不足に悩むなら、計算をローカルマシンやCIランナーにやらせるのをやめ、Pulumi Service(SaaS)側のインフラ上でプレビュー・アップデートを実行する機能(Service-driven deployments)へ移行せよ。ローカルのCPUとメモリは完全に解放される。
—
結び:インフラストラクチャエンジニアとしての誇り
数千のリソースを扱う大規模インフラのパフォーマンスチューニングは、単なる「待ち時間の短縮」ではない。それは、システム全体の複雑性をエンジニアリングの力で制御下置き、デプロイの心理的安全性を極限まで高めるという、SREの核心そのものである。
プロバイダの挙動を読み解き、依存関係を断ち切り、アーキテクチャを美しく分割せよ。
あなたの手にあるPulumiは、もはや重い鉄の塊ではなく、光速のインフラ自動化エンジンへと生まれ変わるはずだ。