はじめに
インフラをコードで管理する IaC(Infrastructure as Code) のツールは複数ある。CloudFormation、AWS CDK、Terraform、Pulumi — どれを使っても「インフラをコードとして定義し、再現可能にデプロイする」というゴールは同じだが、言語・状態の持ち方・AWS への結びつきが違う。
この記事では、個人プロジェクトのインフラ管理に Pulumi + TypeScript を選んだ理由を、他ツールとの比較を交えて整理する。後半では、実際にぶつかった リソースライフサイクルの設計(retainOnDelete、S3 バージョニング・ライフサイクルルール、dependsOn)を具体的なコードで示す。
注: 記事中のバケット名・ドメイン・リポジトリパスはすべてダミー(
*.example/sample-*)に置き換えている。
IaC とは
IaC は、サーバやネットワークなどのインフラを コード(定義ファイル)で宣言し、ツールがそのコードをもとにリソースを作成・更新・削除する考え方。
IaC がない世界
マネジメントコンソールで手作業で構築する場合、以下の問題が起きやすい。
- 再現できない: 「先月作ったあの環境と同じもの」を正確に再現できない
- レビューできない: 変更が人の記憶や手順書頼みで、ダブルチェックが難しい
- 差分がわからない: 本番と検証で何が違うのか、調べるまでわからない
IaC がある世界
インフラの定義がコードになると、ソフトウェア開発と同じワークフローに乗る。
- Git で差分管理: 何がいつ変わったか、コミット履歴でわかる
- PR でレビュー: インフラ変更もコードレビューの対象にできる
-
CI/CD で自動化:
plan→ 承認 →applyの流れをパイプラインに組める
ツール比較 — CloudFormation / CDK / Terraform / Pulumi
IaC ツールにはそれぞれ特性がある。ここでは AWS を主なターゲットとする 4 つを、言語・状態管理・AWS との結びつきの観点で整理する。
| 観点 | CloudFormation | AWS CDK | Terraform | Pulumi |
|---|---|---|---|---|
| 記述言語 | JSON / YAML | TypeScript, Python, Java 等 | HCL(独自言語) | TypeScript, Python, Go, Java 等 |
| 状態(state) | AWS が管理(CFn スタック) | CFn に委譲 |
.tfstate(S3 等に保存) |
.json(S3, Pulumi Cloud 等に保存) |
| AWS 外のリソース | 不可(カスタムリソースで回避は可) | 不可(CFn の制約を継承) | プロバイダが豊富 | プロバイダが豊富 |
| 学習コスト | YAML/JSON の記法 | 汎用言語だが CFn 知識も必要 | HCL を覚える必要あり | 普段の言語が使える(設計力は要求される) |
| コードの標準化 | 記法が固定的で揺れにくい | CDK パターンで一定の統一 | HCL の制約で揃いやすい | 書き方が自由すぎて揺れうる |
| エコシステム | AWS ネイティブ・最も実績豊富 | AWS 公式サポート | コミュニティ資産が圧倒的 | Terraform ブリッジで補完 |
| AWS サポート | 直接テンプレートを調査可能 | CFn 経由で同等 | サードパーティ扱い | サードパーティ扱い |
CloudFormation / CDK の特徴
CloudFormation は AWS の IaC としては最も歴史がある。スタックの状態管理は AWS が面倒を見るため、ユーザが state ファイルの保管場所を考える必要がない。一方で JSON / YAML の記法が冗長になりがちで、条件分岐やループを書くのがつらい場面がある。
CDK はこの冗長さを汎用言語で解決する。TypeScript 等で書いたコードが 最終的に CloudFormation テンプレートに変換(synth) される仕組みだ。言語の表現力は上がるが、出力先は CloudFormationなので、AWS 外のリソースを直接管理することはできない。
Terraform の特徴
Terraform は HCL(HashiCorp Configuration Language) という独自言語を使う。プロバイダの種類が非常に豊富で、AWS 以外にも GCP・Azure・GitHub・Datadog など幅広いサービスを管理できる。ただし、HCL を新たに覚える学習コストがある。状態ファイル(.tfstate)は S3 等に自分で配置する。
Pulumi の特徴
Pulumi は TypeScript・Python・Go・Java などの汎用言語でインフラを定義する。HCL のような新しい言語を覚える必要がなく、普段のプログラミング言語の知識がそのまま活きる。プロバイダも Terraform のエコシステムを(ブリッジ経由で)利用できるため、カバー範囲は広い。
Pulumi が苦手とする領域
メリットばかり並べてもフェアではないので、Pulumi を選ばないほうがよいケースも整理しておく。
書き方の揺れ(vs Terraform)
Terraform の HCL は宣言的な専用言語なので、誰が書いても構造が似通いやすい。一方 Pulumi は汎用言語を使えるぶん、ループ・条件分岐・クラス設計のやり方が人によってバラバラになりうる。ソフトウェア設計のスキルがインフラコードの品質に直結するため、チームの規模が大きいほどコードレビューのコストが上がりやすい。
ただし、これは コーディング規約の整備・ペアプロ・コードレビューといった、通常のソフトウェア開発で使う手段で十分に対処できる問題でもある。普段からアプリコードをレビューする文化があるチームなら、インフラコードも同じ流れに乗せるだけでよく、Terraform 特有の「HCL の書き方」を別途チーム内に広める必要もない。
エコシステムの厚み(vs Terraform)
Terraform は IaC のデファクトスタンダードとして、コミュニティが蓄積した完成済みモジュールや構成例の量が圧倒的に多い。ニッチな SaaS や国内クラウドベンダーのプロバイダも、Terraform 用はあっても Pulumi 用(またはブリッジ変換可能なもの)が存在しない・ドキュメントが薄いケースがある。「困ったらコピペできる資産がどれだけあるか」では Terraform に分がある。
ただし、AWS・Azure・GCP などの主要クラウドを使う前提であれば、Pulumi のプロバイダも十分に整備されており、公式ドキュメントや GitHub 上の構成例も揃っている。エコシステムの差が実感として出やすいのは、あくまでニッチなサービスや国内限定のベンダーを扱うときだ。主要クラウドを中心に使うプロジェクトなら、この点はほとんど気にしなくてよい。
エアギャップ環境での運用(vs Terraform)
Terraform はバイナリひとつで完結し、プロバイダのミラーリングに関するドキュメントも豊富なため、完全にクローズドなネットワーク内(エアギャップ環境)での運用実績が厚い。Pulumi も pulumi login --local でローカル state は使えるが、基本設計は Pulumi Cloud(SaaS)利用を前提にしている面があり、外部接続を一切許容しない環境では Terraform のほうが枯れている。
AWS サポートとの親和性(vs CloudFormation)
CloudFormation は AWS 純正のリソース管理エンジンなので、AWS サポートが直接テンプレートを調査・修正してくれる安心感がある。企業のコンプライアンスで「サードパーティの IaC ツールを介在させたくない」という制約がある場合は、CloudFormation(または CDK)一択になる。Pulumi も Native Provider で新サービスの早期対応を謳っているが、トラブル時にベンダーサポートの切り分けが増える点は意識しておきたい。
まとめると
Pulumi が選ばれにくいのは、「インフラ担当者がプログラミングに精通していない」「既存の膨大な Terraform 資産を移行するコストが見合わない」「state を SaaS に預けたくない厳しいセキュリティ要件がある」といったケースだ。ツールの優劣ではなく、チームのスキルセットと運用制約に合うかどうかで決まる。
なぜ Pulumi を選んだか
汎用言語がそのまま使える
Pulumi を選んだ最大の理由は、TypeScript でインフラを書けることだ。フロントエンド(Next.js)もバックエンド(Webiny のカスタマイズ)も TypeScript で書いているプロジェクトでは、インフラ定義もエディタの補完・型チェック・リファクタリングの恩恵をそのまま受けられる。
const bucket = new aws.s3.BucketV2("my-bucket", {
bucket: "sample-frontend-prod",
}, { retainOnDelete: true });
aws.s3.BucketV2 の引数の型が効くので、プロパティ名のタイポはコンパイル時に弾ける。YAML で書いていると実行するまで気づけないようなミスを、エディタ上で防げる。
ステート用バックエンドを自分で選べる
Pulumi の state(スタックの現在の状態を記録したファイル)は、保存先を自分で選べる。
| バックエンド | 特徴 |
|---|---|
| Pulumi Cloud(SaaS) | デフォルト。手軽だが SaaS への依存が生まれる |
| S3 |
pulumi login s3://my-bucket で利用。AWS 内で完結 |
| ローカルファイル |
pulumi login --local で利用。個人開発の実験向き |
今回のプロジェクトでは S3 に state を置く構成にしている。理由は次の 2 つ。
- SaaS 依存を減らす: Pulumi Cloud のサービス変更・料金改定に振り回されたくない
- AWS 内で閉じる: インフラもアプリも AWS 上にあるなら、state も同じ場所にあるほうが管理しやすい
CloudFormation のスタック管理との対比
CloudFormation では、スタックの状態は AWS が内部的に管理する。ユーザが state ファイルに触ることはない代わりに、状態の保存先を選ぶこともできない。
これは「おまかせできる」メリットでもあるが、CloudFormation というサービス自体の提供形態や仕様の変更に、状態ストアの面で完全に紐づくということでもある。Pulumi では state の保存先を S3 等の汎用ストレージに置けるため、IaC ツール側の変化に「状態ストア」の面では影響を受けにくい。
ただし注意として、リソース自体は AWS API に依存しているのは CloudFormation でも Pulumi でも同じだ。Pulumi を使えば AWS 依存がなくなるわけではなく、あくまで state の管理場所を選べるという一点での自由度の話である。
Webiny との IaC 一貫性
今回のプロジェクトでは Headless CMS に Webiny を使っている。Webiny 自体の AWS へのデプロイも Pulumi ベースで構成されている(yarn webiny deploy の内部で Pulumi が動く)。
配信インフラ(S3・CloudFront 等)を別リポの Pulumi プロジェクトで管理している場合でも、IaC のツール・概念・ドキュメントの系統が揃う。Webiny 側のインフラ構成を調べるときも、「Pulumi の ResourceOptions は…」「dependsOn は…」といった同じ用語と考え方で読めるため、CMS と配信側をまたぐ調査のコストが下がる。
リソースライフサイクルの実践
ここからは、Pulumi + TypeScript で AWS リソースを管理する際に使ったライフサイクル関連の設定を、具体的なコードで紹介する。
プロジェクト構成
エントリポイント(index.ts)で loadConfig() を呼び、どの環境(stg / prod)にどのリソース群をデプロイするかをスタック設定から判定する。shouldDeployFrontend / shouldDeployPulumiState のようなフラグで、デプロイ対象を環境ごとに切り替えている。
retainOnDelete — スタック削除時にリソースを残す
Pulumi でスタックを削除(pulumi destroy)すると、通常はスタック内のリソースもすべて削除される。しかし S3 バケットのように、中身のデータが重要なリソースは、スタック削除時に消されると困る。
const retainBuckets: pulumi.CustomResourceOptions = {
retainOnDelete: true,
};
const frontendBucket = new aws.s3.BucketV2("frontend-bucket", {
bucket: `sample-frontend-${env}`,
}, retainBuckets);
const stateBucket = new aws.s3.BucketV2("state-bucket", {
bucket: `sample-pulumi-state-${env}`,
}, retainBuckets);
retainOnDelete: true を渡すと、pulumi destroy してもバケットの実体は残る。Pulumi の state からは管理対象外になるが、AWS 上にはバケットとデータが残り続ける。
いつ使うか
| ケース | retainOnDelete |
|---|---|
| フロント配信用 S3(デプロイ済みの静的ファイル) |
true — スタック再構築時にもデータを消したくない |
| Pulumi state 用 S3(state ファイル自体を格納) |
true — state が消えると他プロジェクトに影響しうる |
| 一時的な検証用リソース |
false(デフォルト)— 検証が終わったら消えてよい |
S3 バージョニングとライフサイクルルール
Pulumi の state を格納する S3 バケットには、バージョニングを有効にしている。pulumi up のたびに state ファイルが上書きされるが、バージョニングがあれば過去の state に戻せる。
const stateVersioning = new aws.s3.BucketVersioningV2("state-versioning", {
bucket: stateBucket.id,
versioningConfiguration: {
status: "Enabled",
},
});
ただし、バージョニングを有効にするとオブジェクトの旧バージョンが際限なく溜まる。そこでライフサイクルルールで、非現行バージョンを 90 日で自動削除する。
const stateLifecycle = new aws.s3.BucketLifecycleConfigurationV2("state-lifecycle", {
bucket: stateBucket.id,
rules: [{
id: "expire-noncurrent-versions",
status: "Enabled",
noncurrentVersionExpiration: {
noncurrentDays: 90,
},
}],
});
この組み合わせで、直近 90 日分の state 履歴は保持しつつ、ストレージコストの際限ない増加を防ぐ。
アクセスログの設定
state 用バケットへのアクセスを記録するため、ログ用バケットを別に用意し、環境ごとのプレフィックスでログを分ける。
const logsBucket = new aws.s3.BucketV2("logs-bucket", {
bucket: "sample-pulumi-state-logs",
});
const stateLogging = new aws.s3.BucketLoggingV2("state-logging", {
bucket: stateBucket.id,
targetBucket: logsBucket.id,
targetPrefix: `${env}/`,
});
| 環境 | ログの出力先 |
|---|---|
| stg | s3://sample-pulumi-state-logs/stg/ |
| prod | s3://sample-pulumi-state-logs/prod/ |
dependsOn — リソース間の明示的な依存
Pulumi は通常、リソースのプロパティに他リソースの出力値を渡すことで暗黙的に依存関係を解決する。しかし、プロパティ参照だけでは表現できない依存がある場合は dependsOn で明示する。
const logsBucketPolicy = new aws.s3.BucketPolicy("logs-bucket-policy", {
bucket: logsBucket.id,
policy: logsPolicy,
});
const stateBucket = new aws.s3.BucketV2("state-bucket", {
bucket: `sample-pulumi-state-${env}`,
}, {
retainOnDelete: true,
dependsOn: [logsBucketPolicy],
});
この例では、state バケットの作成前にログバケットのポリシーが適用されていることを保証している。ログバケットのポリシーがないと、state バケットからのログ書き込みが権限不足で失敗するためだ。
暗黙的な依存と dependsOn の使い分け
| パターン | 依存の解決方法 |
|---|---|
| バケット ID をプロパティに渡す | 暗黙的(Pulumi が自動で依存を解決) |
| ポリシー適用後にバケットを作りたい |
dependsOn(プロパティ参照がないため明示が必要) |
dependsOn を多用するとコードが読みにくくなるため、プロパティ参照で解決できるならそちらを優先し、dependsOn はプロパティだけでは表現できないケースに限定するのがよい。
CI との連携
GitHub Actions で pulumi preview / pulumi up を実行し、インフラ変更を CI/CD パイプラインに組み込んでいる。
ポイント
-
pulumi login: CI 上でpulumi login s3://sample-pulumi-state-prodを実行し、S3 の state バックエンドに接続する -
config passphrase: Pulumi の暗号化設定(
pulumi config set --secretで保存した値)を復号するためのパスフレーズを SSM Parameter Store から取得する。passphrase の置き場所は GitHub Secrets でも運用上は成り立つが、state 自体を S3 に置いている以上、state の復号に使うパスフレーズも AWS 側(SSM)に寄せるほうが管理の一貫性がある、という判断でこの構成にしている -
PR 時は
previewのみ: 差分を確認し、意図しない変更がないかレビューする。up(実際のデプロイ)はマージ後に実行する
まとめ
| 観点 | 内容 |
|---|---|
| IaC の意義 | インフラの再現性・レビュー・自動化。コンソール手作業からの脱却 |
| Pulumi を選んだ理由 | TypeScript がそのまま使え、型チェック・補完が効く。state の保存先を自分で選べる |
| Webiny との一貫性 | Webiny も Pulumi ベースのため、IaC のツール・概念が統一される |
| state バックエンド | S3 に配置。SaaS 依存を減らし、AWS 内で管理を完結 |
retainOnDelete |
データの消失を防ぐ。フロント用・state 用の S3 に適用 |
| バージョニング + ライフサイクル | 過去 90 日分の state 履歴を保持しつつ、旧バージョンを自動削除 |
dependsOn |
プロパティ参照で表現できない依存を明示。ログバケットのポリシー → state バケットの順序保証 |
| CI |
preview で差分確認 → マージ後に up。passphrase は state と同じ AWS 側(SSM)に寄せる |
IaC ツールの選定は「どれが正解」というより、チームの技術スタックや運用ポリシーに合うかで決まる部分が大きい。TypeScript でフロントもバックも書いているプロジェクトでは、Pulumi でインフラも同じ言語に揃えるのは自然な選択だった。retainOnDelete やバージョニングといったライフサイクル設定は地味だが、本番データを守るために最初に決めておくべきことだと実感している。