1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CDK L2の"勝手に入るデフォルト値"ってIaCとして矛盾してない? — Terraform / CloudFormationと比較して本気で考える

1
Posted at

はじめに

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を作る実装ルール

これらは別記事で書く予定。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?