はじめに
AWSを中心としたクラウドインフラの設計・構築・運用および、TerraformによるIaC設計・標準化を担当するクラウドアーキテクト志向のエンジニアです。これまで「インフラリソースはHCLで一つ一つ明示的に宣言し、完全に管理下に置く」というスタイルでやってきました。
この記事を書く動機は、単なる「CDK入門記」ではありません。組織でIaCの技術選定を行う際、あるいはTerraformチームがCDKプロジェクトへ合流する際に再現可能な判断基準を提供することが目的です。実際、職場のプロジェクトでAWS CDK (Python)が採用されており、「読み書きできるようになる」だけでなく「なぜCDKを選ぶべきか・避けるべきか」を組織に説明できる水準まで理解する必要がありました。
「インフラをPythonで書けるらしい」という前情報こそありましたが、実際に触れてみると、Terraformの「全部自分で書く安心感」 とは対極にある 「何でもできる自由さ」と「強烈な暗黙の挙動」 に大いに振り回されました。これはスキルの問題ではなく、パラダイムシフトの問題です。
本記事では、Terraformエンジニアが初めてCDKに触れた際のドタバタ(ECRが勝手に作られた衝撃など)から、試行錯誤の末に「クリーンアーキテクチャ的な設計ルール」をインフラに導入し、CDKのポテンシャルを引き出せるようになるまでの記録をまとめます。
作成したレポジトリ
概念マッピング:メンタルモデルのリセット
TerraformとCDKは背後にあるマインドセットが根本から異なります。まずここをリセットしないと痛い目を見ます。TerraformのメンタルモデルをそのままCDKに持ち込もうとすると、後述するような実運用トラブルに繋がります。
| Terraform | CDK (Python) | 設計思想の違い |
|---|---|---|
| HCL (宣言的) | Python (手続き・型安全) | HCLは「あるべき状態の羅列」。CDKは「状態を生成するプログラム」。この違いが後述する全ての落とし穴に繋がる。 |
provider "aws" |
Construct Library | CDKはAWS公式が各サービス用のクラス群(Construct)を提供する。Providerより高レベルの抽象層。 |
terraform.tfstate |
CloudFormation テンプレート | CDK自身は状態を持たない。cdk synth で生成されるCloudFormation YAMLが「真の定義」であり、状態管理はAWS側に委ねられる。 |
terraform plan |
cdk diff |
どちらも差分確認だが、CDKは「ローカルでCFnテンプレートを合成し、現在のクラウド環境との差分を見る」ものであり、planと完全に等価ではない。 |
terraform apply |
cdk deploy |
CDK側は「CFn Change Set」を作成して適用を実行する。 |
module |
Construct / Stack | 再利用可能な単位。CDKではPythonのクラス(Construct)としてカプセル化される。これがCDKの最大の武器でもあり、罠でもある。 |
CDKの最重要前提:「CDKは最終的にCloudFormationを生成するラッパーである」。cdk synth コマンドで出力されるYAMLこそがインフラの真の姿です。この認識なしにCDKを使うと、後述するような暗黙挙動に何度も足をすくわれます。
CDKで起きた現実
机上の概念マッピングを終えて実装に入ると、TerraformとCDKの「思想の違い」が身をもって体験できます。今回の比較題材は API Gateway → VPC Lambda → RDS (PostgreSQL) の構成です。
- VPC内にAPI用のLambdaとDB初期化用Lambdaを配置
- Security Groupで互いの通信を制御
- RDSを作成し、Secrets Manager でパスワード管理
- DB初期化スクリプトは
Custom Resourceを使ってデプロイ時に自動実行
この構成を実装する過程で踏み抜いた落とし穴を以下に記録します。これらは個人の凡ミスではなく、CDKの設計思想から来る構造的な認識ギャップです。
詰まった話①:ECRが勝手に生えていた
LambdaでDockerコンテナを動かそうとしたときのことです。以下のたった1行を書きました。
code=lambda_.DockerImageCode.from_image_asset(asset_path)
後日、会社のAWS Consoleを眺めていたところ、知らないECRリポジトリが存在することに気づきました。
Terraformの世界では、resource "aws_ecr_repository" を書かない限りECRが誕生することは天地がひっくり返ってもありません。しかしCDKでは、from_image_asset() を叩いた瞬間、裏側で自動的にDockerビルドが行われ、暗黙のECRリポジトリが自動生成され、そこにイメージがプッシュされる という強烈なマジックが発動していたのです。
この「暗黙の副作用」はCDKが高レベル抽象(L2/L3 Construct)として実現している利便性の裏面です。利便性を享受するためには、「コードを書く = リソースが生まれる」という1対1の対応が成立しない ことを設計原則として受け入れる必要があります。
ガバナンス上の教訓: CDKは1行のコードが裏側で複数のAWSリソースを生成する。cdk synth によるCloudFormationテンプレートの検証をCIパイプラインに組み込み、必須のゲートとして運用する。
詰まった話②:ECRの細かい設定には「2段階デプロイ」が必要
暗黙生成ECRではライフサイクルポリシー(古いイメージの削除)やイメージスキャンなどの詳細設定ができません。そこで、明示的に EcrConstruct を作り、Lambda側からそれを参照する構成に変更しました。
ところがこれだと、ECRが存在しない状態でLambdaをデプロイしようとしてエラー になりました。「Terraformならよしなに依存グラフを読んで一発でapplyしてくれるのに!」と思いましたが、CDK(CloudFormation)はコンテナイメージが実際に存在していないとLambdaを作成できません。
これはTerraformが「リソース依存グラフを自動解決してapplyする」のに対し、CDK(CloudFormation)は「テンプレートに記述されたリソース群を冪等に作成しようとするが、デプロイ前提条件(コンテナイメージの存在)は担保しない」という設計上の違いに起因します。
ガバナンス上の教訓: デプロイ順序は人間が設計・管理する責任がある。「ECR用Stack(Stage1) → Lambda用Stack(Stage2)」のようにStackをライフサイクル単位で分割し、デプロイ手順書に明記する。
詰まった話③:Secrets Managerでの名前衝突
一度環境を壊して作り直そうと cdk destroy → cdk deploy を実行したところ、今度はSecrets Managerの作成で ResourceExistsException が発生しました。
AWSの仕様上、Secrets Managerは削除しても 7〜30日間は「削除保留」状態として名前を占有 し続けます。Terraformを使っていた頃は terraform destroy して再作成するワークフローに慣れていましたが、CDKでこれをやると再実行時に盛大にコケます。
これは「インフラを不変(Immutable)なものとして扱う」CDKの想定運用と、「壊して作り直す」という再構築ワークフローの衝突です。
ガバナンス上の教訓: インフラを 安定リソース(VPC、RDS、手動入力用Secret等) と 可変リソース(Lambda、ECS等) でStackに分割せよ。安定リソースのStackは最初の一回だけデプロイし、以後は二度と destroy しない運用ルールを設計段階で確定させる。
設計ブレイクスルー
格闘の末に辿り着いた結論は「CDKの自由度はルールで制御しなければ組織的に使えない」というものでした。HCLは文法が限られている分、逆に「それ以上のことはできないし、やらなくていい」という制約がありました。一方CDKはただのPythonです。何も考えずに書くと、1つのStackにあらゆるリソースが絡み合うスパゲッティコードが生まれます。
そこで採用したのが、アプリケーション開発で使われる Clean Architecture(クリーンアーキテクチャ)の依存ルール をインフラコードに持ち込む、という設計思想です。
依存方向の厳守(インフラガバナンスの中核)
Network -> Entry -> Compute -> Data
- Network: VPC, Subnet, Security Group
- Entry: API Gateway, ALB
- Compute: Lambda, ECS
- Data: RDS, DynamoDB, S3
この依存方向をCDKのクラス(Construct)設計の 絶対のルール とします。上位レイヤーが下位レイヤーを知ることは許可しますが、逆方向・循環依存は禁止です。これは「コーディング規約」ではなく、コンストラクタのシグネチャで物理的に強制するガバナンス設計です。
Construct / Stack の責務分離
| レイヤー | ファイル | 責務 |
|---|---|---|
resources/ |
*_construct.py |
単一リソースのカプセル化。外部依存はコンストラクタ引数で受け取る |
stacks/ |
*_stack.py |
Construct同士のWiring(接続)のみ。ビジネスロジック禁止 |
app.py |
— | Stack間の接続と設定注入のみ |
Stack内でのWiring(接続)の透明化
▼ 実践するコードの例(StackでのWiring)
class ApiRdsStack(cdk.Stack):
def __init__(self, ...):
# 1. 依存を持たない Network と Security を先に作る
network = NetworkConstruct(...)
security = SecurityConstruct(..., vpc=network.vpc)
# 2. Data層 (RDS) を作る(Network/Securityに依存)
rds = RdsConstruct(..., vpc=network.vpc, rds_security_group=security.rds_sg)
# 3. Compute層 (Lambda) を作る(Data情報を受け取る)
lambda_api = LambdaFactory.create(
...,
vpc=network.vpc,
lambda_security_group=security.lambda_sg,
db_secret=rds.db_secret,
)
# 4. Entry層 (API GW) を作る(Compute情報を受け取る)
api_gateway = ApiGatewayConstruct(..., api_lambda=lambda_api.function)
この設計の肝は以下の3点です。
- 各レイヤーを別々のConstructクラスに分割する — 単体で理解できる粒度にする。
-
コンストラクタのシグネチャで依存方向を強制する —
ApiGatewayConstructの__init__はapi_lambdaを要求するが、VPCやRDSの情報は絶対に受け取らない。型システムがアーキテクチャルールを保証する。 - Construct同士の接続(Wiring)は必ずStackで行う — Stackを読めばインフラの依存グラフ全体が把握できる状態にする。「暗黙の依存」を根絶する。
TerraformのHCLではファイルをどう分割しようが裏側でフラットに扱われるため、「アーキテクチャによって依存方向を物理的に強制する」のが難しいです。Pythonクラスという機能を使って、インフラに明確な責務と境界線を引ける。これこそがCDKの最大の強みです。
なお、L3 Constructと呼ばれる ApplicationLoadBalancedFargateService などの高レベルAPIは、NetworkからEntryまでのすべてをブラックボックスの中で隠蔽して作成してしまいます。このルールの下では使用を禁止しています。理由は「Stackを読んでも依存グラフが把握できない」設計になるからです。
テスト戦略:テスト容易性がインフラ設計品質のバロメーター
このアーキテクチャ設計に pytest によるテストを組み合わせることで、インフラをCI/CDパイプラインの中で自動検証できます。Terraformは terraform plan の結果を目視レビューすることが多いですが、CDKはPythonなので自然にテストコードが書けます。
from aws_cdk import assertions
def test_lambda_stack_contains_docker_image_function():
app = cdk.App()
stack = LambdaStack(app, "TestStack")
template = assertions.Template.from_stack(stack)
# AWS::Lambda::Function リソースが1つだけ存在すること
template.resource_count_is("AWS::Lambda::Function", 1)
# x86_64アーキテクチャのDockerイメージ関数として設定されていること
template.has_resource_properties(
"AWS::Lambda::Function",
{
"PackageType": "Image",
"Architectures": ["x86_64"],
},
)
インフラが想定通りに合成されているか、特定のセキュリティグループが意図しない方向に開いていないかなどを、CIパイプラインの中で自動検証できます。「構成がスパゲッティだとテストが書けなくなる」という逆説的な性質があります。つまり テスト容易性がそのままインフラ設計の品質指標になります。Constructを単体でテスト可能な粒度に保つことが、設計品質を維持するための実践的な基準となります。
Terraform vs CDK(思想比較)
ここからは、同じアーキテクチャをTerraformとCDKで実装した際のコード比較を通じて、「設計思想の違い」を具体的に見ていきます。
4.1 VPC / ネットワーク:明示性 vs 生産性
Terraformでは、VPC本体・サブネット・ルートテーブル・NATゲートウェイなどを個別に記述する必要があります。
▼ Terraform の場合 (イメージ)
resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" }
resource "aws_subnet" "public_1a" { ... }
resource "aws_subnet" "private_app_1a" { ... }
resource "aws_subnet" "private_db_1a" { ... }
# ...さらにルートテーブルやNATをひたすら書く...
▼ CDK の場合
self.vpc = ec2.Vpc(
self,
"Vpc",
max_azs=2,
nat_gateways=1,
subnet_configuration=[
ec2.SubnetConfiguration(name="Public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=24),
ec2.SubnetConfiguration(name="AppPrivate", subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS, cidr_mask=24),
ec2.SubnetConfiguration(name="DbPrivate", subnet_type=ec2.SubnetType.PRIVATE_ISOLATED, cidr_mask=24),
],
)
CDKでは ec2.Vpc オブジェクトを1つ宣言するだけで、内部的に必要なサブネットやルーティング設定、NATゲートウェイまで全て一撃で構築してくれます。
トレードオフ: Terraformの明示性は「何が作られるか」を完全にコントロールできる反面、変更コストが高い。CDKは「何が作られるか」を知るために cdk synth が必須になる反面、変更が柔軟で高速です。
4.2 Security Group の相互参照:文字列ID vs オブジェクト参照
Terraformで煩雑になりがちなSecurity Group同士の参照も、CDKではオブジェクト指向の強みが出ます。
▼ Terraform の場合
resource "aws_security_group_rule" "rds_from_lambda" {
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_group_id = aws_security_group.rds_sg.id
source_security_group_id = aws_security_group.lambda_sg.id
}
▼ CDK の場合
self.rds_sg.add_ingress_rule(
peer=self.lambda_sg, # オブジェクトを直接渡す
connection=ec2.Port.tcp(5432),
description="Allow PostgreSQL from Lambda",
)
リソースIDの文字列を渡すのではなく、peer=self.lambda_sg のように オブジェクトそのものの参照を渡せる のが非常に直感的でした。型システムによる保証もあります。
4.3 RDS + Secrets Manager + IAM の衝撃:抽象化の威力と違和感
個人的に一番感動したのがここです。
▼ Terraform の場合
# Secrets Managerを作り、パスワードを生成し、RDSに渡し、LambdaのRoleにポリシーをアタッチ...
# (実際は数十行のIAMポリシー定義が必要)
▼ CDK の場合
# RDSインスタンス生成時、Credentialsに指定するだけで Secrets Manager が自動生成される
self.instance = rds.DatabaseInstance(
...,
credentials=rds.Credentials.from_generated_secret("appuser"),
)
# --- 中略 ---
# DBのシークレットをLambdaから読めるようにする「だけ」
db_secret.grant_read(self.function)
Terraformなら数十行にも及ぶIAMポリシーの記述やアタッチ作業が、db_secret.grant_read(self.function) という たった1行のメソッド呼び出しで完結 してしまいます。
トレードオフ(重要): grant_read() の「最小権限の自動付与」は強力ですが、「実際にどのようなIAMポリシーが生成されているか」という可視性が失われます。セキュリティレビューの観点では、この暗黙の付与を cdk synth で必ずIAMポリシーを検証する習慣とセットで運用する必要があります。「楽に書ける = セキュリティが担保される」ではありません。
DBマイグレーション(Custom Resource):ハック vs ファーストクラス
Terraformで初回デプロイ時にDBスキーマを流し込もうとすると、null_resource で local-exec を叩かせたりとかなりハック感が出ます。CDKでは CustomResource がファーストクラスの機能として提供されています。
db_init_provider = cr.Provider(
self, "DbInitProvider", on_event_handler=db_init.function
)
db_init_custom_resource = cdk.CustomResource(
self,
"DbInitCustomResource",
service_token=db_init_provider.service_token,
)
# RDSの構築完了を待ってから実行する
db_init_custom_resource.node.add_dependency(rds.instance)
デプロイフローの中でAWS側にLambdaを実行させ、結果をCloudFormationのステータスに反映させることができます。Terraformでは local-exec がローカルマシン依存になるのに対し、CDKのCustomResourceはAWS側で完結するため、CI/CD環境との親和性が高いです。
技術選定と結論
ここからはスタッフエンジニアの視点で、組織における技術選定の意思決定モデルを整理します。「どちらが優れているか」ではなく、「どのコンテキストでどちらを選ぶべきか」という問いへの回答です。
フルマネージド / サーバーレス領域における「開発スピード」の差
TerraformでAPI GatewayやLambda、DynamoDBといったフルマネージドサービスを中心としたアーキテクチャを構築しようとすると、リソース本体に加えて大量のIAMロール、ポリシーごとのアタッチ、ロググループ、APIのメソッド設定などをHCLで律儀に手書きしなければならず、開発スピードが著しく低下します。
一方CDKは、grant_read() のような気の利いたメソッド(L2 Construct)によって必要な周辺設定をよしなに生成してくれるため、フルマネージド領域での開発ベロシティは圧倒的です。この差は規模が大きくなるほど顕著になります。
「アプリケーションと同じ言語で書ける」ことの組織的意味
CDKのもう一つの強力なメリットは、バックエンドのアプリケーションコードと同じ言語(今回はPython)でインフラを書ける 点です。エンジニアのコンテキストスイッチが不要になるだけでなく、IDEの強力な型補完、リンター、テストフレームワーク(pytest等)をそのまま流用できます。
組織的に重要なのは、アプリケーションエンジニアがインフラを所有できる ようになる点です。「インフラはインフラ専任チームが管理する」という組織構造から、「各機能チームが自分たちのインフラをオーナーとして管理する」というDevOps的なモデルへの移行を、技術面で支援します。インフラとアプリの距離感が縮まり、アプリケーションエンジニアがインフラを見るハードルを大きく下げてくれます。
技術選定の意思決定モデル
| 評価軸 | Terraform(明示性の価値) | AWS CDK(生産性・テストの価値) |
|---|---|---|
| 学習コスト | HCLのルールを覚えればインフラ知識だけで書ける | インフラ知識 + プログラミング言語の「設計思想」が必要 |
| 暗黙挙動 | ほぼ無し。書いた分だけリソースができる | 非常に多い。cdk synth 検証が前提 |
| 依存方向の強制 | 仕組み無し(ただの羅列) | クラス設計により物理的に強制可能 |
| 動的なリソース |
count や for_each の構文が辛くなりがち |
Pythonの自然なループ等で自在に記述可能 |
| テスト手法 | terratest / checkov、または plan の目視 | pytest + CDK Assertions でCI自動検証 |
| IAM可視性 | 明示的に書くためレビューが容易 |
grant_* 系で抽象化されるため cdk synth での確認が必須 |
| 組織適用 | インフラ専任チーム、複数クラウド統合管理 | アプリエンジニア主導、フルマネージド中心チーム |
| 向く場面 | 静的・安定した基盤層(VPC、EKS等)の厳格管理 | フルマネージド/サーバーレス中心の高速開発 |
選定基準の整理
意思決定の判断軸として以下を推奨します。
CDKを選ぶべきケース:
- フルマネージド・サーバーレスを中心としたアーキテクチャ
- バックエンドエンジニアがインフラもオーナーとして担当するチーム構成
- インフラのテスト自動化・CI/CD統合を優先する場合
- 動的なリソース生成(環境数が多い、テナント構成等)が必要な場合
Terraformを選ぶべきケース:
- 複数クラウド・複数プロバイダの統合管理が必要な場合
- インフラ専任チームが厳格な変更管理プロセスを持つ場合
- Planの結果を非エンジニアにも説明責任が求められる場合
- 既存のKubernetes/EKSインフラとの統合など静的な基盤管理が主軸の場合
CDKを組織で使うためのガバナンス設計
CDKを組織規模で使いこなすには「ルール」ではなく「ガバナンス設計」が必要です。以下は他チームへ展開できる最小セットです。
-
cdk synthのCI組み込み — デプロイ前のテンプレート検証を必須ゲートとする -
依存方向の物理的強制 —
Network → Entry → Compute → Dataの依存方向をConstructのコンストラクタシグネチャで強制する - Stackのライフサイクル分割 — 安定リソース(VPC、RDS)と可変リソース(Lambda、ECS)を別Stackに分離し、安定リソースのdestroyを禁止する
-
pytestによる自動検証 — ランタイム・ハンドラー・IAM・サブネット配置・Security Groupの方向などをCI上でテストする -
L3 Constructの使用禁止 —
ApplicationLoadBalancedFargateServiceなどの高レベルAPIはStackの透明性を損なうため組織ルールとして禁止する
まとめ
「Pythonで書けるから楽」などと甘く見ていたCDKでしたが、蓋を開けてみれば、その自由さと強烈な暗黙挙動に振り回された日々 でした。
しかし、これは欠陥ではなく「設計責任の移譲」です。TerraformはHCLの文法制約によって設計の自由度を制限しますが、CDKはその制限をPythonエンジニアの設計思想に委ねます。「HCLが強制してくれないのなら、Pythonのクラス設計で自らルールを作ればいいのだ」と切り替えてからは世界が変わりました。Constructをレイヤーごとに分割し、依存を一方向に強制し、ライフサイクルでStackを分ける。そうしたベストプラクティスを確立してからは、むしろ Terraform以上に柔軟かつ厳密なインフラ構築 が可能になっています。
TerraformとCDKは競合ではなく、解決しようとしている問題の層が異なるツールです。組織として最大の価値を引き出すには「どちらが正しいか」ではなく「どのコンテキストでどちらを使うか」という使い分けの原則を確立することが、スタッフエンジニアとしての設計判断の核心です。
Terraformの「安心感」も捨てがたいですが、ルールさえ敷ければCDKの「表現力」は強力無比です。インフラを「構成」するのではなく「プログラミング」する。ルールを「規約に書く」のではなく「コードで強制する」。その発想の転換こそが、CDKをただのツールから 組織の設計力の武器 に変える鍵です。