3年間CDK・Terraform・Pulumiを本番運用した結果。どれ選んだら後悔しないのか
AWS・Terraform・Pulumiを同時運用してわかった選定の落とし穴。マルチアカウント環境での運用コストと導入時の注意点を実体験から解説します。
本番で全部使ってみた結果、正直な話をします
うちのチームは3年前、AWSのインフラを「手動更新」から脱却するために、CDK・Terraform・Pulumiを同時に検証し始めました。正直、最初は「どれ選んでもいいでしょ」くらいの認識だったんですよね。でも、マルチアカウント環境で10個以上のスタックを回し始めた時点で、その認識は完全に崩壊しました。
今回は、3年間の運用で踏んだ地雷、本番で見えた違い、そして「結局どれを選ぶべきか」について、個人的な失敗も含めて共有したいと思います。
各ツールの本番運用を数値で見える化する
まず、うちが実装してきた規模感を共有しておきます。
xychart-beta
title IaC運用規模の推移(3年間)
x-axis [2024年初, 2024年中, 2025年初, 2025年中, 2026年初]
y-axis "スタック数" 0 --> 50
line [5, 12, 18, 35, 48]
line [0, 0, 5, 15, 22]
line [0, 0, 3, 8, 12]
これはうちのチームで管理しているスタック数の推移です。CDKから始めて、Terraformを段階的に導入、Pulumiは特定プロジェクトで試験的に採用しています。
各ツールの特性を実装で比較してみた
CDK:やっぱり慣性が強い
うちがCDKを選んだ理由は単純でした。チーム全員がTypeScriptやPythonを書いているし、AWS公式ツールだから情報も豊富。最初の3つのプロジェクトはCDKで統一したんです。
実装例としてVPC+EKSクラスタ構成を見てみると:
const vpc = new ec2.Vpc(this, 'MainVpc', {
maxAzs: 3,
cidrMask: 24,
natGateways: 1,
});
const cluster = new eks.Cluster(this, 'EksCluster', {
vpc,
version: eks.KubernetesVersion.V1_31,
defaultCapacity: 3,
outputClusterName: true,
});
// L3コンストラクト(高レベルAPI)のおかげで、boilerplate が少ない
ここまでは良かったんですよ。でも、2024年中盤にマルチアカウント構成を導入する際に問題が出ました。
問題1:State管理の複雑さ
CDKはCloudFormationベースなので、stack outputsをクロスアカウント参照する時、手動で値をコピペしたり、Parameter Storeに保存する必要がある。Terraformなら terraform_remote_state で自動的に解決できる話です。それまで「CloudFormation でいいか」って思ってたんですけど、スタック数が増えると本当に面倒になるんですね。
問題2:チーム間での知識差
CDKはコード書く側のスキルに依存しすぎます。JavaScriptのクロージャやジェネリクスが分かんないと、「なぜこの値が渡ってるのか」が謎になる。実際、3人のチームメンバーが同じ機能を実装したら、やり方が3通りになってました。これ、レビューするの大変なんですよ。
問題3:アップグレード地獄
2024年と2026年でのCDKアップグレードで、L3コンストラクトのAPIが変わって、既存コードをごっそり直した経験があります。正直「後方互換性、大事にしてほしいな」と思いました。Pulumiに比べると、CDKのバージョン更新はちょっと容赦ない。
Terraform:地味だけど安定している
2024年中盤、新規プロジェクトでTerraformを導入してみました。理由は「state管理が堅牢」「言語に依存しない」「AWSに限定されない」――将来的にマルチクラウドを視野に入れていたので。
# variables.tf
variable "environment" {
type = string
validation {
condition = contains(["dev", "stg", "prod"], var.environment)
error_message = "Valid values are dev, stg, prod."
}
}
# vpc.tf
resource "aws_vpc" "main" {
cidr_block = var.environment == "prod" ? "10.0.0.0/16" : "10.1.0.0/16"
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
# eks.tf
resource "aws_eks_cluster" "main" {
name = "${var.environment}-cluster"
role_arn = aws_iam_role.eks_cluster.arn
vpc_config {
subnet_ids = aws_subnet.private[*].id
}
depends_on = [aws_iam_role_policy_attachment.eks_cluster]
}
# Remote State参照(マルチアカウント)
data "terraform_remote_state" "prod" {
backend = "s3"
config = {
bucket = "terraform-state-prod"
key = "infrastructure/terraform.tfstate"
region = "ap-northeast-1"
}
}
Terraformで良かった点は以下の3つですね。
1. State管理が明示的
terraform.tfstate ファイルがあるので「今何がデプロイされているのか」が一目瞭然。CloudFormationStackを複数管理するより見通しが良い。何か問題が起きた時も、State ファイル見れば状況把握できます。
2. Plan → Apply の段階分けがしっかりしている
本番環境に入れる前に terraform plan で差分を確認できる。CDKの cdk diff も同じことできますが、Terraformの方が詳細な差分情報が出るんです。本番前の確認作業が心理的に安心感につながります。
3. 言語依存がない
HCLは学習曲線が低いし、「インフラの専門家が管理する」みたいな運用も可能。つまり、プログラマじゃなくてもメンテナンスできるんですよ。これ、長期運用では結構大事です。
ただし、正直な話、欠点もあります。
問題1:Terraformの学習コスト
HCLは簡潔ですが、for_each、dynamic、locals を組み合わせた複雑な構成を書くと、コードの可読性がガタ落ちします。特に、ループと条件分岐が絡むと「これ何やってるんだ?」って感じに。地味に難しいんですよ。
問題2:カスタムロジックが弱い
「本番環境の時だけ、このタグをつける」「AZの数に応じて、NAT Gatewayの数を動的に決める」みたいな、ちょっと複雑なロジックが書きづらい。Terraformは「宣言的」に徹しているので、汎用プログラミング言語ほどの自由度がない。複雑な要件は、Terraform では頭をひねることになります。
問題3:Module地獄
Terraformで再利用可能なコンポーネント(Module)を作ろうとすると、.tfvars ファイルの管理が煩雑になります。うちは一時期、同じ構成を異なる値で10回デプロイする際に、Moduleを使うべきか for_each でループするべきか悩んで、結局両方やった時代がありました(笑)。これ、本当に迷いどころなんですよ。
AWS構成図:実際に運用してる構成
graph TB
subgraph "Terraform State管理"
S3["S3 Backend<br/>terraform.tfstate<br/>Version Control enabled"]
DynamoDB["DynamoDB<br/>State Lock<br/>Prevent concurrent runs"]
end
subgraph "マルチアカウント構成"
subgraph "管理アカウント"
Control["Control Tower<br/>+ CDK Pipelines"]
Remote["Remote State<br/>Reference"]
end
subgraph "開発アカウント"
Dev["EKS Dev<br/>Terraform管理"]
DevRDS["RDS Dev<br/>CDK管理"]
end
subgraph "本番アカウント"
Prod["EKS Prod<br/>Terraform管理"]
ProdRDS["RDS Prod<br/>Terraform管理"]
end
end
subgraph "CI/CDパイプライン"
GitHub["GitHub<br/>Main Branch"]
CodePipeline["CodePipeline<br/>Multi-Account Deployment"]
Validation["Terraform Validate<br/>CDK Synth<br/>Security Scan"]
end
S3 -->|State管理| Dev
S3 -->|State管理| Prod
DynamoDB -->|Lock| S3
Control -->|Cross-account| Remote
Remote -->|参照| Dev
Remote -->|参照| Prod
GitHub -->|Webhook| CodePipeline
CodePipeline -->|Pipeline| Validation
Validation -->|Deploy| Dev
Validation -->|Deploy| Prod
style S3 fill:#FF9900
style DynamoDB fill:#FF9900
style Control fill:#232F3E
style CodePipeline fill:#FF9900
これは、うちが2026年時点で落ち着いた構成です。Terraformで管理するものと、CDKで管理するものを分けています。この構成に至るまで、結構な試行錯誤がありました。
Pulumi:「プログラミング言語として書ける」の本当の価値
2025年初頭、特定の機械学習プロジェクトでPulumiを試してみました。理由は「Python中心チームだし、複雑なロジックが必要だから」。
import pulumi
import pulumi_aws as aws
import json
config = pulumi.Config()
environment = config.get('environment') or 'dev'
model_endpoint = config.get('sagemaker_endpoint')
# 環境に応じた動的なリソース作成
def create_inference_infrastructure():
# 計算リソース数を環境で決定
instance_count = 1 if environment == 'dev' else 3
instance_type = 'ml.t3.medium' if environment == 'dev' else 'ml.p3.2xlarge'
# 本番環境の場合、追加のセキュリティグループを作成
security_group_rules = [
aws.ec2.SecurityGroupIngressArgs(
protocol='tcp',
from_port=443,
to_port=443,
cidr_blocks=['0.0.0.0/0'],
),
]
if environment == 'prod':
security_group_rules.append(
aws.ec2.SecurityGroupIngressArgs(
protocol='tcp',
from_port=8080,
to_port=8080,
security_groups=['sg-xxxxx'], # VPN経由のみ
)
)
sg = aws.ec2.SecurityGroup(
f'{environment}-ml-sg',
ingress=security_group_rules,
)
# SageMaker Endpointの作成
endpoint_config = aws.sagemaker.EndpointConfigArgs(
production_variants=[
aws.sagemaker.EndpointConfigProductionVariantArgs(
model_name=model_endpoint,
variant_name='AllTraffic',
initial_variant_weight=1,
initial_instance_count=instance_count,
instance_type=instance_type,
)
],
)
endpoint = aws.sagemaker.Endpoint(
f'{environment}-inference-endpoint',
endpoint_config_name=endpoint_config.name,
)
return endpoint, sg
# デプロイ
endpoint, sg = create_inference_infrastructure()
# CloudWatchダッシュボードを動的に作成
dashboard = aws.cloudwatch.Dashboard(
f'{environment}-ml-dashboard',
dashboard_body=pulumi.Output.concat(
'{"widgets": [{"type": "metric", "properties": {"metrics": [["AWS/SageMaker", "Invocations", {"stat": "Sum", "label": "',
endpoint.name,
'"}]]}]}'
),
)
pulumi.export('endpoint_name', endpoint.endpoint_name)
pulumi.export('security_group_id', sg.id)
Pulumiの強さは、ここです。Python(やGo・TypeScript)をそのまま書ける。Terraformで「環境別に場合分けして、複雑なロジック組む」のが辛いなら、Pulumiなら普通のプログラミング言語で解決できるんです。
Pulumiで実感したメリット:
-
汎用プログラミング言語の力:ループ、条件分岐、関数、クラスをフル活用できる。エンタープライズアプリケーション開発者にとって、超直感的。
-
Type Safety(Python型ヒント対応):
mypyでチェックできるから、デプロイ前に型エラーを検出できます。事前チェックができるのは心強い。 -
テストが書きやすい:Unit testを pytest で書けば、ロジックが正しいか検証可能。インフラの品質保証が、アプリケーション開発と同じレベルでできるんですよ。
ただし、Pulumiで地雷を踏みました。
問題1:State管理がまだ成熟していない感
PulumiもStatefileを使いますが、2026年時点でも「S3でのStateロック機構」がTerraformほど洗練されていません。うちは本番環境をPulumiで管理するのはやめて、開発環境限定にしました。正直「Pulumi 使うなら、まだ本番環境には早い」というのが実感です。
問題2:コミュニティサイズ
TerraformやCDKに比べて、日本語のTipsが少ない。困った時、StackOverflowにも解答が少ない。2026年でも状況は変わってません。ドキュメントは充実してますが、「実例」が少ないんですよ。
問題3:ベンダーロックイン懸念
Pulumiは企業製品です。「無料版」の機能に上限があり、本番環境運用するなら有料契約が必要。長期的なコストを考えると、OSSのTerraformの方が安心感がある。これ、3年運用した今だから言えることですけど、最初は見落としがちです。
本番で見えた選定基準:チームの視点で語る
うちが3年運用してきた中で、「結局どのツール選ぶべき?」の判断基準を整理しました。
条件1:チームスキルセット
選定基準としては以下の感じですね。
- Pythonチーム・TypeScriptチーム:Pulumi検討の価値あり
- HCLを学んでもいい、言語不問:Terraform推奨
- とにかく早くAWSだけで完結させたい:CDK
うちの場合、バックエンド(Python)チーム、インフラ(言語不問)チーム、フロントエンド(JavaScript)チームがいるので、「全員が読める」という観点ではTerraformが有利でした。これ、実装効率だけじゃなくて、トラブル時の対応速度も変わります。
条件2:State管理の複雑さ
運用規模によって、State 管理の複雑さは大きく変わるんです。
単一アカウント:
├─ CDK(S3 backendで十分。そもそも CloudFormation が管理してくれる)
└─ Terraform(Overkill気味だが問題なし。むしろ将来的には安定)
マルチアカウント:
├─ Terraform(`terraform_remote_state`で自動参照。スケーラビリティ最高)
├─ CDK(Parameter Storeで手動橋渡し、やや煩雑。数増えるとつらい)
└─ Pulumi(S3 backend使うなら、CDKと同じ課題。長期保守には不安)
うちの場合、この表を見た時点で「マルチアカウント運用するなら、Terraform一択かな」ってなりました。
条件3:チーム規模とメンテナンス性
個人的に感じたのは:
- 5人以下:何選んでも大丈夫。むしろ「1つに統一する」ことが重要。迷い続けてるのが一番ダメ。
- 5〜20人:TerraformかCDK。Pulumiはコミュニティサポートが心配。本番環境での安定性も考えると、やっぱり実績のあるツールを選びたい。
- 20人以上:Terraform。スケーラビリティと知識共有の観点から、もう Terraform で決定しちゃった方が効率的。
うちのチーム(15人程度)では、最終的に「Terraform を主軸に、CDKを補助」という形に落ち着きました。これ、本当に正解だと思ってます。
実装の落とし穴:本当に困った話
CDKで失敗したこと
失敗1:L3コンストラクトへの過度な依存
CDK V2の初期段階で、ドキュメントに掲載されていたL3コンストラクト(ApplicationLoadBalancerなど)を使い倒しました。でも、バージョン更新で「この機能は廃止」となって、コード全体を修正する羽目に。正直、かなり痛い経験でした。
対策としては、**L1/L2コンストラクト(CloudFormationに近いレイヤー)を基本に、L3は補助的にのみ使う。**これでバージョン更新の影響を最小化できます。
失敗2:TypeScriptの動的な値渡し
// こういうコードを書くと、CDK Synthの時点でエラーが分かりにくい
const tags = {
Environment: environment,
Team: teamName,
...dynamicTags // 動的に追加される
};
const vpc = new ec2.Vpc(this, 'Vpc', { tags });
これ、本番環境で「このタグ、なぜついてないの?」って問題が発生しました。動的にタグを追加してるつもりが、条件によっては undefined になってたんです。Terraformならバリデーションが型注釈より厳密ですから、この手のバグが減ります。
Terraformで失敗したこと
失敗1:State ファイルの肥大化
数年運用すると、.tfstate ファイルが500MBを超えるようになって、terraform plan の実行時間が10分以上かかるようになった。これ、本当に痛い。CI/CD パイプラインも遅くなるし、デプロイの心理的なストレスも増えます。
対策としては、Workspace分割 + Module化で、State を複数に分割。
# workspace.tf
terraform {
required_version = "~> 1.9"
backend "s3" {
bucket = "terraform-state"
key = "${var.environment}/terraform.tfstate"
dynamodb_table = "terraform-lock"
}
}
こうすることで、開発環境と本番環境の State を分離して、本番の plan 実行も高速化できました。
失敗2:for_each での参照エラー
# これがうっかりうまくいかない
resource "aws_instance" "servers" {
for_each = toset(["web-1", "web-2", "web-3"])
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
# このタグが予期しない値になることがある
tags = {
Name = each.key
Index = index(keys(aws_instance.servers), each.key) # ← 危険
}
}
index() 関数を使って動的にインデックスを取得してたんですが、リソースの順序が変わると値がズレるんです。かなり厄介なバグでした。
対策としては、**count ではなく for_each を徹底。その代わり、indexが必要な場合は別途 Map で管理。**これでリソースの順序に依存しない安定した構成にできます。
Pulumiで失敗したこと
失敗1:Stack 間の依存関係でハマった
Pulumiは複数の Stack を同時実行できますが、State file の同期がシビアで、「Stack A が完成した後に Stack B をデプロイ」みたいな順序依存が表現しづらい。CDK や Terraform では明示的に依存関係を書けますが、Pulumi だと工夫が必要です。
対策としては、**開発環境は Pulumi、本番環境は Terraform に統一。Single source of truth を徹底。**これなら、複雑な依存関係を Terraform で管理できて、開発段階では Pulumi の柔軟性を活かせます。
失敗2:Pulumi AI(ベータ)の過信
2025年に Pulumi が「AIで既存リソースを自動IaC化」という機能を出しました。試してみたら、結構な量の手修正が必要で、むしろ手動で書いた方が速かった。AIのテクノロジーは進化中ですけど、「銀の弾丸」じゃないんですね。期待値管理は大事です。
比較テーブル:最終的な評価
3年間の実運用を踏まえた評価です。
| 項目 | CDK | Terraform | Pulumi |
|---|---|---|---|
| 学習曲線 | 中程度(TypeScriptの知識必須) | 低い(HCLは簡潔) | 低い(既知言語で書ける) |
| コード可読性 | 高い(慣れたら) | 高い(宣言的) | 最高(プログラミング言語) |
| マルチアカウント対応 | △ | ◎ | △ |
| State管理 | CloudFormation任せ | ◎(明示的) | 〇 |
| コミュニティサイズ | ◎ | ◎ | △ |
| 本番環境での信頼性 | ◎ | ◎ | 〇 |
| 複雑なロジック実装 | ◎ | △ | ◎ |
| ベンダーロックイン | △(AWS依存) | ◎(言語不問) | △(有料プラン) |
見ての通り、「完璧なツール」って存在しないんですよ。トレードオフとの付き合い方が大事です。
まとめ
3年の本番運用で、「IaC選定は技術的な正解より、チーム・組織・規模の観点の方が重要」だと学びました。技術は言い訳で、結局は人なんです。
最終的なうちの判断:
-
マルチアカウント環境 × 15人チーム = Terraform主軸:State管理の堅牢性とスケーラビリティで、Terraformに軍配。運用効率も圧倒的に良い。
-
ECS Fargate + RDS などのシンプル構成 = CDK補助:既存の CloudFormation との親和性を活かして、特定プロジェクトのみ。やっぱり AWS 公式ツールの安心感は大事。
-
機械学習パイプライン = Pulumi検証:Python でロジック組めるメリットは大きいが、本番稼動はまだ。「やりたいことを実装できる」という体験は最高ですけど、継続性と信頼性を考えると、まだ開発環境限定。
次のアクション:
- 今からなら Terraform から始める。 言語依存なしで、10年先も使える。実績もあるし、コミュニティも成熟してる。ぶっちゃけ失敗しにくい。
- チームに Python 得意な人がいるなら、Pulumi を「開発環境限定」で試す。 本番は Terraform export 後、手修正。複雑なロジックはここで吸収できます。
- AWS 専業で、さっさと実装したいなら CDK も選択肢。 ただし L3 コンストラクトへの過度な依存は避ける。L1/L2 でしっかり基礎を作る。
皆さんのチームは、どのツール選びました?正直、「完璧な選択肢」はないと思うので、失敗しながら適応していくしかない。ただ、この記事の失敗パターンを知ってれば、同じ沼にハマることは避けられるはず。
何か質問あれば、コメント欄で聞いてください。本当の話、何が困ったかまで含めて答えますから。