AWS CDK(Cloud Development Kit)は Infrastructure as Code を実現するための強力なツールですが、プロジェクトが大きくなるにつれてコードの管理が難しくなることがあります。適切なディレクトリ構造を採用することで、開発効率の向上、コードの再利用性の促進、メンテナンス性の向上が期待できます。この記事では、CDK開発における効率的なディレクトリ構造と、実践的なベストプラクティスについて解説します。
目次
- CDKプロジェクトの基本構造解説
- 推奨ディレクトリ構造パターン
- マルチスタック環境のディレクトリ設計
- コンストラクトの分割と再利用
- テスト用ディレクトリの構成
- 環境別設定の管理方法
- ベストプラクティスと反面教師パターン
- 終わりに
1. CDKプロジェクトの基本構造解説
CDKプロジェクトをcdk initコマンドで作成すると、以下のような基本的なディレクトリ構造が生成されます(TypeScriptの場合):
my-cdk-project/
├── .git/
├── .gitignore
├── .npmignore
├── README.md
├── bin/
│ └── my-cdk-project.ts
├── cdk.json
├── jest.config.js
├── lib/
│ └── my-cdk-project-stack.ts
├── node_modules/
├── package-lock.json
├── package.json
├── test/
│ └── my-cdk-project.test.ts
└── tsconfig.json
各ディレクトリ・ファイルの役割:
- bin/ - アプリケーションのエントリーポイント。CDKアプリケーションとスタックの初期化が行われる
- lib/ - スタックやコンストラクトの定義を格納
- test/ - テストコードを格納
- cdk.json - CDKアプリ実行時の設定
- package.json - プロジェクトの依存関係やスクリプト定義
この基本構造をベースに、プロジェクトの規模や要件に応じて拡張していくことになります。
2. 推奨ディレクトリ構造パターン
基本構造は単純なプロジェクトには適していますが、実際の業務プロジェクトでは以下のような拡張された構造が効果的です:
my-cdk-project/
├── bin/
│ └── app.ts
├── lib/
│ ├── constructs/ # 再利用可能なカスタムコンストラクト
│ │ ├── database/
│ │ ├── network/
│ │ └── compute/
│ ├── stacks/ # スタック定義
│ │ ├── network-stack.ts
│ │ ├── database-stack.ts
│ │ └── application-stack.ts
│ └── aspects/ # CDKアスペクト(クロスカッティングコンサーン)
├── config/ # 環境ごとの設定ファイル
│ ├── dev.json
│ ├── staging.json
│ └── prod.json
├── scripts/ # 開発/デプロイメント用スクリプト
├── resources/ # Lambda関数のソースコードなど
│ └── lambda/
├── test/
│ ├── unit/ # 単体テスト
│ ├── integration/ # 統合テスト
│ └── snapshot/ # スナップショットテスト
└── cdk.context.json # CDKコンテキスト値
この構造の利点:
- 関心事の分離 - コンストラクト、スタック、設定などが明確に分離されている
- 再利用性の向上 - コンストラクトを適切に分割することで、異なるスタック間での再利用が容易になる
- 環境別設定の管理 - 開発、ステージング、本番環境ごとの設定ファイルを分けて管理できる
- テストの構造化 - 単体テスト、統合テスト、スナップショットテストなどを整理できる
3. マルチスタック環境のディレクトリ設計
大規模プロジェクトでは、複数のスタックを管理する必要があります。以下はマルチスタック環境におけるディレクトリ設計の例です:
lib/
├── stacks/
│ ├── network/
│ │ ├── vpc-stack.ts
│ │ └── security-group-stack.ts
│ ├── data/
│ │ ├── rds-stack.ts
│ │ └── s3-stack.ts
│ └── application/
│ ├── ecs-service-stack.ts
│ └── lambda-stack.ts
└── pipeline/
└── pipeline-stack.ts # CI/CDパイプラインスタック
マルチスタック環境では、以下の点に注意することをお勧めします:
- 依存関係の明確化 - スタック間の依存関係を明示的に定義し、デプロイ順序を管理
-
スタック間の参照管理 -
CfnOutputなどを活用して、スタック間でリソース情報を共有 - ネスト構造の適切な深さ - ディレクトリ階層が深すぎると可読性が低下するため、3〜4階層程度を目安にする
スタック間でパラメータを受け渡す例:
// network/vpc-stack.ts
export class VpcStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, 'MainVpc', {
// VPC設定
});
// VPC IDをエクスポート
new cdk.CfnOutput(this, 'VpcId', {
value: this.vpc.vpcId,
exportName: `${this.stackName}-VpcId`,
});
}
}
// application/ecs-service-stack.ts
export class EcsServiceStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, vpc: ec2.Vpc, props?: cdk.StackProps) {
super(scope, id, props);
// 前のスタックからVPCを受け取って使用
const cluster = new ecs.Cluster(this, 'Cluster', {
vpc: vpc,
});
// クラスター設定...
}
}
// bin/app.ts
const vpcStack = new VpcStack(app, 'VpcStack');
new EcsServiceStack(app, 'EcsServiceStack', vpcStack.vpc);
4. コンストラクトの分割と再利用
コンストラクトは CDK の基本構成要素です。適切に分割することで再利用性が高まり、コードの保守性が向上します。
4.1 コンストラクトの階層分け
lib/
├── constructs/
│ ├── database/
│ │ ├── aurora-cluster.ts
│ │ └── dynamodb-table.ts
│ ├── network/
│ │ ├── vpc.ts
│ │ └── security-groups.ts
│ └── application/
│ ├── lambda-function.ts
│ └── api-gateway.ts
4.2 L3コンストラクトの作成例
特定のユースケース向けにカスタマイズしたL3コンストラクトの例:
// lib/constructs/database/aurora-cluster.ts
export interface AuroraServerlessProps {
vpc: ec2.IVpc;
backupRetentionDays?: number;
scalingConfiguration?: rds.ServerlessScalingOptions;
}
export class AuroraServerlessCluster extends Construct {
public readonly cluster: rds.ServerlessCluster;
public readonly secretArn: string;
constructor(scope: Construct, id: string, props: AuroraServerlessProps) {
super(scope, id);
const { vpc, backupRetentionDays = 7, scalingConfiguration } = props;
this.cluster = new rds.ServerlessCluster(this, 'Database', {
engine: rds.DatabaseClusterEngine.AURORA_POSTGRESQL,
parameterGroup: rds.ParameterGroup.fromParameterGroupName(
this, 'ParameterGroup', 'default.aurora-postgresql10'
),
vpc,
scaling: scalingConfiguration || {
autoPause: Duration.minutes(10),
minCapacity: rds.AuroraCapacityUnit.ACU_2,
maxCapacity: rds.AuroraCapacityUnit.ACU_16,
},
backupRetention: Duration.days(backupRetentionDays),
deletionProtection: true,
});
this.secretArn = this.cluster.secret!.secretArn;
}
}
// 使用例
const database = new AuroraServerlessCluster(this, 'MyDatabase', {
vpc: myVpc,
backupRetentionDays: 14
});
コンストラクト設計のコツ:
- 単一責任の原則 - 各コンストラクトは1つの責任を持つべき
- 適切な粒度の選定 - 小さすぎると管理が煩雑に、大きすぎると再利用性が低下
- 明確なインターフェース - Props型を使った入力パラメータの明確化
- デフォルト値の提供 - 必要最小限のパラメータだけで使えるようにする
5. テスト用ディレクトリの構成
CDKではテストが重要です。以下はテストディレクトリの効果的な構成例です:
test/
├── unit/ # 単体テスト
│ ├── constructs/ # 個別コンストラクトのテスト
│ │ ├── database/
│ │ └── network/
│ └── stacks/ # スタックのテスト
│ ├── network-stack.test.ts
│ └── database-stack.test.ts
├── integration/ # 統合テスト
│ └── app.test.ts
└── snapshot/ # スナップショットテスト
├── __snapshots__/ # 生成されたスナップショット
└── stacks.test.ts
ユニットテストの例:
// test/unit/constructs/database/aurora-cluster.test.ts
test('AuroraServerlessCluster creates a cluster with correct properties', () => {
// ARRANGE
const app = new cdk.App();
const stack = new cdk.Stack(app, 'TestStack');
const vpc = new ec2.Vpc(stack, 'TestVPC');
// ACT
new AuroraServerlessCluster(stack, 'TestCluster', {
vpc,
backupRetentionDays: 14
});
// ASSERT
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::RDS::DBCluster', {
Engine: 'aurora-postgresql',
BackupRetentionPeriod: 14,
DeletionProtection: true
});
});
テスト戦略のポイント:
- カバレッジ目標の設定 - プロジェクト規模に応じて適切なカバレッジ目標を設定
- テストピラミッド - 単体テスト > 統合テスト > スナップショットテストの優先順位
- 自動化 - CI/CDパイプラインにテストを組み込む
- スナップショットテスト - 意図しないCloudFormationテンプレートの変更を検出する強力なツール
6. 環境別設定の管理方法
複数環境(開発・ステージング・本番)での設定を効率的に管理する方法について解説します。
6.1 設定ファイル構造
config/
├── common.json # 共通設定
├── dev.json # 開発環境固有設定
├── staging.json # ステージング環境固有設定
└── prod.json # 本番環境固有設定
設定ファイルの例(config/dev.json):
{
"app": {
"name": "my-app-dev",
"env": "dev"
},
"vpc": {
"cidr": "10.0.0.0/16",
"maxAzs": 2
},
"database": {
"instanceType": "t3.small",
"backupRetentionDays": 3,
"deletionProtection": false
}
}
6.2 設定の読み込みと適用
// lib/config.ts
import * as fs from 'fs';
import * as path from 'path';
import { mergeWith, isArray } from 'lodash';
export interface AppConfig {
app: {
name: string;
env: string;
};
vpc: {
cidr: string;
maxAzs: number;
};
database: {
instanceType: string;
backupRetentionDays: number;
deletionProtection: boolean;
};
}
export function loadConfig(environment: string): AppConfig {
// 共通設定を読み込み
const commonConfigPath = path.join(__dirname, '../config/common.json');
const commonConfig = JSON.parse(fs.readFileSync(commonConfigPath, 'utf8'));
// 環境固有設定を読み込み
const envConfigPath = path.join(__dirname, `../config/${environment}.json`);
const envConfig = JSON.parse(fs.readFileSync(envConfigPath, 'utf8'));
// 設定をマージ(配列は上書きではなく結合)
return mergeWith(commonConfig, envConfig, (objValue, srcValue) => {
if (isArray(objValue)) {
return srcValue;
}
});
}
// 使用例 - bin/app.ts
const app = new cdk.App();
const environment = app.node.tryGetContext('env') || 'dev';
const config = loadConfig(environment);
new DatabaseStack(app, `${config.app.name}-database`, {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
instanceType: config.database.instanceType,
backupRetentionDays: config.database.backupRetentionDays,
deletionProtection: config.database.deletionProtection,
});
環境ごとのデプロイコマンド例:
# 開発環境へのデプロイ
cdk deploy --context env=dev '*'
# 本番環境へのデプロイ
cdk deploy --context env=prod '*'
7. ベストプラクティスと反面教師パターン
CDKプロジェクトのディレクトリ構造におけるベストプラクティスと避けるべきパターンを紹介します。
ベストプラクティス
| ベストプラクティス | 説明 |
|---|---|
| 機能別ディレクトリ分割 | 関連する機能ごとにディレクトリを分割し、コードの発見性を向上させる |
| 浅い階層構造 | ディレクトリ階層は3〜4階層程度に抑え、複雑さを軽減する |
| 命名規則の一貫性 | ファイル名やディレクトリ名に一貫した命名規則を適用する(例:ケバブケース、キャメルケースなど) |
| 設定と実装の分離 | 環境固有の設定は別ファイルで管理し、コードから分離する |
| テストカバレッジの確保 | コンストラクトやスタックに対する適切なテストを作成する |
避けるべきパターン
| 避けるべきパターン | 問題点 | 改善策 |
|---|---|---|
| 単一ファイルでの巨大スタック | 可読性低下、メンテナンス困難 | 複数のスタックやコンストラクトに分割 |
| 過度に深いディレクトリ階層 | ナビゲーションが困難、パス指定が煩雑 | 階層を浅く保ち、意味のあるグループ化を行う |
| ハードコードされた環境固有値 | 環境間の切り替えが困難、設定変更にコード修正が必要 | 設定ファイルや環境変数の活用 |
| テストなしの開発 | 品質低下、回帰バグの発生 | テスト駆動開発の採用、少なくとも主要部分のテスト作成 |
| コピー&ペーストでの再利用 | コード重複、変更時の修正漏れ | 再利用可能なコンストラクトの作成 |
プロジェクト成長に伴うリファクタリングの指針:
- 段階的な分割 - 一度に全てを変更せず、機能ごとに段階的にリファクタリング
- テスト先行 - リファクタリング前にテストを充実させ、変更の安全性を確保
- 依存関係の見直し - モジュール間の依存関係を明確にし、循環参照を排除
- ドキュメント更新 - ディレクトリ構造の変更に伴い、README等のドキュメントを更新
8. 終わりに
適切なディレクトリ構造は、CDKプロジェクトの成功に不可欠な要素です。この記事で紹介したパターンとベストプラクティスを参考に、プロジェクトに最適な構造を検討してみてください。
重要なポイントをまとめると:
- プロジェクトの規模や要件に合わせて構造を選択する
- 関心事の分離を意識し、コードの再利用性を高める
- 環境固有の設定は設定ファイルとして外部化する
- テストを重視し、品質を担保する
- 成長に合わせて適宜リファクタリングを行う
CDKプロジェクトはアプリケーション開発に近い側面を持ちますが、インフラストラクチャとしてのベストプラクティスも考慮する必要があります。両者のバランスを取りながら、保守性と拡張性に優れた構造を目指しましょう。
次のステップ
- AWS CDKワークショップで実践的なスキルを磨く
- 既存のCDKプロジェクトを分析し、構造のパターンを学ぶ
- 自分のプロジェクトでリファクタリングを試みる
参考文献・参考サイト
- 「AWS CDKガイドブック」AWS開発チーム, AWS Documentation, https://docs.aws.amazon.com/cdk/latest/guide/
- 「AWS CDKベストプラクティス」AWS Documentation, https://docs.aws.amazon.com/cdk/latest/guide/best-practices.html
- 「AWS CDKワークショップ」AWS, https://cdkworkshop.com/
- Nathan Peck「CDK設計パターン」AWS開発者ブログ, 2022, https://aws.amazon.com/blogs/developer/cdk-design-patterns/
- 「TypeScriptプロジェクト構造のガイド」Microsoft, https://github.com/microsoft/TypeScript/wiki/Coding-guidelines


