【テクニカル・上級編】Pulumiのデプロイを爆速化する並列処理とキャッシング戦略:大規模インフラ管理の最適解 – インフラ構成管理(IaC)活用バイブル

Pulumi巨大インフラの呪縛を断つ:数千リソースを秒速でねじ伏せる並列処理とキャッシングの極限最適化

インフラストラクチャ・アズ・コード(IaC)の規模が数千、数万リソースに達した瞬間、すべてのエンジニアは同じ悪夢を見る。
`pulumi up` を叩いた後、コーヒーを淹れに行き、戻ってきてもなお `Refreshing…` のプログレスバーが微動だにしない絶望の待ち時間。ステートのロック、依存関係の解決に費やされるCPUサイクル、そして無駄なAPIコールによるレートリミットの嵐。

Terraformのグラフ構造の限界に絶望し、TypeScriptやPythonの表現力に救いを求めた者たちが次に直面するのが、この「スケールの壁」だ。

だが、言っておこう。Pulumiは遅くない。使い方を間違っているだけだ。

本稿では、数千リソース規模の巨大なPulumiプロジェクトにおいて、デプロイおよびプレビューの速度を理論値の限界まで引き上げるための「非同期処理の極限活用」「スタック分割のトポロジー設計」「言語ランタイム別のメモリ・CPUチューニング」を、内部アーキテクチャの深淵から解き明かす。

—

1. 内部アーキテクチャの理解:なぜ巨大プロジェクトは失速するのか?

Pulumiのエンジンとプロバイダ(Provider)の通信は、gRPCベースで行われる。
各リソースのライフサイクル(Create, Read, Update, Delete)は、DAG(有向非巡回グラフ)の依存関係に基づいて評価されるが、デフォルト設定のままでは以下のボトルネックが確実に発生する。

1. シリアル化された評価の罠: コード内の非同期I/OやPromise/Futureのハンドリングを誤ると、実質的に同期処理となり、CPUコアが遊んでいるのにデプロイが進まない。
2. プランニング(Preview)時のAPIラウンドトリップ過多: `pulumi preview` は、既存クラウドリソースの状態をクラウドプロバイダのAPIを叩いて確認する。数千リソースある場合、この直列的なRead処理だけで数分を消費する。
3. ランタイムのGCとメモリプレッシャー: Node.js (V8) や Python のランタイムが、数万個の資源オブジェクト(Resource Objects)をメモリ上に保持することで、ガベージコレクション(GC)の停止時間が爆発的に延びる。

これらを粉砕するための具体的なアプローチをコードベースで示そう。

—

2. 非同期処理の極限最適化(TypeScript / Node.jsの例)

数千のVPCサブネット、セキュリティグループ、IAMロールを動的に生成する際、標準的な `for` ループや不適切な `Promise.all` の使い方はパフォーマンスを殺す。V8エンジンのイベントループを飽和させず、かつ最大限の並列度を引き出す実装パターンがこれだ。

以下のコードは、数千個のS3バケットやリソースを「バッチ処理」かつ「非同期の並列度制御(Concurrency Control)」を利かせて爆速でプロビジョニングする設計パターンである。

import as aws from “@pulumi/aws”;
import as pulumi from “@pulumi/pulumi”;

// 並列度を制御するためのヘルパー関数(p-limitのコンセプトをPulumi用に最適化)
async function mapConcurrent(
items: T[],
concurrency: number,
fn: (item: T, index: number) => Promise
): Promise {
let index = 0;
const results: U[] = new Array(items.length);
const executing: Promise[] = [];

const enqueue = async (): Promise => {
if (index === items.length) return;
const currentIndex = index++;
const item = items[currentIndex];

// 各タスクを実行し、完了したら配列に結果を格納
results[currentIndex] = await fn(item, currentIndex);

// 実行中プールから自身を削除し、次のタスクを呼び出す
const currentPromise = executing.indexOf(p);
if (currentPromise !== -1) {
executing.splice(currentPromise, 1);
}
await enqueue();
};

const p = fn(items[index], index); // プレースホルダー
// 指定された並列度(concurrency)まで同時にタスクを走らせる
for (let i = 0; i < Math.min(concurrency, items.length); i++) { executing.push(enqueue()); } await Promise.all(executing); return results; } // 活用例:2000個のマイクロサービス用KMSキーを爆速で生成 async function createBulkEncryptedBuckets() { const serviceCount = 2000; const serviceIds = Array.from({ length: serviceCount }, (_, i) => `service-node-${i}`);

// クラウドプロバイダのAPIレートリミットを考慮し、並列度を「50」にハードリミット設定
// 無制限に並列化するとAWS SDK側でThrottlingException (429) が発生し、リトライで逆に遅くなる。
const concurrencyLimit = 50;

console.time(“BulkResourceCreation”);

await mapConcurrent(serviceIds, concurrencyLimit, async (id, index) => {
// KMSキーの作成
const key = new aws.kms.Key(`key-${id}`, {
description: `KMS key for ${id}`,
deletionWindowInDays: 7,
enableKeyRotation: true,
});

// S3バケットの作成とKMS紐付け
const bucket = new aws.s3.BucketV2(`bucket-${id}`, {
bucket: `enterprise-data-store-${index}-${id}`,
});

new aws.s3.BucketServerSideEncryptionConfigurationV2(`enc-${id}`, {
bucket: bucket.id,
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: “aws:kms”,
kmsMasterKeyId: key.arn,
},
},
});

return bucket.id;
});

console.timeEnd(“BulkResourceCreation”);
}

💡 エキスパートの知見:スロットリングの制御

AWSやAzureなどのパブリッククラウドAPIには必ずレートリミットが存在する。Pulumiの並列度を無限に上げても、プロバイダ側で `429 Too Many Requests` が多発し、内部的な指数バックオフ(Exponential Backoff)リトライが発動して全体がスロースタートになる。
「プロバイダの限界値の8割」を維持する並列度チューニングこそが、デプロイを最速にする黄金律である。

—

3. スタック分割のトポロジー設計:モノリスからの脱却

数千リソースを1つのPulumiスタック(単一のStateファイル)で管理することは、たとえ非同期処理を極めても、アルゴリズムの限界(Stateのパース、ロック競合、メモリ消費)に突き当たる。

真にスケーラブルなインフラストラクチャは、「コンポジット・スタック・アーキテクチャ(Composite Stack Architecture)」によって構築されるべきである。

[Global Network Stack (VPC / Transit Gateway)]
│ (Outputs: Subnet IDs, VPC ID via Pulumi StackReference)
▼
[Data Tier Stack (RDS / ElastiCache / S3)]
│ (Outputs: DB Endpoints)
▼
[Compute Tier Stack (EKS / ECS / Lambda – 幾千ものリソース)]

StackReferenceによる疎結合な高速化

スタックを分割することで、変更のないネットワーク層やデータベース層のプレビュー/アップデートを完全にスキップできる。

import as pulumi from “@pulumi/pulumi”;

// 別スタック(Network Tier)からの参照。APIコールを伴わず、Pulumi Service / State Backend から一瞬で値を取得
const networkStackRef = new pulumi.StackReference(“my-org/network/production”);
const vpcId = networkStackRef.requireOutput(“vpcId”);
const privateSubnetIds = networkStackRef.requireOutput(“privateSubnetIds”);

// コンピューート層のみを高速にデプロイ

これにより、数千リソースの変更であっても、依存関係の計算グラフが最小化され、`pulumi preview` の実行時間が数分単位から数秒単位へと激変する。

—

4. キャッシング戦略とCI/CDパイプラインの極限チューニング

デプロイ速度をボトルネックにしているもう一つの要因は、「プラグインのダウンロード」「依存関係のインストール(npm install / pip install)」、そして「エンジンバイナリの初期化」である。

GitHub ActionsやGitLab CIなどのCI/CD環境において、これらを完全にキャッシュし、ゼロからビルドする無駄を排除する。

最強の GitHub Actions ワークフロー設定例

name: “Lightning Fast Pulumi Deploy”

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest
steps:

  • name: Checkout Repo

uses: actions/checkout@v4

# 1. Node.jsのセットアップとキャッシュの完全同期

  • name: Setup Node.js

uses: actions/setup-node@v4
with:
node-version: ’20’
cache: ‘npm’

# 2. Pulumi CLIのキャッシュおよびインストール高速化

  • name: Install Pulumi CLI

uses: pulumi/action-install-pulumi@v3
with:
pulumi-version: ‘latest’

# 3. Pulumiプラグインのキャッシュディレクトリを固定し、CIキャッシュに乗せる
# デフォルトでは ~/.pulumi/plugins に入るため、ここをターゲットにする

  • name: Cache Pulumi Plugins

uses: actions/cache@v4
with:
path: ~/.pulumi/plugins
key: ${{ runner.os }}-pulumi-plugins-${

タイトルとURLをコピーしました