【テクニカル・上級編】Pulumiでよくあるエラー10選と解決策:デプロイ失敗時のトラブルシューティング – インフラ構成管理(IaC)活用バイブル

Pulumi地獄からの生還:デプロイ失敗を完全制圧する10の極限知見

インフラストラクチャをTypeScriptやPython、Goといった汎用プログラミング言語で記述できるPulumiは、TerraformのHCL(HashiCorp Configuration Language)の表現力の限界に絶望した我々SREにとって救世主であった。条件分岐、ループ、型安全性、そして既存のテストフレームワークの流用。これらはIaCのパラダイムシフトをもたらした。

しかし、「コードで書ける」ということは、「コードを書く人間が犯すあらゆるバグや、言語ランタイムの挙動、そしてクラウドプロバイダの暗部を直接踏み抜く」ことを意味する。HCLであれば静的解析の段階で弾かれたであろう矛盾が、ランタイムのエラーやステートの破損として容赦なく牙を剥く。

本稿では、数々の修羅場をくぐり抜けてきたインフラストラクチャの現場において、Pulumiのデプロイメントパイプラインが陥る代表的な地獄と、それを秒速で鎮圧するための「骨の髄まで掌握した者」の知見を全公開する。

—

1. 認証・権限エラー:IAMの迷宮とSDKプロバイダの挙動差分

地獄の症状

`pulumi up` を実行した瞬間、AWSなら `AccessDenied`、GCPなら `PermissionDenied` が突如として爆発する。ローカル環境では成功するのに、GitHub Actions等のCI/CDパイプライン上だけでこのエラーが発生し、原因究明に何時間も溶かす。

エキスパートの解決策

Pulumiの認証は、「Pulumi CLI自体のバックエンド認証」と「各クラウドプロバイダのSDKが裏で要求する認証」の2階建てになっていることをまず理解しろ。多くのエンジニアがこれを混同する。

特にCI環境では、環境変数のリークや、OIDC(OpenID Connect)のクレデンシャルプロバイダ設定ミスが頻発する。TypeScriptでプロバイダを明示的に初期化し、デバッグログを最大化するコードパターンを標準化せよ。

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

// 明示的にプロバイダの設定を行い、どのクレデンシャルが使われているかをコード側で担保する
// 暗黙的な環境変数依存(AWS_ACCESS_KEY_ID等)は、マルチアカウント環境のガンである。
const targetProvider = new aws.Provider(“explicit-us-east-1”, {
region: “us-east-1”,
// AssumeRoleを使う場合の鉄板パターン
assumeRole: {
roleArn: “arn:aws:iam::123456789012:role/DeploymentPipelineRole”,
sessionName: “PulumiDeploymentSession”,
},
});

// プロバイダをリソースに明示的にバインド
const myBucket = new aws.s3.Bucket(“my-secure-bucket”, {}, { provider: targetProvider });

> SREの極意: `PULUMI_DEBUG_GKE=true` や `AWS_SDK_LOAD_CONFIG=1` を仕込み、プロバイダがどの設定ファイルをどの順序で読み込んでいるのか、標準エラー出力のトレースを血眼になって読め。迷ったら `pulumi preview –logtostderr –log-flow` だ。

—

2. 依存関係(DependsOn)の崩壊:暗黙的依存の罠と明示的強制

地獄の症状

リソースAの作成完了を待たずにリソースBが作られ、API側で `ResourceNotFound` や `DependencyViolation` が発生する。Pulumiはリソース間の入出力(Output)を解析して依存関係を自動構築するが、「値の参照はないが、論理的な順序が必要なケース」(例: IAMポリシーアタッチとインスタンスプロファイルの伝播遅延)で必ずこのバグを踏む。

エキスパートの解決策

Outputの非同期評価モデルを理解していないとここにハマる。TypeScriptの `pulumi.all` や `apply` を駆使し、明示的な依存関係 (`dependsOn` オプション) を強制しろ。

import as aws from “@pulumi/aws”;

const role = new aws.iam.Role(“my-role”, {
assumeRolePolicy: “…”,
});

const policy = new aws.iam.Policy(“my-policy”, {
policy: “…”,
});

// 【地獄パターン】policyのARNをroleに渡していないため、Pulumiはこれらを並行(あるいは順序不定)で作成しようとする。
// 【解決策】dependsOnを明示し、論理的順序を保証する。
const rolePolicyAttachment = new aws.iam.RolePolicyAttachment(“rpa”, {
role: role.name,
policyArn: policy.arn,
}, { dependsOn: [role, policy] });

// さらに、IAMの伝播遅延(Eventual Consistency)対策として、
// リソース作成後のバリア(Null Resource的な待機)を挟むのが真のプロの技である。

—

3. タイムアウトとレートリミット(429 Too Many Requests)の制約突破

地獄の症状

マイクロサービスの爆発的デプロイ時、数千個のKubernetesマニフェストやクラウドナレッジが一斉にAPIを叩き、クラウドプロバイダのAPI GatewayやKubernetes API Serverから `429 Too Many Requests` や `Context Deadline Exceeded` が返される。

エキスパートの解決策

デフォルトの並行度(Concurrency)のまま本番環境に特攻してはならない。Pulumiエンジンはデフォルトで並行処理を行うため、APIのスロットリング上限を容易に超越する。

環境変数、またはPulumiのスタック設定で並行度を制御しつつ、プロバイダレベルでのリトライ戦略をチューニングせよ。

環境変数で最大並行数を強制的に絞る(例: 同時実行数を10に制限)
export PULUMI_MAX_PARALLEL=10
pulumi up –yes

プログラム側では、プロバイダのタイムアウトを延長する。

import as aws from “@pulumi/aws”;

// 重いリソース(RDSやK8sクラスターなど)の作成にはタイムアウトを明示的に指定
const cluster = new aws.eks.Cluster(“heavy-cluster”, {
// …設定…
}, {
timeout: “1h”, // デフォルトでは足りないケースが多い
});

—

4. スタックロックの呪い:スタックステートの完全強制解放

地獄の症状

CI/CDジョブがOOM Killerに刈り取られたり、ネットワーク切断で途中で死んだりした際、スタックが `locked` 状態のまま凍結する。以後、すべての `pulumi up` が拒絶され、パイプラインが完全停止する。

エキスパートの解決策

焦ってPulumi ServiceのWebコンソールをポチポチしてはならない。CLIの低レイヤコマンドで強制解除(Break Lock)を実行する。

ロックを強制解除する(※他のプロセスが動いていないことを確認してから実行すること!)
pulumi stack export –show-secrets > state_backup.json
pulumi stack rm-lock –force

万が一ステート自体が破損した場合は、JSONを直接修正してインポートし直す修羅場をくぐる覚悟を持て
pulumi stack import < repaired_state.json > SREの警鐘: ステートのロック強制解除は「核のボタン」だ。必ずチーム内で「現在誰もデプロイしていないこと」を合意してから実行しろ。さもなくば、ダブルライトによるステート崩壊(Split-Brain)という真の地獄が待っている。

—

5. プレビューと実際の乖離(Drift):Refreshの強制とライフサイクル管理

地獄の症状

`pulumi preview` では「変更なし」と出るのに、`pulumi up` を実行すると実態との差異(Drift)でエラーになる、あるいは意図しないリソースの再作成(Re-creation)が走る。

エキスパートの解決策

Pulumiはローカルのステートキャッシュを信頼しすぎる傾向がある。クラウド側のコンソールで直接変更(Manual Hotfixなど)が行われた場合、ステートとの乖離が発生する。

デプロイの自動化パイプラインの先頭には、必ず `pulumi refresh` を組み込め。

GitHub Actions等でのパイプライン定義のベストプラクティス
steps:

  • name: Checkout Repo

uses: actions/checkout@v4

  • name: Setup Pulumi

uses: pulumi/action-install@v3

  • name: Refresh State to Detect Drift

run: pulumi refresh –yes –non-interactive
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}

  • name: Execute Deployment

run: pulumi up –yes –non-interactive
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}

さらに、リソースの意図しない置換を防ぐためには、ライフサイクルオプション(`deleteBeforeReplace`)を適切に設計に組み込む必要がある。

import as aws from “@pulumi/aws”;

const db = new aws.rds.Instance(“my-db”, {
// …
}, {
// リソースを置き換える際、「新規作成 -> 削除」の順にするか、「削除 -> 新規作成」にするか
// ダウンタイム許容度に応じた設計が必須
deleteBeforeReplace: true,
});

—

6. シークレット漏洩と暗号化の罠:ConfigSecretの正しい扱い

地獄の症状

DBのパスワードやAPIトークンを通常の `pulumi.Config` で取得し、誤って `console.log` やエクスポートに平文で流し込んでしまい、CIのログに機密情報が盛大に晒される。

エキスパートの解決策

Pulumiのシークレット管理は `Output` 型のラップによって暗号化される。これを安易に `.get()` や `.apply()` で非シークレットな文字列として取り出すと、暗号化の魔術が解けて平文がログに露出する。

import as pulumi from “@pulumi/pulumi”;

const config = new pulumi.Config();
// 必ず getSecret を使うこと。通常の get() は厳禁。
const dbPassword = config.requireSecret(“dbPassword”);

// 良い例: Outputのままリソースに渡す(内部で自動的に暗号化ハンドリングされる)
const dbInstance = new aws.rds.Instance(“db”, {
password: dbPassword, // Outputをそのまま渡す
});

// 悪い例: 絶対にやってはならない
dbPassword.apply(pwd => {
console.log(`Password is ${pwd}`); // CIログに平文が出力される!死刑!
});

—

7. プロバイダプラグインのバージョン不整合地獄

地獄の症状

開発者のローカルPCでは動くのに、CI環境や同僚のPCで `pulumi up` を実行すると、突然 `plugin version mismatch` や gRPCの通信エラーでクラッシュする。

エキスパートの解決策

Pulumiの各クラウドプロバイダ(AWS, GCP, Kubernetes等)は、Pulumi CLI本体とは独立した「言語プラグイン(gRPCサーバー)」として動作している。このバージョンが環境ごとにバラバラであることが、不具合の温床となる。

プロジェクトのルートにある `Pulumi.yaml` に、明示的なプラグイン要件をロックせよ。

name: my-infrastructure
runtime: nodejs
description: Production grade infrastructure with strict plugin pinning
plugins:
providers:

  • name: aws

version: “6.25.0”

  • name: kubernetes

version: “4.5.0”

CIのパイプラインでは、プラグインを自動ダウンロードさせるのではなく、キャッシュ機構を利用するか、明示的に `pulumi plugin install` をビルドステップの最初に挟め。

—

8. 巨大化したステートファイル(State Bloat)とメモリ爆発

地獄の症状

Kubernetesプロバイダを大量のマニフェストとともに使用し、数千、数万のリソースを単一のPulumiスタックで管理した結果、`pulumi up` 実行時に Node.js プロセスが `JavaScript heap out of memory` でクラッシュする。

エキスパートの解決策

モノリシックなIaC設計の限界だ。Terraformでも言えることだが、PulumiにおいてはNode.js/Pythonのランタイムメモリ制限に直接ヒットする。

1. スタックの分割(Domain-Driven Infrastructure):
ネットワーク、共通基盤、個別アプリケーションのK8sリソースを単一スタックに詰め込むな。`StackReference` を用いて、小さく疎結合なスタック群に分割せよ。
2. Node.jsのメモリ上限引き上げ:
CI環境での実行時に、V8エンジンのメモリヒープサイズを明示的に拡張する。

V8エンジンのメモリ制限を8GBに拡張してPulumiを実行
export NODE_OPTIONS=”–max-old-space-size=8192″
pulumi up –yes

—

9. 非同期プログラミングの罠:Promiseの未解決によるサイレント失敗

地獄の症状

TypeScriptやPythonでカスタムロジック(外部APIを叩いて動的に設定を生成するなど)を書いた際、`async/await` の付け忘れや、`Output` の非同期解決モデルの誤解により、リソースが空のプロパティを持ったまま作成される。エラーも吐かずにデプロイが「成功」するため、本番稼働後に障害として発覚する。

エキスパートの解決策

Pulumiのプログラムは通常のアプリケーションコードではない。宣言的モデルの上に手続き型言語の皮をかぶせているため、プログラミング言語の非同期構文とPulumiの `Output` の挙動を完璧に調停する必要がある。

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

// 外部APIから動的に設定値を取得する関数(例)
async function fetchExternalConfig(): Promise {
// 何らかの非同期処理
return “config-value”;
}

// 【誤り】トップレベルでawaitを使わない、あるいはOutputの概念を無視する
// const val = fetchExternalConfig(); // Promise がそのまま渡り、リソース作成がバグる

// 【正解】pulumi.output と apply を完璧に使い分ける
const configOutput = pulumi.output(fetchExternalConfig());

const myResource = new aws.s3.Bucket(“dynamic-bucket”, {
tags: configOutput.apply(val => ({
ExternalConfig: val,
})),
});

—

10. カスタムプロバイダ・Dynamic Providerのデバッグ地獄

地獄の症状

既存のプロバイダでカバーしきれない独自のプロビジョニングロジック(社内APIの叩き、特殊なハードウェア制御など)を実現するために `pulumi.dynamic.Resource` を実装したところ、`Create` や `Update` メソッド内で例外が発生し、ステートが中途半端な `Corrupted` 状態になる。

エキスパートの解決策

Dynamic Providerは強力だが、エラーハンドリングを怠るとステートの整合性が一瞬で崩壊する諸刃の剣だ。各ライフサイクルメソッド(`create`, `update`, `delete`, `diff`)内で、徹底的な構造化ロギングと冪等性の担保(Idempotency)を実装せよ。

import as pulumi from “@pulumi/pulumi”;

class MyCustomResourceProvider implements pulumi.dynamic.ResourceProvider {
async create(inputs: any): Promise {
try {
// 冪等性を担保した外部APIコール
const externalId = await callMyApiWithRetry(inputs);
return {
id: externalId,
outs: { …inputs, externalId },
};
} catch (error) {
// エラー時でもPulumiエンジンが正しく認識できるよう詳細なエラーをスロー
throw new Error(`Failed to create custom resource: ${error.message}`);
}
}
}

—

結び:インフラストラクチャを「掌握」するということ

Pulumiは、コードの柔軟性ゆえに、書き手の技量がそのままインフラストラクチャの堅牢性に直結する。HCLのような「制限された言語」のフレームワークから解放された我々は、プログラミングのあらゆる知見――非同期制御、メモリ管理、例外処理、型安全性――を総動員してインフラを構築しなければならない。

エラーに直面したとき、エラーメッセージをただ眺めるな。裏で動いているgRPCの通信、プロバイダSDKの挙動、ステートJSONの構造、そしてランタイムのメモリ空間。そのすべてを解像度高くイメージできた時、あなたはもはや「Pulumiを使っているエンジニア」ではなく、「Pulumiというシステムを掌の上で転がすアーキテクト」の領域に到達している。

地獄の底からでも、コードと知見があれば、インフラは必ず復旧できる。冷静に、泥臭く、そして極限までコードを洗練させよ。

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