AWS CDK のデプロイベストプラクティス

AWS CDK のデプロイにおけるベストプラクティスをご紹介します。

目次

目次


AWS CDK のデプロイベストプラクティス

AWS CDK では、様々な方法でスタックを定義し、デプロイすることができます。しかし、様々な方法があるからこそ、どの方法が最適なのか迷うこともあるでしょう。そこで、私が考える AWS CDK のデプロイに関するベストプラクティスをご紹介します。

リソースの構成や Construct の実装方法などではなく、あくまで CDK におけるデプロイ周りにスコープを絞ってみました。


本記事では、以下の 4 つのアプローチをご紹介します。

  1. 静的スタック作成 + Stage を使おう
  2. Synthesize once, deploy many
  3. アセットのビルド・公開とデプロイのフェーズを分離しよう
  4. cdk.context.json をコミットしよう


注意

「ベストプラクティス」というタイトルですが、あくまで筆者の試行錯誤中の内容であり、一つの例です。必ずしも正解というわけではありませんので、参考程度にご覧いただけると幸いです。

また、チームや環境によっても適切な方法が異なる場合もあります。あらかじめご承知おきください。


1. 静的スタック作成 + Stage を使おう

まず最初は、スタックの定義方法についてです。静的スタック作成、かつ Stage を使う方法をおすすめします。

const app = new cdk.App();

new MyStage(app, 'Dev', {
  ...getProps('Dev'),
  env: {
      region: 'us-east-1',
      account: '111111111111',
  },
});

new MyStage(app, 'Stg', {
  ...getProps('Stg'),
  env: {
      region: 'us-east-1',
      account: '222222222222',
  },
});

new MyStage(app, 'Prod', {
  ...getProps('Prod'),
  env: {
      region: 'us-east-1',
      account: '333333333333',
  },
});


静的スタック作成と動的スタック作成

そもそもスタックの作成方法には、静的なアプローチと動的なアプローチの 2 種類があります。

静的スタック作成は以下のように、dev, stg, prod などのスタック構成を明示的にコード上で定義する方法です。具体的には、スタックのインスタンスを環境ごとにそれぞれ new する方法です。

const app = new cdk.App();
new MyStack(app, 'DevStack', { ... });
new MyStack(app, 'StgStack', { ... });
new MyStack(app, 'ProdStack', { ... });


一方、動的スタック作成は、スタックのインスタンスは 1 つだけ new し、dev / stg / prod といった環境情報を cdk deploy の --context ( -c ) オプションで渡すことで、環境ごとのスタックを生成する方法です。

const app = new cdk.App();
const env = app.node.tryGetContext('env') as string;

new MyStack(app, `${env}Stack`, { ... });


静的スタック作成がおすすめな理由

2 つのアプローチにはいくつか違いがありますが、主な違いは、「一度に全環境を合成するか」です。

cdk deploy コマンドの内部では、 cdk synth コマンド相当の合成処理が実行されます。そして、cdk synth コマンドでは、基本的に CDK アプリ全体のスタックが合成されます。つまり、静的スタック作成の場合は、dev / stg / prod などの全環境分のスタックが一度に合成されることになります。一方、動的スタック作成の場合は、cdk deploy コマンドで --context オプションで指定した環境に応じたスタックのみが合成されます。


この特徴により、静的スタック作成の場合では、より早い段階で本番環境のエラーを検知できるというメリットがあります。例えば dev のコードは正しいんだけど prod のコードが間違っている、というような場合に、prod のデプロイを実行する前にエラーを検知できます。

このような理由で、筆者は静的スタック作成をおすすめしています。


動的スタック作成が合っているケース

逆に言えば、dev のエラーが原因で prod も失敗してしまうのは避けたいという方は、動的スタック作成の方が合っているかもしれません。

また、大規模なプロジェクトで、全環境分のスタックを一度に合成したときに合成に時間がかかりすぎるような場合も、動的スタック作成の方が良いかもしれません。


それぞれのアプローチについてより詳しく知りたい方は、『AWS CDK 環境管理ガイド 〜静的 vs 動的スタック作成〜』という記事をご覧ください。こちらは、私がドイツの AWS HERO である Thorsten Höger さんと共同で執筆した記事になります。

go-to-k.hatenablog.com


ハイブリッドなアプローチ

静的スタック作成をおすすめしましたが、コンテキストを用いた動的スタック作成のアプローチを組み合わせることも可能です。

例えば、1 つの AWS アカウントを複数人で共有して開発しているような環境で、個人用の検証環境を作成したい場合に有効です。

以下のように、cdk deploy コマンドの --context ( -c ) オプションで、スタック名に個人を識別するための情報を渡すことで、個人環境用のスタックを動的に生成することができます。Dev のみ個人環境が必要な場合は、Stg や Prod では先ほどの静的スタック作成のコードのままで良いでしょう。

npx cdk deploy -c owner=goto Dev/*
const owner = app.node.tryGetContext('owner') as string;
const devStageName = owner ? `Dev-${owner}` : 'Dev';

new MyStage(app, devStageName, {
  ...getProps('Dev'),
  env: {
    region: 'us-east-1',
    account: '111111111111',
  },
});

// Stg、Prodは先ほどと同様
// ...


もし 1 人に 1 つ AWS 環境が割り当てられている場合は、コンテキストを使わずとも以下のように、account に自身の AWS アカウント ID を指定することで、個人環境用のスタックを定義できます。

const app = new cdk.App();

new MyStage(app, 'Dev', {
  ...getProps('Dev'),
  env: {
      region: 'us-east-1',
      account: process.env.AWS_ACCOUNT_ID || '111111111111',
  },
});


Stage のメリット

Stage は、App と Stack の間に位置付けられる概念で、複数のスタックをまとめるための単位です。複数のスタックで構成されるアプリケーションでは、先ほどの静的アプローチのように環境ごとに明示的にスタックを new する方法だと、環境とスタックを掛け合わせた数だけ new する必要があり、コードが冗長になってしまいます。

Stage を使うことで、環境ごとに Stage を new し、その中で複数のスタックを定義することができるため、コードがすっきりします。たとえ、今スタックが 1 つしかなくても、将来的にスタックが増える可能性がある場合は、最初から Stage を使っておくと良いでしょう。

class MyStage extends cdk.Stage {
  constructor(scope: cdk.App, id: string, props?: cdk.StageProps) {
    super(scope, id, props);

    new StackA(this, 'StackA', { ... });
    new StackB(this, 'StackB', { ... });
    new StackC(this, 'StackC', { ... });
  }
}

const app = new cdk.App();
new MyStage(app, 'Dev', { ... });
new MyStage(app, 'Stg', { ... });
new MyStage(app, 'Prod', { ... });


Stage についての詳細は、以下の『AWS CDK の Stage の活用方法とスタックの静的・動的作成の比較』という記事をご覧ください。

go-to-k.hatenablog.com


Stage に移行する際の注意点

基本的には、非 Stage のスタックを Stage に移行すること自体は難しくありません。

ステージを使った場合、スタック名は ${stageName}-${スタック ID} (e.g. MyStage-SampleStack) というルールで作られるため、ただステージを導入するだけだとスタック名が変わってしまいます。 つまり、新規スタック作成処理が走ります(元のスタックに対する削除処理は走りません)。

しかし、StackProps には stackName というプロパティがあるため、これを明示的に指定する(今までのスタック名を指定する)と、スタック名に ${stageName}- というプレフィックスが付くのを防ぐことができ、スタックの新規作成にならず今までのスタックを維持することができます。

// 移行前
new StackA(app, 'DevStackA', { ... }); // スタック名は `DevStackA`
new StackB(app, 'DevStackB', { ... });
new StackC(app, 'DevStackC', { ... });

// 移行後
class MyStage extends cdk.Stage {
  constructor(scope: cdk.App, id: string, props?: cdk.StageProps) {
    super(scope, id, props);

    // stackName を指定しない場合、`Dev-StackA` のようなスタック名になり、新規スタック作成処理が走る
    new StackA(this, 'StackA', { stackName: `${id}StackA`, ... });
    new StackB(this, 'StackB', { stackName: `${id}StackB`, ... });
    new StackC(this, 'StackC', { stackName: `${id}StackC`, ... });
  }
}

const devStage = new MyStage(app, 'Dev');


また、基本的には、Stage に移行しても既存のリソースに影響のないことが多いです。

しかし、一部のリソースでは破壊的変更が発生するケースがあるため、注意が必要です。

詳細については、以下の『AWS CDK で非 Stage から Stage への移行で置換が発生するリソースとその原因』という記事をご覧ください。

go-to-k.hatenablog.com


2. Synthesize once, deploy many

こちらは、CI/CD パイプラインの構成に関するベストプラクティスです。

普段アプリケーション開発をしている方は、Build once, deploy many という言葉を聞いたことがある方もいるのではないでしょうか。

CDK では同じような言葉で、Synthesize once, deploy many と表現されることがあります。これは、CDK アプリの合成は 1 回だけ行い、その後のデプロイは何度も行うという考え方です。つまり、大事なのは、CI/CD パイプラインの中で、CDK アプリの合成は 1 回だけ行うということです。


合成を複数回実行するリスク

前の章で(静的スタック作成がおすすめな理由)、cdk deploy コマンドの内部では、 cdk synth コマンド相当の合成処理が実行されるという話をしました。

また、dev / stg / prod などの環境ごとに cdk deploy コマンドを実行する場合、環境ごとに cdk synth コマンドも実行されることになります。つまり、環境ごとに合成処理が走ることになります。


合成が複数回実行されると、コードによっては、1 回目と 2 回目の合成結果で異なる出力が作成されるリスクがあります。正しいコードを書いていると思っていても、実装ミスやバージョンの統一漏れなどによる思わぬミスが起きることもあります。

開発環境での合成と本番環境での合成が違うタイミングで実行されることで、想定していない環境差異が混入する可能性もあるため、合成は 1 回だけ行うのが望ましいです。


またリスク以外にも、そもそも合成処理を 2 回行うということは冗長でもあり、非効率であることは言うまでもありません。特に大規模なアプリケーションの場合、合成にはかなりの時間がかかることがあるため、デプロイ時間の大幅な増加につながる可能性があります。


CI/CD パイプラインでの実現方法

では、CI/CD パイプラインの中で、Synthesize once, deploy many を実現するにはどうすれば良いでしょうか。


cdk synth コマンドを実行すると、CDK アプリのコードが合成され、CloudFormation テンプレートなどのクラウドアセンブリと呼ばれるファイル群が cdk.out というディレクトリに出力されます。

一方、cdk deploy コマンドでは、--app ( -a ) オプションに、合成されたクラウドアセンブリの場所を指定することができます。指定された場合は、デプロイコマンド中の合成処理をスキップして、指定されたクラウドアセンブリをもとにデプロイ処理が実行されます。


つまり、あらかじめ CDK アプリの合成を 1 回だけ行い、全環境のクラウドアセンブリをアーティファクトとして保存しておき、後続の各環境でのデプロイジョブでは、そのクラウドアセンブリを参照してデプロイを行うという構成にすれば、Synthesize once, deploy many を実現できるのです。

また、これは先ほどの「静的スタック作成」のアプローチだからこそ成せる技です。なぜなら、静的スタック作成のアプローチでは、dev / stg / prod などの全環境のクラウドアセンブリが 1 回の合成で生成されるからです。


具体的には、GitHub Actions を例にすると、以下のような構成になります。

jobs:
  synth:
    runs-on: ubuntu-latest
    steps:
      # ... checkout, setup-node, npm ci ...
      - run: npx cdk synth # スタック名未指定で all と同様
      - uses: actions/upload-artifact@v4
        with:
          name: cdk-out
          path: cdk.out

  deploy-dev:
    needs: synth
    steps:
      # ... checkout, setup-node, npm ci, download-artifact, configure-aws-credentials ...
      - run: npx cdk deploy --app cdk.out --require-approval never Dev/*

  deploy-stg:
    needs: deploy-dev
    steps:
      # ...
      - run: npx cdk deploy --app cdk.out --require-approval never Stg/*

  deploy-prod:
    needs: deploy-stg
    steps:
      # ...
      - run: npx cdk deploy --app cdk.out --require-approval never Prod/*


cdk.out に関して詳しくは以下の『AWS CDK の cdk.out (クラウドアセンブリ)ってなぜ必要なの?』という記事をご覧ください。

go-to-k.hatenablog.com


3. アセットのビルド・公開とデプロイのフェーズを分離しよう

次は、AWS CDK におけるアセットのビルド・公開と、デプロイのフェーズの分離についてです。


cdk deploy コマンドを実行すると、CDK アプリの合成、アセットのビルド・公開、CloudFormation によるデプロイという一連の処理が実行されます。

アセットとは、CDK アプリで定義した Lambda 関数のコードや、ECS で使用する Docker イメージなどのことです。cdk deploy では、CDK アプリの合成が完了した後、Docker イメージのビルドが実行され、その後ビルドされたイメージや Lambda 関数のコードファイルが S3 や ECR にアップロード(公開)されます。


先ほどの章でご紹介した --app ( -a ) オプションを使うと合成処理をスキップすることができますが、アセットのビルド・公開は変わらずデプロイ時に実行されます。

アセットのビルド・公開とデプロイが同じコマンドで実行されるのは便利です。一方、Twelve-Factor App でいうところの「Build, release, run」のように、ビルドとデプロイ(リリース)を分けるという観点では、アセットのビルド・公開とデプロイのフェーズが分離されている方が望ましいと言えます。


フェーズ分離のメリット

AWS CDK においてアセットのビルド・公開とデプロイのフェーズが分離されている場合、デプロイジョブの責務がシンプルになり、CloudFormation の実行だけに専念できるようになります。これにより、以下のメリットが得られます。

  • ビルドが一時的に失敗するようなケースでもデプロイは影響を受けず、より安全に実行できる
  • デプロイ失敗時にアセットのビルドをスキップしてデプロイだけリトライできる
  • フェーズごとに必要な IAM 権限を最小限に絞ることができる
  • Docker ビルドなどの重い処理が事前のフェーズに行われるため、デプロイフェーズ自体にかかる時間が短縮される
  • デプロイフェーズで CDK CLI を使わず、AWS CLI による CloudFormation デプロイも可能になる


cdk publish-assets コマンド

CDK CLI では、今までアセットのビルド・公開とデプロイが一体となっていました。

cdk-assets というライブラリを使ってアセットのビルド・公開を行うことはできましたが、CDK CLI とは別の依存関係が必要だったり、CDK のドキュメントで十分に紹介されていなかったりといった理由から、あまり一般的に使われている方法ではありませんでした。


そこで CDK CLI において、デプロイを行わずに、合成からアセットのビルド・公開までを行う cdk publish-assets コマンドが追加されました。こちらを使うことで、アセットのビルド・公開とデプロイのフェーズを分離することができます。

※ 2026 年 4 月時点では、cdk publish-assets コマンドはまだ unstable 版であるため、--unstable=publish-assets オプションを付けて実行する必要があります。

# スタック名未指定で --all と同様
npx cdk publish-assets --unstable=publish-assets

# --app オプションで合成をスキップすることも可能
npx cdk publish-assets --unstable=publish-assets --app cdk.out


CI/CD パイプラインでのフェーズの分離例

以下のように、synth ジョブと deploy ジョブの間に、cdk publish-assets コマンドを実行するジョブを挟む構成にすることで、アセットのビルド・公開とデプロイのフェーズを分離することができます。

cdk publish-assets コマンドでも、synth ジョブで合成したクラウドアセンブリを --app オプションで指定することで合成をスキップできます。これにより、Synthesize once, deploy many を実現しつつ、アセットのビルド・公開とデプロイのフェーズも分離することができます。

  synth:
    runs-on: ubuntu-latest
    steps:
      # ... checkout, setup-node, npm ci ...
      - run: npx cdk synth
      - uses: actions/upload-artifact@v4
        with:
          name: cdk-out
          path: cdk.out

  publish-assets:
    needs: synth
    runs-on: ubuntu-latest
    steps:
      # ... checkout, setup-node, npm ci, configure-aws-credentials ...
      - uses: actions/download-artifact@v4
        with:
          name: cdk-out
          path: cdk.out
      - run: npx cdk publish-assets --unstable=publish-assets --app cdk.out --all
      # ステージ(スタック)を指定して公開も可能
      # 同一ジョブ内なので、複数回コマンドを実行しても Docker のレイヤーキャッシュが効き、2 回目以降のビルドは実質スキップされる
      # AWS環境が分かれている場合は、各アカウントへのアクセス権限を持つロールに都度スイッチすること
      # npx cdk publish-assets --unstable=publish-assets --app cdk.out Dev/*
      # npx cdk publish-assets --unstable=publish-assets --app cdk.out Stg/*
      # npx cdk publish-assets --unstable=publish-assets --app cdk.out Prod/*

  deploy-dev:
    needs: publish-assets
    steps:
      # ... checkout, setup-node, npm ci, download-artifact, configure-aws-credentials ...
      - run: npx cdk deploy --app cdk.out --require-approval never Dev/*

参考

実はこの cdk publish-assets コマンドは、私が AWS CDK にコントリビュートしました!

github.com


また、CDK の元メンテナのブログでも、アセットのビルド・公開フェーズの分離の話が紹介されています。こちらもぜひご覧ください。

こちらのブログで紹介されている通り、CDK Pipelines でも、publish-assets というステージがデプロイステージの前に作成されています。

www.endoflineblog.com


4. cdk.context.json をコミットしよう

最後は、cdk.context.json のコミットについてです。


cdk.context.json とは

cdk.context.json とは、合成中に AWS アカウントから取得した値をキャッシュしておくための格納ファイルになります。


例えば、アベイラビリティーゾーン情報や EC2 インスタンスで現在利用可能な Amazon マシンイメージ (AMI) ID などを、AWS アカウントから動的に取得して格納したりします。

具体的には、CDK で提供される fromLookup() などの context メソッドと呼ばれるメソッドを実行した際に、内部で AWS SDK を通じて AWS アカウントに情報を取りにいき、その結果を cdk.context.json ファイルに格納します。

const vpc = Vpc.fromLookup(this, 'Vpc', {
  vpcId,
});

const parameter = StringParameter.valueFromLookup(this, parameterName);


そして、合成またはデプロイ時、キャッシュである cdk.context.json にその情報があった場合は、AWS アカウントに SDK で情報を取得する処理は走らず、ファイル内の情報を使用するようになっています。


cdk.context.json をコミットするメリット

cdk.context.json をコミットするメリットとして、以下の 2 点が挙げられます。

  • 非決定的な動作の回避
  • デプロイ速度の向上


非決定的な動作の回避

例えば、context メソッドで EC2 の最新の AMI を取得するようにしてデプロイしていたとしましょう。

もし、ある日を境に新しい AMI のバージョンがリリースされたとします。CDK では最新のイメージを取得するように実装しているので、いつの間にかすでにデプロイしている EC2 のものと取得した AMI の値が変わってしまい、EC2 の置換(再構築)が走ることになってしまいます。


このように、デプロイ実行時のタイミングで構成が変わってしまうような「非決定的」な動作を避けるために、cdk.context.json ファイルにデプロイしたときの AMI 情報をキャッシュしておき、次回以降のデプロイではそのキャッシュ情報を参照することで毎回のデプロイで同じ値を使用し、「決定的」な動作を保証することができるわけです。


デプロイ速度の向上

context メソッドを使用している CDK アプリでは、合成時に cdk.context.json に該当の情報がない場合、一度ダミー値が格納されて合成が終了します。

その後、CDK CLI が AWS SDK を通じて AWS アカウントからその情報を取得し、cdk.context.json を更新するという処理が走ります。

そして再度、合成処理が走り、今度は cdk.context.json に格納された値をもとに最終的なクラウドアセンブリが生成されるという流れになります。


つまり、cdk.context.json をコミットせず、ファイルが存在していない場合、合成が 2 回走ることになるため無駄に時間がかかってしまいます。

また、合成やデプロイを実行するたびに、AWS SDK による通信が走ることでもあるため、さらに無駄な時間がかかってしまいます。


つまり、cdk.context.json をコミットしておくことで、合成は 1 回だけで済みAWS SDK による通信も走らないため、デプロイの速度が向上することになります。


cdk.context.json の詳細情報

cdk.context.json についてより詳しく知りたい方は、以下の『AWS CDK における「cdk.context.json」の必要性』という記事をご覧ください。

go-to-k.hatenablog.com


公式ドキュメント

AWS CDK の公式ドキュメントでもベストプラクティスに関する内容が紹介されています。こちらもぜひご覧ください。

docs.aws.amazon.com


まとめ

本記事では、AWS CDK のデプロイにおけるベストプラクティスとして、以下の 4 つのアプローチをご紹介しました。

  1. 静的スタック作成 + Stage を使おう
  2. Synthesize once, deploy many
  3. アセットのビルド・公開とデプロイのフェーズを分離しよう
  4. cdk.context.json をコミットしよう


これらのアプローチが必ずしも全てのプロジェクトに当てはまるわけではありません。しかし、一つの参考例として、皆さんのプロジェクトに合った方法を選択する際のヒントになれば幸いです。