はじめに
AWS CDKはIaC(Infrastructure as Code)の選択肢として、プロジェクト採用で軍配が上がる場面が増えている。
- TypeScriptで型安全に書ける
- 繰り返し構造を関数・クラス・ループで整理できる
- AWSサービスの推奨構成がL2構築子に集約されている
- JestなどでIaCにユニットテストが書ける
このあたりの開発者体験はCloudFormation(以下CFn)の生書きやTerraformのHCLでは得難い。
ところが、CDKを本格運用し始めると、あるタイミングで必ずこの疑問にぶつかる。
「L2構築子、書いてない設定まで勝手に入ってくるけど、それってIaCの原則に矛盾してないか?」
本記事は、この「L2デフォルト問題」に正面から向き合う。
- IaCの原則からみてL2デフォルトは本当に矛盾なのか
- 矛盾だとすれば(あるいは矛盾でないとすれば)どう運用管理すべきか
- Terraform / CFnは「デフォルト値」をどう扱っているのか
を整理する。
対象読者: CDK(v2)をある程度触っていて、IaCの設計判断に責任を持つ立場の方
IaCの原則を1行でおさらい
「同じコードから、いつでも、誰がapplyしても、同じインフラが再現できること」
この原則を支える具体的な性質はいくつかある。
- 宣言的(Declarative): 最終状態をコードで表現する
- 再現性(Reproducibility): 同じコード → 同じ成果物
- 可読性・監査性: コードを読めば何がデプロイされるか分かる
- バージョン管理との親和性: diffで差分が明示される
この中で、CDK L2のデフォルト値がもっとも「あれ?」となるのが 可読性・監査性 の部分だ。
CDK L2が「勝手にやる」ことの正体
まずは具体例で見る。次のコードは、ただVPCを1つ作るだけの実に素朴なCDKコードだ。
import * as ec2 from 'aws-cdk-lib/aws-ec2';
new ec2.Vpc(this, 'MyVpc');
たった1行。しかし cdk synth すると、吐き出されるCloudFormationテンプレートは数百行になる。
- 複数AZにまたがるPublic/Privateサブネット
- Internet Gateway
- NAT Gateway(複数AZで複数個)
- Route Table群
- それぞれのAssociation
問題はNAT Gatewayだ。NAT Gatewayは1個あたり月額数千円以上かかる。CDK L2はデフォルトで複数AZに展開するので、「1行でVPC作ろう」と思った開発者は、翌月の請求で目を丸くする。
つまりCDK L2は、
- 書いていない設定を勝手に入れる
- その内容は金銭的・セキュリティ的に無視できない
- そしてそれらはコードを読んだだけではわからない
という性質を持つ。これがIaC原則との緊張を生む本丸である。
じゃあCDK L2はIaCとして「矛盾」なのか?
結論から言う。
矛盾ではない。ただし、"正しい宣言ファイル"はソースコードではなく、synth後のCFnテンプレートである、という理解が前提になる。
CDKは「コンパイラ」である
CDKの本質は、AWSが公式ドキュメントで繰り返し強調しているとおり CloudFormationテンプレートを生成するコンパイラ である。
TypeScriptコード → [cdk synth] → CFnテンプレート → [cdk deploy] → AWSリソース
↑ ↑
これがソース これが宣言的アーティファクト
- ソース = ビルド設計書
- synth後のCFn = 宣言ファイル(最終成果物)
C言語のソースとgccが吐くアセンブリの関係に近い。アセンブリだけ見れば何が起こるかすべて書いてあるが、人間はCで書く。最適化フラグ(CDK L2のデフォルト)でアセンブリは変わるが、最適化フラグを固定すれば再現性は担保される。
「宣言的」の対象レイヤーが違うだけ
| レイヤー | 宣言的か | 可読性 |
|---|---|---|
| CDKソース(.ts) | 部分的(デフォルトは書かれない) | 高い |
| synth後CFn(.json) | 完全に宣言的 | 低い(機械可読) |
| 実環境 | (デプロイ結果) | — |
CDK運用では「コードだけ読めばわかる」は成立しない。かわりに 「コード + synth結果 + CDKバージョン」のセット で宣言が完結する。
じゃあ他のIaCツールはデフォルト値をどうしてるの?
ここがこの議論のキモだ。実は 「デフォルト値」はどのIaCツールにも存在する。存在するレイヤーが違うだけだ。
デフォルト値は3層ある
┌─────────────────────────────────┐
│ L1: IaCツール固有のデフォルト │ ← CDK L2 / Terraform module
├─────────────────────────────────┤
│ L2: IaCプリミティブのデフォルト │ ← CFn Properties / TF resource
├─────────────────────────────────┤
│ L3: AWSサービスAPIのデフォルト │ ← 結局ここで何かしら入る
└─────────────────────────────────┘
どのツールでも、書かなかった項目は必ず"何らかのデフォルト"に着地する。問題は「それがどの層で決まっていて、どれくらい透明か」だけだ。
CloudFormation(生書き)
CFnを直接書く場合、テンプレートに書いていないプロパティは AWSサービスAPIのデフォルト にフォールバックする。
MyBucket:
Type: AWS::S3::Bucket
# 何も書かない
この場合、暗号化・バージョニング・ブロックパブリックアクセスなど、すべてS3サービス側のデフォルトが適用される。2023年以降はS3の新規バケットはデフォルトで暗号化されるようになったが、これは CFnテンプレートではなくAWSサービスAPIの変更 だ。
つまりCFn生書きは「IaCツール層のデフォルトは無いが、サービスAPI層のデフォルトは不可避」という構造になる。
Terraform
Terraformも完全に宣言的かというと、実はそうでもない。
resource "aws_s3_bucket" "example" {
bucket = "my-bucket"
}
このリソースにも、プロバイダが埋めるデフォルト値と、AWS API側のデフォルトの両方が存在する。違いは、
-
Terraformはstateにapply後の実値を保存する →
terraform planで差分として可視化される - スキーマでデフォルト値が明示される(プロバイダドキュメント)
さらに Terraform modules を使い始めると、CDK L2と同等の「隠れデフォルト」が発生する。たとえば terraform-aws-modules/vpc/aws は、デフォルトでNAT Gatewayを作る・作らないなど、数十個のdefault変数を持つ。モジュールを呼ぶだけの .tf を読んでも、最終構成はモジュール実装を読まないとわからない。
TerraformモジュールとCDK L2は、抽象化レイヤーとしてほぼ同質 と見ていい。
CDK L1 / L2 / L3
CDKには3段階の抽象度がある。
| 構築子 | 抽象度 | デフォルト値 | 例 |
|---|---|---|---|
| L1 (Cfn*) | CFnと1:1 | なし(=CFnと同じ) | CfnBucket |
| L2 | AWS推奨のまとまり | あり(セキュアな既定値を志向) | Bucket |
| L3 (Pattern) | ユースケース単位 | 大量にあり | ApplicationLoadBalancedFargateService |
抽象度と「隠蔽されるデフォルトの量」は比例する。L1はCFn生書きとほぼ等価で、IaCの"生"の姿になる。L2/L3は生産性と引き換えにデフォルトが増える。
対照表:結局どのツールで何が起こるのか
| 観点 | CFn生書き | Terraform(resource直書き) | Terraform(module) | CDK L1 | CDK L2 |
|---|---|---|---|---|---|
| ツール層デフォルト | なし | 最小限 | 多い | なし | 多い |
| サービスAPI層デフォルト | あり | あり | あり | あり | あり |
| コード=最終構成の透明性 | 高 | 高 | 低 | 高 | 低 |
| 差分プレビュー | change set | terraform plan |
terraform plan |
change set | cdk diff |
| 記述量 | 多 | 中 | 少 | 多 | 少 |
| セキュアな既定値 | サービス依存 | サービス依存 | モジュール次第 | サービス依存 | 構築子が配慮 |
CDK L2が悪い、という話ではない。抽象度を上げた結果としてデフォルト値が増えるのは、Terraform moduleでも同じ現象が起きる。CDK L2は「セキュアな既定値を構築子レベルで持つ」という点で、むしろデフォルトを"良い方向に効かせようとしている"とも言える(たとえばS3のblockPublicAccess BLOCK_ALLなど)。
CDKをIaCとして健全に運用する実務ルール
「矛盾ではないが、可読性が落ちる」のがCDKの宿命だとしたら、それを運用で補う必要がある。以下、実際に現場で効いているプラクティスをまとめる。
1. CDKバージョンを厳密に固定する
CDKのL2デフォルトは マイナーバージョンでも変わり得る。
// package.json
{
"dependencies": {
"aws-cdk-lib": "2.150.0" // ^ や ~ を付けない
}
}
lockfileと合わせて、CI/CDでは同一バージョンでsynthされることを保証する。
2. Snapshot Testを必ず入れる
「コード変更で意図しないCFn差分が出ていないか」を機械的に検知する。
import { Template } from 'aws-cdk-lib/assertions';
test('VPC stack snapshot', () => {
const app = new App();
const stack = new VpcStack(app, 'Test');
expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
});
L2デフォルトが変わった瞬間、または自分のコード変更で想定外のプロパティが動いた瞬間、snapshot diffが出る。これが 「コード=最終形ではない」問題への最大の対抗策 になる。
3. cdk diff をプルリクに必ず貼る
レビュアが見るべきは .ts の差分ではなく、cdk diff の差分。GitHub ActionsでPRにコメントさせると強い。
- name: CDK Diff
run: npx cdk diff --all > cdk-diff.txt
- uses: actions/github-script@v7
# PR本文にdiffを貼る
「ソース差分は5行だが、CFn差分は300行」みたいな事態がL2では普通に起きる。レビューの対象は後者だ、という合意をチームで取る。
4. セキュリティ・コスト関連は明示する
L2デフォルトに頼るな、特にお金とセキュリティは。
// 悪い: デフォルトに任せる
new ec2.Vpc(this, 'Vpc');
// 良い: NATゲートウェイ数を明示
new ec2.Vpc(this, 'Vpc', {
maxAzs: 2,
natGateways: 1, // ← デフォルトだと maxAzs と同数 = 高額
});
暗号化、パブリックアクセス、ログ保持期間、削除ポリシーあたりは、「暗黙の安全側」に寄りかからず明示するのを運用ルールに入れる。
5. L1へのエスケープハッチを許容する
CDKは「L2で書けないところはL1でオーバーライドできる」という逃げ道を持っている。
const bucket = new s3.Bucket(this, 'B');
const cfnBucket = bucket.node.defaultChild as s3.CfnBucket;
cfnBucket.addPropertyOverride('LoggingConfiguration.LogFilePrefix', 'logs/');
「L2で書きにくくなったらL1に落ちる」は敗北ではない。IaC透明性を優先する判断としてむしろ健全だ。
6. cdk.context.json と環境差分の扱い
CDKはAZ情報などをcontextにキャッシュする。これを .gitignore するか commit するかで再現性が変わる。
- commit する派: 他メンバー / CIで完全一致
- 無視する派: アカウント毎に実値を取得
基本はcommitするのが再現性の観点で安全。
じゃあCDK使う意味あるの?という問いへの自分の答え
ここまで書くと「透明性のためにCFn生書きに戻すべきでは」という議論になる。私の答えはこうだ。
抽象化による生産性の向上 > デフォルト値による透明性の低下
ただし、不等号を成立させるための運用装備(snapshot test / cdk diff / バージョン固定)は必須。
TerraformでModuleを使うなら同じ議論になる。モジュールを使わないTerraformと、L1だけのCDKはほぼ等価で、どちらも可読性は高いが記述量が爆発する。
IaCの本質は「コードが最終形そのもの」ではなく「同じ入力から同じ成果物が再現できる」ことだ。CDKはその定義から外れていない。ただ「同じ入力」の定義に CDKバージョンとsnapshot を含める必要がある、というだけの話だ。
まとめ
- CDK L2のデフォルト値は、一見IaCの「宣言的」原則と矛盾するように見える
- しかしCDKは CFnコンパイラ であり、宣言的アーティファクトはsynth後のCFnテンプレート
- 「デフォルト値」はCFn / Terraform / CDKすべてに存在し、ただ存在する 層 が違うだけ
- Terraform Module ≒ CDK L2 という抽象度の相似がある
- CDKをIaCとして正しく運用するには、バージョン固定・snapshot test・cdk diffレビュー・セキュリティ項目の明示 の4点セットが必要
- 「コードだけ読めば全部わかる」という幻想を捨て、「コード + synth結果 + CDKバージョン」で宣言が完結 と再定義する
抽象化は常にトレードオフだ。CDKを採用した時点で「生産性を取り、透明性は運用で担保する」という契約に署名している、と考えると、デフォルト値問題の付き合い方が見えてくる。
関連トピック
-
cdk diffをGitHub ActionsでPRに貼るワークフローの具体例 - Snapshot Testの運用(スナップショット更新ポリシー)
- L3 Pattern Construct(ECS Fargate Pattern等)のデフォルト値一覧
- CDKで見やすいCFnを作る実装ルール
これらは別記事で書く予定。