CloudFormationの問題点
AWSインフラを管理した経験があれば、その辛さはよく分かるはずだ。シンプルなAPI Gateway + Lambda + DynamoDBの構成でも、500行超えのYAMLに膨れ上がることがある。同じVPC設定を10個のスタックにコピペして、あとで3箇所のCIDRブロックを更新し忘れたことに気づく。リファクタリングはもはや遺跡発掘作業だ。
CloudFormationは機能する。だが、インフラをコードではなく「ドキュメント」として扱う。ループもなく、関数もなく、型チェックもない。何かが壊れたとき、エラーメッセージは意味不明だ。そこそこ複雑なスタックをロールバックする?最低でも20分は覚悟しよう。
問題はCloudFormation自体ではなく、そのパラダイムにある。宣言的YAMLは人間が読めるコンフィグのために設計されたのであって、3つの環境で動く15個のマイクロサービスのためではない。ある規模を超えると、もはやインフラを書いているのではなく、YAMLをフルタイムで保守しているだけになる。
コアコンセプト:AWS CDKが実際に何をするのか
AWS CDK(Cloud Development Kit)はそのモデルを逆転させる。書くのはTypeScriptコードだけ——CDKがデプロイ時にCloudFormationテンプレートを自動的に生成してくれる。
この転換は大きな意味を持つ。IDEの補完機能とコンパイル時の型チェックが使える。本物のループと条件分岐が書ける。インフラをバージョン管理されたnpmパッケージとして共有できる。そして重要なのは、エラーがデプロイ前に表面化すること——20分のロールアウトの途中ではなく。
CDKはリソースを3つのレベルのコンストラクトとして整理する:
- L1(Cfn*クラス):CloudFormationリソースを直接ラップしたもの。完全なコントロールが可能だが冗長。CloudFormation仕様のすべてのプロパティに1対1でマッピングされる。
- L2:デフォルト値とヘルパーメソッドを持つ高レベルの抽象化。作業の90%はこれを使う。
- L3(パターン):ALBの背後のECSサービスなど、一般的な複数リソース構成向けの意見付き定義済みソリューション。
cdk deployを実行すると、CDKがTypeScriptをCloudFormationテンプレートに変換し、デプロイする。YAMLは内部に存在し続けるが、触る必要はまったくない。
生のテンプレートからCDKへ移行したことで、12スタックにわたる約8,000行のYAMLの保守から、どの開発者でも午後一つで読めるTypeScriptコードベースへと変わった。この転換は、チーム全体のインフラに対する考え方を変えた。
実践
前提条件とインストール
Node.js 18以上、認証情報が設定済みのAWS CLI、そして有効なAWSアカウントが必要だ。
npm install -g aws-cdk
cdk --version
アカウント/リージョンのペアごとに一度だけブートストラップを実行する——これでCDKの動作に必要なS3バケットとIAMロールが作成される:
aws configure # アクセスキー、シークレット、デフォルトリージョンを設定
cdk bootstrap aws://YOUR_ACCOUNT_ID/ap-northeast-1
最初のCDKプロジェクトを作成する
mkdir my-infra && cd my-infra
cdk init app --language typescript
実際の作業はこの2つのファイルが担う:
my-infra/
├── bin/
│ └── my-infra.ts # アプリのエントリーポイント — スタックをインスタンス化する
├── lib/
│ └── my-infra-stack.ts # スタック定義 — リソースが定義される場所
├── cdk.json
└── package.json
実践的なスタックの構築:S3 + Lambda + API Gateway
lib/my-infra-stack.tsを開き、デフォルトの内容を実用的なパターンに置き換えよう:
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { Construct } from 'constructs';
export class MyInfraStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// バージョニングとサーバーサイド暗号化を有効にしたS3バケット
const bucket = new s3.Bucket(this, 'StorageBucket', {
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
removalPolicy: cdk.RemovalPolicy.DESTROY, // 本番環境ではRETAINに変更すること
});
// Lambdaハンドラー
const handler = new lambda.Function(this, 'ApiHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
code: lambda.Code.fromAsset('src/lambda'),
handler: 'index.handler',
environment: {
BUCKET_NAME: bucket.bucketName,
},
});
// 1行で適切なIAMポリシーを自動的に付与する
bucket.grantReadWrite(handler);
// LambdaにつないだAPI Gateway
const api = new apigateway.RestApi(this, 'MyApi', {
restApiName: 'マイサービスAPI',
});
api.root.addMethod('GET', new apigateway.LambdaIntegration(handler));
new cdk.CfnOutput(this, 'ApiUrl', { value: api.url });
}
}
bucket.grantReadWrite(handler)の呼び出しは実際に重要な処理をしている。適切なIAMポリシーを構築し、Lambda実行ロールに自動でアタッチするのだ。生のCloudFormationで同じことをすると15〜20行のYAMLが必要になり、バケット名やロールのARNが変わるたびに手動で更新しなければならない。
Lambdaハンドラーを作成する:
mkdir -p src/lambda
// src/lambda/index.ts
export const handler = async (event: any) => {
return {
statusCode: 200,
body: JSON.stringify({
message: 'CDKからこんにちは!',
bucket: process.env.BUCKET_NAME,
}),
};
};
デプロイ前にプレビューする
闇雲にデプロイしてはいけない。まずdiffを確認しよう:
cdk diff # リソースの変更を表示する — インフラにおけるgit diffのようなもの
cdk synth # 生のCloudFormationテンプレートを生成して確認できる
diffに問題がなければ、デプロイしよう:
cdk deploy
CDKはIAMパーミッションの変更があれば確認を求める——意図的な安全策だ。その後、デプロイの進捗がターミナルに直接ストリーミングされる。
重複なしのマルチ環境サポート
ここでCDKが手書きテンプレートより明らかに優れている点が分かる。アプリレベルでスタックをパラメータ化しよう:
// bin/my-infra.ts
import * as cdk from 'aws-cdk-lib';
import { MyInfraStack } from '../lib/my-infra-stack';
const app = new cdk.App();
const env = app.node.tryGetContext('env') || 'dev';
new MyInfraStack(app, `MyInfra-${env}`, {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
フラグ1つでどの環境にもデプロイできる:
cdk deploy -c env=staging
cdk deploy -c env=prod
再利用可能なコンストラクト:CDKの真骨頂
複数のスタックを構築し始めると、同じパターンがあちこちに現れることに気づく。CDKではそれらをチームの標準を組み込んだ合成可能なコンストラクトに抽出できる:
// lib/constructs/monitored-lambda.ts
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import { Construct } from 'constructs';
interface MonitoredLambdaProps extends lambda.FunctionProps {}
export class MonitoredLambda extends Construct {
public readonly function: lambda.Function;
constructor(scope: Construct, id: string, props: MonitoredLambdaProps) {
super(scope, id);
this.function = new lambda.Function(this, 'Function', {
...props,
tracing: lambda.Tracing.ACTIVE, // X-Rayを常時有効化
});
// すべてのLambdaに自動でエラーレートアラームを設定
new cloudwatch.Alarm(this, 'ErrorAlarm', {
metric: this.function.metricErrors(),
threshold: 5,
evaluationPeriods: 1,
});
}
}
これ以降、チームが起動するすべてのLambdaにはデフォルトでX-RayトレーシングとCloudWatchエラーアラームが設定される。誰も忘れない。スタック間の設定のずれも発生しない。
リソースの削除
cdk destroy
CDKがプロビジョニングしたものをすべて削除する。RemovalPolicy.RETAINでマークされたリソース——たとえば本番データベース——は削除されず、警告とともにスキップされる。
CDKが有効な場合と有効でない場合
CDKがその価値を発揮するのは、インフラの複雑さが抽象化を正当化するときだ。環境をまたぐ繰り返しパターン、TypeScriptやPythonを既に知っているチーム、インフラをバージョン管理されたパッケージとして配布する必要性——このうちどれか一つでも当てはまれば傾く。三つすべて?迷う必要はない。
ほとんど変更のない少数の静的リソースには、生のCloudFormationやAWSコンソールの方が本当にシンプルだ。余計な手続きは不要。チームがTerraformに慣れているなら、そのHCL構文と豊富なモジュールレジストリの方が合うかもしれない——CDKはAWS専用なので、特にマルチクラウドの構成では。
しかし、AWSで本格的なスケールでサービスを展開しているチームにとって、CDKには積み重なる優位性がある:インフラが本物のコードベースになるのだ。aws-cdk-lib/assertionsでユニットテストを書ける。プルリクエストでインフラの変更をレビューできる。本番に当たる前にリグレッションを検出できる。
CDKを最大限に活用しているチームは、lib/をアプリケーションコードと同じように扱う——合成可能なコンストラクト、テストカバレッジ、意味のあるコミット履歴。dev、staging、prodにわたる30以上のスタックを管理するとき、その規律はすぐに報われる。新しいエンジニアが一度のセッションでアーキテクチャ全体を把握できるようになるのだ。

