AWS CDK(v2)でインフラをコードとして開発・運用するときのベストプラクティスをまとめたものです。
目次
- 基本方針
- プロジェクト構成
- Construct の設計
- Stack の設計
- 環境・設定の管理
- リソースの命名とステートフルリソース
- セキュリティ
- テスト
- デプロイと CI/CD
- コスト・運用
- よくあるアンチパターン
- チェックリスト
1. 基本方針
- インフラとアプリケーションコードを同じリポジトリで管理する 一緒に変更されるものは一緒にバージョン管理・デプロイします。Lambda のコードとその実行基盤を同じ PR でレビューできるのが理想です。
- 合成(synth)時に判断し、デプロイ時に判断しない
CfnConditionやFn.conditionIfなどの CloudFormation 側の条件分岐より、プログラミング言語のif文で分岐させます。合成結果の CloudFormation テンプレートは決定的(deterministic)であるべきです。 - CloudFormation の機能より CDK の抽象化を優先する
CfnParameterやConditionsは基本的に使わず、コンテキストや Props で値を渡します。 - L2 / L3 Construct を優先して使う
L1(
Cfn*)はL2で表現できない場合の最終手段とします。L2 で足りないプロパティはエスケープハッチ(node.defaultChild、addPropertyOverride)で補います。 - CDK のバージョンを固定し、定期的に更新する
aws-cdk-libと CLI(aws-cdk)のバージョンをそろえ、Dependabot / Renovate で継続的に更新します。
2. プロジェクト構成
推奨ディレクトリ構成(TypeScript の例)
my-app/
├── bin/
│ └── app.ts # エントリポイント(App と Stack のインスタンス化のみ)
├── lib/
│ ├── stacks/ # Stack 定義
│ │ ├── network-stack.ts
│ │ ├── database-stack.ts
│ │ └── api-stack.ts
│ ├── constructs/ # 再利用可能な独自 Construct
│ │ └── secure-bucket.ts
│ └── config/ # 環境ごとの設定
│ └── environments.ts
├── src/ # Lambda などのアプリケーションコード
│ └── handlers/
├── test/ # テスト
├── cdk.json
└── package.json
ポイント
bin/app.tsはできるだけ薄くし、Stack を組み立てるだけにします。- ロジックは Construct に寄せ、Stack は Construct を組み合わせるだけの「デプロイ単位」と考えます。
- 複数チームで共有する Construct は、別パッケージ(社内 npm / Construct Hub / Projen など)として切り出しバージョン管理します。
3. Construct の設計
Construct を「論理的な単位」としてモデル化する
export interface SecureBucketProps {
readonly bucketName?: string;
readonly retainOnDelete?: boolean;
}
export class SecureBucket extends Construct {
public readonly bucket: s3.Bucket;
constructor(scope: Construct, id: string, props: SecureBucketProps = {}) {
super(scope, id);
this.bucket = new s3.Bucket(this, 'Bucket', {
bucketName: props.bucketName,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
versioned: true,
removalPolicy: props.retainOnDelete === false
? RemovalPolicy.DESTROY
: RemovalPolicy.RETAIN,
});
}
}設計指針
- Props は
interface+readonlyで定義する。必須項目を最小限にし、安全なデフォルト値を持たせます。 - 環境変数を Construct の中で読まない。
process.envの参照はbin/app.ts(最上位)だけにし、値は Props で渡します。 - Construct の論理 ID(
id)を安易に変更しない。ID が変わるとリソースが作り直され、ステートフルリソースではデータ消失につながります。 - 必要なものを公開プロパティとして出す(
public readonly bucketなど)。他の Construct が参照できるようにします。 - 権限付与は
grant*メソッドを使う(bucket.grantRead(fn)など)。IAM ポリシーを手書きするより最小権限になりやすく、読みやすくなります。 - 組織共通のルールは Construct に埋め込むか、Aspects で強制します。
4. Stack の設計
分割の考え方
- ステートフル(DB、S3、KMS など)とステートレス(Lambda、API Gateway、ECS サービスなど)で Stack を分ける ステートレス側は気軽に作り直せる一方、ステートフル側には削除保護をかけて慎重に扱えます。
- ライフサイクルとデプロイ頻度で分ける ネットワーク(VPC)のように変更頻度が低いものと、アプリのように頻繁に変わるものを分離します。
- Stack を細かくしすぎない 分割しすぎるとクロススタック参照が増え、依存関係の管理が難しくなります。迷ったら少なめに。
- CloudFormation のリソース上限(1スタック500リソース)を意識する
Stack 間の参照
- 同一 App 内では Stack のプロパティとして Construct を渡す のが基本です(CDK が自動で Export/Import を生成します)。
const db = new DatabaseStack(app, 'Database', { env });
new ApiStack(app, 'Api', { env, table: db.table });- 自動生成された Export は、参照元が使っている間は削除できない(「Export がロックされる」)点に注意します。参照を外すときは、2段階のデプロイ(
this.exportValue()で一時的に Export を残す)が必要です。 - 疎結合にしたい場合や別 App 間では、SSM Parameter Store 経由で値を受け渡します。
Stack に付けておきたい設定
new ApiStack(app, 'Api-prod', {
env: { account: '123456789012', region: 'ap-northeast-1' },
terminationProtection: true,
description: 'API layer for my-app (prod)',
});5. 環境・設定の管理
アカウント・リージョンを明示する
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
};本番では、アカウント ID とリージョンをハードコード(設定ファイルに記載)して、意図しないアカウントへのデプロイを防ぎます。env を指定しない「環境非依存 Stack」は Vpc.fromLookup などが使えず、制約が多くなります。
環境ごとの設定は型付きで管理する
// lib/config/environments.ts
export interface EnvConfig {
readonly account: string;
readonly region: string;
readonly instanceType: string;
readonly minCapacity: number;
}
export const environments: Record<'dev' | 'stg' | 'prod', EnvConfig> = {
dev: { account: '111111111111', region: 'ap-northeast-1', instanceType: 't3.micro', minCapacity: 1 },
stg: { account: '222222222222', region: 'ap-northeast-1', instanceType: 't3.small', minCapacity: 1 },
prod: { account: '333333333333', region: 'ap-northeast-1', instanceType: 'm6i.large', minCapacity: 3 },
};cdk deploy -c env=prodその他
cdk.context.jsonはコミットする。fromLookupの結果がキャッシュされ、合成結果が安定します。- 環境はアカウントで分離する(dev / stg / prod を別アカウントに)。AWS Organizations と組み合わせるのが推奨です。
- シークレットはコードや
cdk.jsonに書かない。Secrets Manager や SSM SecureString を参照します。
6. リソースの命名とステートフルリソース
物理名は可能な限り自動生成に任せる
bucketName や tableName などを固定すると、
- 置き換えを伴う変更ができない(同名リソースが作れない)
- 同じ Stack を複数環境・複数リージョンにデプロイできない
といった問題が起きます。名前が必要なら、参照する側に環境変数などで渡します。
fn.addEnvironment('TABLE_NAME', table.tableName);削除ポリシーとデータ保護
- ステートフルリソースは
RemovalPolicy.RETAIN(またはSNAPSHOT) を明示します。 - DynamoDB のポイントインタイムリカバリ、RDS の削除保護・自動バックアップ、S3 のバージョニングを有効にします。
- 開発環境だけ
DESTROY/autoDeleteObjects: trueにするなど、環境で切り替えます。 - 論理 ID の変化を検知する:
cdk diffで「replace」が出ていないかを必ず確認します。リファクタリング時はoverrideLogicalIdで ID を維持するか、CDK のリファクタ機能(cdk refactor)を活用します。
ログの保持期間
Lambda などのロググループは保持期間を明示します(デフォルトは無期限でコストが膨らみます)。
new lambda.Function(this, 'Fn', {
// ...
logGroup: new logs.LogGroup(this, 'FnLogs', {
retention: logs.RetentionDays.ONE_MONTH,
}),
});7. セキュリティ
- 最小権限:
grant*メソッドを優先し、*リソース・*アクションの付与を避けます。 - cdk-nag を導入する:AWS Solutions などのルールパックで、セキュリティ上の問題を合成時に検出します。
import { AwsSolutionsChecks } from 'cdk-nag';
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));抑制(NagSuppressions)する場合は、必ず理由を書きます。
- Permissions Boundary / IAM ロールの制約:組織で必要なら
PermissionsBoundary.of(scope).apply(...)で一括適用します。 - Bootstrap の権限を絞る:
cdk bootstrapの--cloudformation-execution-policiesで、デフォルトのAdministratorAccessから必要な権限に絞ることを検討します。クロスアカウントデプロイでは--trustを必要なアカウントだけにします。 - 暗号化をデフォルトにする:S3、EBS、RDS、SQS、SNS などは暗号化を有効にします。
- パブリックアクセスを遮断する:S3 の
BLOCK_ALL、enforceSSL: trueなど。 - セキュリティグループは必要なポートとソースだけ許可する:
allowAllOutboundもデフォルトのままでよいか検討します。 - IAM の変更を確認する:
cdk diffの IAM / セキュリティグループの変更表示と、cdk deployの承認プロンプト(--require-approval)を活用します。
8. テスト
テストの種類
| 種類 | 目的 | 手段 |
|---|---|---|
| スナップショットテスト | 意図しないテンプレート変更の検出 | Template.fromStack + Jest のスナップショット |
| ファイングレインドアサーション | 特定リソースやプロパティの存在を検証 | hasResourceProperties、resourceCountIs |
| バリデーション | 不正な Props を合成時に弾く | node.addValidation()、コンストラクタ内のチェック |
| ポリシーチェック | セキュリティ・コンプライアンス | cdk-nag、CloudFormation Guard |
| 統合テスト | 実際にデプロイして動作確認 | @aws-cdk/integ-tests-alpha、integ-runner |
例
import { App } from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
test('S3 バケットは暗号化され、パブリックアクセスが遮断されている', () => {
const app = new App();
const stack = new StorageStack(app, 'Test');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::S3::Bucket', {
BucketEncryption: Match.objectLike({}),
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
});
});ポイント
- スナップショットテストだけに頼らない。差分が大きいと「とりあえず更新」になりがちなので、重要な性質はアサーションで明示的にテストします。
- アセットのハッシュなど変わりやすい値はスナップショットから除外・マスクします。
- CI で
cdk synthとテストを必ず実行します。
9. デプロイと CI/CD
- 本番へのデプロイはパイプライン経由に限定する。手元からの
cdk deployは開発環境だけにします。 - CDK Pipelines(
aws-cdk-lib/pipelines) や GitHub Actions を使い、Stage単位で dev → stg → prod とプロモーションします。
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
synth: new pipelines.ShellStep('Synth', {
input: pipelines.CodePipelineSource.connection('org/repo', 'main', { connectionArn }),
commands: ['npm ci', 'npm test', 'npx cdk synth'],
}),
});
pipeline.addStage(new AppStage(this, 'Stg', { env: stgEnv }));
pipeline.addStage(new AppStage(this, 'Prod', { env: prodEnv }), {
pre: [new pipelines.ManualApprovalStep('PromoteToProd')],
});cdk diffを PR で可視化する。レビュー時に変更内容(特に replace と IAM 変更)を確認できるようにします。- GitHub Actions からは OIDC で AWS 認証する。長期のアクセスキーを使いません。
- ロールバックとアラーム:Lambda なら CodeDeploy の段階的デプロイ、ECS なら Circuit Breaker を有効にします。
cdk watch/--hotswapは開発環境専用。本番では CloudFormation を経由しない更新になるため使いません。- ドリフト検出:コンソールでの手動変更は避け、定期的にドリフトを確認します。
10. コスト・運用
- タグを一括付与する:
Tags.of(app).add('Project', 'my-app')で、コスト配分やリソース管理を楽にします。 - Aspects で組織ルールを強制する:タグの付与漏れ、暗号化漏れなどを合成時に検出・修正します。
class RequireLogRetention implements IAspect {
visit(node: IConstruct) {
if (node instanceof logs.CfnLogGroup && node.retentionInDays === undefined) {
Annotations.of(node).addError('LogGroup に retention を設定してください');
}
}
}
Aspects.of(app).add(new RequireLogRetention());- 監視も CDK で定義する:CloudWatch アラームやダッシュボードをリソースと一緒に定義します(
cdk-monitoring-constructsなども有効)。 - NAT Gateway の数に注意:開発環境では
natGateways: 1にするなどしてコストを抑えます。 - 不要になった開発環境は
cdk destroyで片付ける。
11. よくあるアンチパターン
| アンチパターン | 問題 | 推奨 |
|---|---|---|
Construct 内で process.env を読む | 再利用性・テスト性が下がる | Props で渡す |
CfnParameter / CfnCondition を多用 | 合成時に値が決まらず、CDK の利点が失われる | コンテキストと if 文で分岐 |
| リソースの物理名を固定 | 置き換え不可・複数環境で衝突 | 自動生成に任せる |
| 論理 ID を気軽に変更 | リソースが作り直され、データ消失の恐れ | ID を維持、cdk diff で確認 |
| ステートフル・ステートレスを同じ Stack に | 作り直しの影響範囲が大きい | Stack を分離 |
cdk.context.json を .gitignore | 環境によって合成結果が変わる | コミットする |
| コンソールで手動変更 | ドリフト発生、次回デプロイで上書き | すべてコードで変更 |
IAM ポリシーを * で手書き | 過剰な権限 | grant* メソッド |
| ロググループの保持期間未設定 | ログが無期限に残りコスト増 | retention を明示 |
本番へ手元から cdk deploy | 誰がいつ何を変えたか追えない | パイプライン経由に限定 |
12. チェックリスト
設計
- Stack はステートフル / ステートレスで分離されている
- Construct は Props で設定を受け取り、環境変数を直接読んでいない
- L2 / L3 Construct を優先して使っている
- リソースの物理名を不必要に固定していない
安全性
- ステートフルリソースに
RemovalPolicy.RETAINと削除保護を設定している - 本番 Stack に
terminationProtection: trueを設定している -
cdk diffで replace と IAM 変更を確認している - cdk-nag などのポリシーチェックを CI で実行している
- シークレットをコードに含めていない
運用
-
env(アカウント・リージョン)を明示している -
cdk.context.jsonをコミットしている - 本番デプロイはパイプライン経由で、承認ステップがある
- タグとロググループの保持期間が設定されている
- CDK のバージョンを固定し、定期的に更新している