【テクニカル・上級編】Pulumiプログラムの高速化手法:並行処理制御(Concurrency)とリソース依存関係(DependsOn)の正しい設計 – インフラ構成管理(IaC)活用バイブル

Pulumi超高速化の深淵:並行処理制御と`dependsOn`の呪縛を断ち切るアーキテクチャ設計

インフラストラクチャ・アズ・コード(IaC)の領域において、デプロイメントの遅延はエンジニアリング組織の死活問題だ。Terraformのプランニング・applyのノロノロとした動作に絶望し、次世代のパラダイムとしてPulumiを選択したあなたなら、TypeScript、Python、Goといった汎用プログラミング言語の表現力がいかに強力であるかを理解しているはずだ。

しかし、ここで問いかけたい。
「お前のPulumiプログラム、本当に並行処理されているか?」

「宣言的にリソースを書けば、勝手に並行でデプロイされる」という甘い幻想を抱いているなら、今すぐその認識を改めよ。グラフ評価のメカニズムと`Output`の伝播を正しく理解していなければ、あなたの書いたコードは実質的にシングルスレッドの逐次処理と化し、APIのレートリミットを無駄に踏み荒らし、CI/CDパイプラインを数十分も停滞させている。

本稿では、Pulumiの内部アーキテクチャの深部(エンジンとプロバイダ間のRPC通信、グラフ評価モデル)にメスを入れ、デプロイ時間を極限まで圧縮するための「並行処理制御」と「依存関係(`dependsOn`)のアンチパターン打破」に関する極限の知見を授ける。

—

1. Pulumiエンジンと非同期グラフ評価のメカニズム

Pulumiの核心は、プログラムの実行とリソースのプロビジョニングを仲介するPulumiエンジンにある。

あなたが書いたPulumiコードを実行すると、言語ランタイム(Node.js, Python等)は直接クラウドAPIを叩くわけではない。コードは gRPC を介して Pulumiエンジンと通信し、Resource Graph(リソース依存関係グラフ)を構築する。

評価フェーズの正体

1. ランタイム評価フェーズ (Runtime Evaluation): プログラムが上から順に実行され、リソースのインスタンス化(`new aws.s3.Bucket(…)` など)が行われる。この時点では、クラウド上のリソースはまだ作成されていない。
2. プレビュー / アップデートフェーズ (Engine Orchestration): エンジンは構築された依存関係グラフを解析トポロジカルソートし、依存関係のないリソースを完全な並行非同期(Async/Awaitモデル)でプロバイダプラグインにディスパッチする。

ここで重要なのは、「並行性の限界は、プログラム側が定義した依存関係のグラフ構造によってのみ決まる」という点だ。エンジン自体は可能な限りすべてを同時に作ろうとするが、私たちが無知ゆえに不必要な直列化(ボトルネック)をコードに埋め込んでしまっているのである。

—

2. 不要な明示的 `dependsOn` の多用がパフォーマンスを落とす理由

初心者が陥る最大の罠、それは「何となく不安だから」という理由で `dependsOn: […]` を乱用することだ。

アンチパターンの例:不毛な依存関係の連鎖

// 最悪な例:すべてのリソースが一本の鎖で繋がれている
const vpc = new aws.ec2.Vpc(“main”, { cidrBlock: “10.0.0.0/16” });

const subnetA = new aws.ec2.Subnet(“subnet-a”, {
vpcId: vpc.id,
cidrBlock: “10.0.1.0/24”,
}, { dependsOn: [vpc] }); // 【悪手】vpc.idを参照している時点で依存関係は暗黙的に解決される

const subnetB = new aws.ec2.Subnet(“subnet-b”, {
vpcId: vpc.id,
cidrBlock: “10.0.2.0/24”,
}, { dependsOn: [subnetA] }); // 【大罪】subnetAとsubnetBに依存関係の必然性はないのに直列化されている

なぜこれがパフォーマンスを殺すのか?

1. 暗黙的依存関係の自動解決の破壊: Pulumiは、あるリソースのプロパティ(例: `vpc.id`)が別のリソースの入力として使われている場合、自動的に暗黙の依存関係グラフを構築する。したがって、`vpc.id`を渡している時点で `dependsOn: [vpc]` は完全に冗長である。
2. 並行度の殺害: `subnetB` に `dependsOn: [subnetA]` を指定した瞬間、エンジンは「`subnetA` の作成が100%完了するまで `subnetB` の作成を開始してはならない」と強制される。AWSのAPIは独立したサブネットの同時作成を許容しているにもかかわらず、自らパイプラインをシングルスレッド化しているのだ。

無駄な `dependsOn` は、グラフの幅(同時に実行できるタスクの数)を狭め、デプロイ時間を線形に増大させる。

—

3. `Output` の連鎖を最適化し、デプロイ時間を劇的に短縮するコードリファクタリング術

Pulumiの性能を極限まで引き出すには、`pulumi.Output` の特性を完全に理解し、非同期データの伝送路を最適化する必要がある。

悪臭を放つコード:不必要な `apply` のネスト

非同期値を取り出そうとして `output.apply()` を無駄にネストさせると、グラフの評価が遅延し、エンジンのスケジューリング効率が落ちる。

// 悪い例:逐次的なOutputの解決
const db = new aws.rds.Instance(“db”, { / … / });

// DBのエンドポイントを別のリソースで使いたいがために apply を深くネストさせる
const appServer = db.endpoint.apply(endpoint => {
// この中でさらに別のリソースを定義してはいけない!
// グラフの動的構築が阻害され、エンジンの最適化が効かなくなる
return new aws.ec2.Instance(“app”, {
userData: `DB_HOST=${endpoint}`,
// …
});
});

覚醒したコード:`Output` のネイティブバインディングと並行配列処理

リソースの入力プロパティには、`Output` を直接渡すことができる。`apply` の内部でリソースを生成するのではなく、`Output` を流し込む形にリファクタリングせよ。

// 模範解答:Outputの直接伝播と動的リソースの並行生成
const vpc = new aws.ec2.Vpc(“main”, { cidrBlock: “10.0.0.0/16” });

// 複数のアベイラビリティゾーンに対するサブネット動的生成
const azs = [“ap-northeast-1a”, “ap-northeast-1c”, “ap-northeast-1d”];

const subnets = azs.map((az, index) => {
// 明示的dependsOnなし。vpc.idを通じた暗黙的依存のみ。
// これにより、3つのサブネットは完全に並行(Async)でAWS APIにリクエストされる。
return new aws.ec2.Subnet(`subnet-${az}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${index}.0/24`,
availabilityZone: az,
});
});

このアプローチにより、Pulumiエンジンは3つのサブネット作成リクエストを同時にAWSへ発射し、ネットワークI/Oの待ち時間を最小化する。

—

4. 大規模プロジェクトでの実践的チューニング事例

数百から数千のリソースを抱えるエンタープライズ環境(マイクロサービス基盤、マルチテナントK8sクラスター等)における、実戦投入済みのチューニングハックを公開する。

ハック1: プロバイダのパラレリズム制限(Rate Limiting)の回避

数千のS3オブジェクトやIAMポリシーを同時に作成しようとすると、クラウドプロバイダ側のAPIレートリミット(Throttling / 429 Too Many Requests)に激突し、Pulumiのリトライロジックによってかえってデプロイが遅延する。

これを防ぐため、TypeScriptの `p-limit` や言語ネイティブのセマフォパターンを使い、論理的な並行度をコントロールしつつ、エンジンには負荷をかけない設計を取り入れる。

import as pulumi from “@pulumi/pulumi”;
import as aws from “@pulumi/aws”;
import { promisify } from “util”;

// 大量のIAMロールを安全かつ高速に生成する例
const policyArns = [/ 50個のポリシーARN /];

// ループで一気に生成するのではなく、バッチまたはストリーム状に流し込むことで
// プロバイダプラグインのgRPCストリーム飽和を防ぐ
const roles = policyArns.map((arn, i) => {
return new aws.iam.Role(`role-${i}`, {
assumeRolePolicy: “…”,
// 暗黙的依存により、このロール作成は並行処理されるが、
// 適切なチャンク分割を行うことでAPIスロットリングを回避可能
});
});

ハック2: スタックの分割とアーキテクチャの隔離

ひとつのPulumiプログラムに全リソース(VPC、DB、K8s、App)を詰め込むのは、グラフ評価のメモリ消費量を爆発させ、プレビュー時間を数分に引き延ばす最大のガンだ。

  • Root Stack: VPC、基盤ネットワーク
  • Data Stack: RDS、ElastiCache(Root Stackの出力を `pulumi.StackReference` で取得)
  • App Stack: ECS / EKS Workloads

この階層化により、変更頻度の低い基盤レイヤーのグラフ評価を日々のデプロイから切り離し、App Stackの評価スコープを最小化することで、プレビュー時間を < 3秒 に維持できる。

—

エピローグ:技術至上主義者へのメッセージ

自動化とは、ただ動くコードを書くことではない。ミリ秒単位の効率を削り出し、パイプラインの歯車を極限まで噛み合わせることだ。

あなたがこれまで何気なく書いていた `dependsOn` は、本当に必要だったか?
`apply` の中でリソースを生成し、非同期のフローを自ら破壊していなかったか?

今日のこの瞬間から、あなたのPulumiコードのグラフ構造を見直せ。エンジンを信じ、依存関係を最小限に絞り込み、完全なる並行処理の奔流を解き放て。それこそが、真のSRE、真のインフラストラクチャ・アーキテクトの仕事である。

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