Pulumi Stateの極限制御:セルフホストバックエンド移行とKMSキーローテーションの要塞化
テックリードの君なら、こんな恐怖を味わったことが一度はあるはずだ。
「あ、やべ。デフォルトのPulumi Service(SaaS)の無料枠制限に引っかかった」「セキュリティ監査で、ステートファイルを完全に自社管理のS3/KMS下に置けって言われた」「KMSのキーローテーションをしたら、既存のスタックが突然復号できなくなって全リソースがロストしかけた……」
Terraformであれば `backend “s3″` の設定でお茶を濁せるが、プログラム言語の柔軟性を持つPulumiにおいて、ステート管理の裏側を理解していないのは、時限爆弾を抱えたまま高速道路を逆走するようなものだ。
今回は、デフォルトのPulumi Serviceから脱却し、AWS(S3 + DynamoDB + KMS)を用いた完全セルフホスト型かつ堅牢なステート管理基盤への移行と、ゼロダウンタイムでのKMSキーローテーションを完遂するための実践的知見を叩き込む。
—
1. 開発スピードを加速させるPulumi CLIの隠れた武器
本題に入る前に、日々のオペレーションをマッハにするための実務テクニックを共有する。これを知っているだけで、開発サイクルの速度が文字通り桁違いになる。
隠れた神コマンド & ショートカット
- `pulumi refresh –expect-no-changes`
- クラウドの現状とステートの乖離を高速検知する。CI/CDのパイプラインの最初期にこれを挟むことで、ドリフトによるデプロイ失敗を未然に防ぐ。
- `pulumi stack export > stack.json` / `pulumi stack import`
- ステートのJSONを直接いじる必要がある絶望的な状況(リソースのゾンビ化など)で、エディタの強力な置換機能を使って一括修正するための最終兵器。
- 環境変数によるバックエンド切替の自動化
- `PULUMI_BACKEND_URL` を使いこなせ。これがないとマルチテナントやマルチクラウドの環境で死ぬ。
ローカル/S3バックエンドを即座に切り替えるためのエイリアス例
alias pulumi-prod=’export PULUMI_BACKEND_URL=”s3://my-company-pulumi-states-prod?region=ap-northeast-1&awskmskey=alias/pulumi-prod” && pulumi’
alias pulumi-staging=’export PULUMI_BACKEND_URL=”s3://my-company-pulumi-states-staging?region=ap-northeast-1&awskmskey=alias/pulumi-staging” && pulumi’
—
2. デフォルトからセルフホスト(AWS S3 + DynamoDB)への移行
SaaS版のPulumi Serviceは便利だが、エンタープライズ領域では「ステートを自社VPC内(あるいは管理下のアカウント)から一歩も出さない」という要件が降ってくる。
セルフホストバックエンドの要件は以下の2点だ。
1. ステートファイルの置き場所: バージョニングと暗号化が有効な Amazon S3
2. 排他制御(ロック)の仕組み: 同時実行によるステート破壊を防ぐ Amazon DynamoDB
バックエンド用インフラストラクチャの構築(Terraform/Pulumi)
まずはインフラ側の受け皿を作る。ここでのポイントは、ステートを暗号化するための専用Customer Managed Key (CMK) を作成することだ。
// Pulumi TypeScriptによるバックエンド基盤の定義例 (backend-infra.ts)
import as aws from “@pulumi/aws”;
// 1. ステート暗号化用の専用KMSキー
const pulumiKmsKey = new aws.kms.Key(“pulumi-state-key”, {
description: “Pulumi State Encryption Key”,
deletionWindowInDays: 30,
enableKeyRotation: true, // 自動ローテーションを有効化(後述の運用に直結)
});
const pulumiKeyAlias = new aws.kms.Alias(“pulumi-state-alias”, {
targetKeyId: pulumiKmsKey.id,
name: “alias/pulumi-state-encryption”,
});
// 2. ステート保存用S3バケット
const pulumiBucket = new aws.s3.Bucket(“pulumi-state-bucket”, {
bucket: “my-company-secure-pulumi-states-tokyo”,
versioning: {
enabled: true, // 誤って上書き・削除された際のライフセーバー
},
serverSideEncryptionConfiguration: {
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: “aws:kms”,
kmsMasterKeyId: pulumiKmsKey.arn,
},
},
},
});
// パブリックアクセスは完全ブロック
const bucketPublicAccessBlock = new aws.s3.BucketPublicAccessBlock(“public-access-block”, {
bucket: pulumiBucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
});
// 3. 排他制御用DynamoDBテーブル
const pulumiLockTable = new aws.dynamodb.Table(“pulumi-lock-table”, {
name: “pulumi-state-locks”,
billingMode: “PAY_PER_REQUEST”,
hashKey: “LockID”,
attributes: [
{ name: “LockID”, type: “S” },
],
});
移行手順(Migrate Backend)
既存のPulumi Service(組織名 `my-org`)から、新しく作ったS3バックエンドへ移行するコマンド手順は以下の通りだ。
1. 移行先のバックエンドURLを指定
export PULUMI_BACKEND_URL=”s3://my-company-secure-pulumi-states-tokyo?region=ap-northeast-1&awskmskey=alias/pulumi-state-encryption”
2. 現在のスタックをログイン確認
pulumi login $PULUMI_BACKEND_URL
3. 既存のスタックを移行(SaaS側からエクスポートしてS3側へインポート)
※ 事前に Pulumi SaaS から JSON を抜いておく、またはログインを切り替えてマイグレーションを実行
pulumi stack migrate s3://my-company-secure-pulumi-states-tokyo?region=ap-northeast-1&awskmskey=alias/pulumi-state-encryption
—
3. KMSを用いたステートファイル暗号化の深層
「S3のサーバーサイド暗号化(SSE-S3 / SSE-KMS)」だけでは不十分なケースがある。Pulumiのステートファイルには、データベースのパスワード、APIトークン、SSH秘密鍵などの機密情報(Secrets)が暗号化された状態で含まれている。
しかし、バックエンド自体の暗号化(保管時の暗号化: Encryption at Rest)に加え、Pulumi独自のConfig Secrets Encryptionがどのように連携しているかを理解する必要がある。
[Pulumi CLI]
│ (機密情報を固有の passphrase または KMS で暗号化)
▼
[暗号化されたState JSON]
│ (S3の SSE-KMS / カスタマー管理キーでバケット全体を暗号化)
▼
[Amazon S3 Storage]
二重防御の構築
1. データ層: S3のKMSキー(`alias/pulumi-state-encryption`)でストレージレベルを保護。
2. ペイロード層: Pulumi自体が持つSecrets Provider(AWS KMS Secrets Provider)を使用し、スタック内の機密変数を個別に暗号化。
これを実現するPulumiスタックの初期化コマンドはこれだ。
pulumi stack init production \
–secrets-provider=”awskms://arn:aws:kms:ap-northeast-1:123456789012:key/your-kms-key-uuid”
これにより、コード上の `config.requireSecret(“dbPassword”)` は、AWS KMSを通じた強固な暗号化ロジックの保護下に置かれる。
—
4. 運用中のキーローテーションでデプロイ障害を起こさないためのベストプラクティス
AWS KMSの「自動キーローテーション(毎年1回)」を有効にしている場合、あるいはセキュリティポリシーで手動のキーローテーションを義務付けられている場合、最悪のシナリオは「新しいキーで暗号化されたが、古いキーを参照し続けてデプロイが死ぬ、あるいはその逆」という事態だ。
AWS KMSのキーローテーションは、厳密には「暗号化に使用するプライマリキーのバージョンが切り替わる(CMKのID自体は変わらない)」という挙動をする。そのため、正しく運用していれば過去のデータ(古いキーバージョンで暗号化されたステート)は自動的に復号できる。
しかし、完全なキーの廃止(Deletion)や、別キーへの移行(Re-keying)を行う際は細心の注意が必要だ。
🚨 障害を防ぐための鉄則:キーローテーション手順
もしキー自体を新しく作り直す(リケーリングする)場合の、現場で実践すべき安全な手順を公開する。
[Phase 1: 準備]
新KMSキー作成 ──> 既存ポリシーの付与 ──> Pulumi設定の更新
[Phase 2: エクスポート & インポート (Re-encryption)]
旧バックエンドから取得 ──> 新KMSキーで再暗号化してS3へ書き戻し
[Phase 3: 旧キーの廃止留保]
旧キーを即座に削除せず、最低30日間のエイリアス維持(フォールバック用)
1. エイリアスの抽象化を使う
コードや環境変数に「KMSキーのARN」を直接ハードコーディングしてはならない。必ず KMS Alias(例: `alias/pulumi-state-encryption`) を使え。キーの裏側がローテーションされても、Aliasが新しいキーを指していれば、Pulumi側でコードを変更する必要はない。
2. 定期的な「ドライラン(Dry-run)」の実施
キーローテーション後、次のデプロイで必ず障害が起きるチームがある。これを防ぐため、CI/CDパイプラインに以下のチェックを組み込め。
GitHub Actions のワークフロー断片例
- name: Verify Pulumi State Readability
env:
PULUMI_BACKEND_URL: s3://my-company-secure-pulumi-states-tokyo?region=ap-northeast-1&awskmskey=alias/pulumi-state-encryption
run: |
# デプロイ前にステートが正常に読み込めるか(暗号化が破綻していないか)を検証
pulumi stack ls –non-interactive
もしKMSの権限設定ミスやキーの誤削除が発生していれば、この `pulumi stack ls` が一瞬でエラーを吐いて止まるため、本番デプロイ中の予期せぬ中断を防げる。
—
5. チーム開発のための設定共有化ルールとプロジェクト構成
最後に、複数人のエンジニアがこのセルフホストバックエンド環境で安全に開発を進めるためのディレクトリ構成と設定のベストプラクティスを示す。
ディレクトリ構成案
プロジェクトルートに `Pulumi.yaml` を配置し、バックエンドのURLはコードに含めず、実行環境(ターミナルやCI)またはラッパースクリプトで注入する。
my-infrastructure-repo/
├── .github/
│ └── workflows/
│ └── deploy.yml # CI/CDでのバックエンドURL指定とAWS認証
├── infra/
│ ├── Pulumi.yaml # プロジェクト定義
│ ├── Pulumi.staging.yaml # ステージング用設定(Secrets含む)
│ ├── Pulumi.production.yaml # 本番用設定(Secrets含む)
│ ├── index.ts # メインのリソース定義
│ └── package.json
└── scripts/
└── deploy.sh # 環境変数とバックエンドURLを安全にラップするスクリプト
実用的なラッパースクリプト (`scripts/deploy.sh`)
チームメンバーが手元からデプロイする際、間違ったバックエンドや古いKMSキーを参照しないよう、環境変数を強制するスクリプトを用意する。
!/usr/bin/env bash
set -euo pipefail
引数からステージを取得 (staging / production)
STAGE=”${1:-staging}”
ステージに応じたAWS KMSキーとS3バケットのバインド
if [ “$STAGE” == “production” ]; then
export AWS_REGION=”ap-northeast-1″
export PULUMI_BACKEND_URL=”s3://my-company-secure-pulumi-states-tokyo?region=ap-northeast-1&awskmskey=alias/pulumi-state-production”
elif [ “$STAGE” == “staging” ]; then
export AWS_REGION=”ap-northeast-1″
export PULUMI_BACKEND_URL=”s3://my-company-secure-pulumi-states-staging?region=ap-northeast-1&awskmskey=alias/pulumi-state-staging”
else
echo “Error: Unknown stage ‘$STAGE’. Use ‘staging’ or ‘production’.”
exit 1
fi
echo “==> Logging into Pulumi backend for [${STAGE}]…”
pulumi login “$PULUMI_BACKEND_URL”
echo “==> Selecting stack: ${STAGE}”
pulumi stack select “$STAGE”
echo “==> Running pulumi up…”
pulumi up –stack “$STAGE”
—
魂のまとめ
Pulumiのセルフホストバックエンド運用とKMS暗号化・ローテーションは、一見すると面倒くさく見える。しかし、SaaS依存からの脱却、コンプライアンス要件のクリア、そして何より「自分たちのインフラの命綱を自分たちの手で完全にコントロールしている」というエンジニアリングの確信を与えてくれる。
- バックエンド指定には必ず KMS Alias を使え。
- S3 + DynamoDBの組み合わせで、スケーラブルかつ安全なロック機構を担保しろ。
- キーローテーションの際は、Aliasの切り替えと事前の `pulumi stack ls` による健全性チェックを忘れるな。
この設計を導入したその日から、あなたのチームのインフラ基盤は、どんな監査も、どんな障害も微動だにしない「要塞」へと生まれ変わる。さあ、コードを書け。