AWS CDK(v2)でインフラをコードとして開発・運用するときのベストプラクティスをまとめたものです。


目次

  1. 基本方針
  2. プロジェクト構成
  3. Construct の設計
  4. Stack の設計
  5. 環境・設定の管理
  6. リソースの命名とステートフルリソース
  7. セキュリティ
  8. テスト
  9. デプロイと CI/CD
  10. コスト・運用
  11. よくあるアンチパターン
  12. チェックリスト

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 のバージョンを固定し、定期的に更新している

参考

0件のページ