要点: CloudFormation はスタックあたり最大 500 リソースという上限があります。CDK アプリをサービス境界で分割し、依存グラフを明示し、環境ごとにパイプラインを整備すると、cdk deploy は予測可能に回り続けます。夜間のドリフト検出とガードレールを組み合わせ、数十スタック規模でも保守できる状態を作りましょう。

なぜ CDK アプリを分割するのか

CloudFormation には 1 スタックあたり 500 リソースというハードリミットがあります。環境やリージョンを複製する大規模案件では、あっという間にこの上限へ到達します。上限に届く前でも、単一スタックはデプロイ時間を引き延ばし、障害時の影響範囲を想像しにくくします。

官公庁向けのクラウド移行プロジェクトでは、当初「すべてを 1 つの CDK アプリで管理する」方針でした。半年後には以下を抱えるようになりました。

  • 環境ごとに 4 本のパイプライン(ユニット、統合、外部連携、本番)を持っていたが、全てが同じ巨大スタックを対象にしていたため、毎回同じレビューと承認を 4 回繰り返す必要があった。
  • ネットワーク、セキュリティ、コンピュート、メッセージング、分析など共通基盤の責務を 1 スタックに詰め込んでいたので、わずかな変更でも cdk diff が膨大になりリスクが高かった。
  • テンプレートは最終的に 500 リソース上限を超えており、すべての環境が同じ合成結果を使っていたため、どのアカウントでも即座にエラーが発生する状態だった。

単体のデプロイ時間は許容範囲でしたが、4 本のパイプラインを都度同期し、大量の cdk diff を読み解く作業で夜が潰れていました。スタックを分割したことでようやく現場が落ち着きました。

スタック分割がもたらす主なメリット:

  • 反復速度の向上: 小さなスタックなら数分でデプロイでき、ロールバックも迅速。
  • 責任範囲の明確化: チームが担当サービスと一致するスタックを所有できる。
  • 権限管理の強化: スタックごとに用途が明確だと、最小権限のデプロイロールを築きやすい。
  • 読みやすい差分: CloudFormation の Change Set が 200 リソース以下なら理解しやすいサイズにまとまる。

CDK のリソース数カウントを理解する

論理コンストラクト 1 つがリソース 1 つとは限りません。 たとえばセキュリティグループのインバウンド/アウトバウンドルールは、それぞれ AWS::EC2::SecurityGroupIngressAWS::EC2::SecurityGroupEgress としてカウントされます。CDK 上で 30 個のインバウンドルールと 1 個のアウトバウンドルールを持つセキュリティグループを定義すると、スタックのリソース数は 32 になります。ルートテーブル関連、カスタムポリシー、Step Functions の状態なども同様にリソース数を増やします。CI で継続的にリソース数を計測し、500 上限に近づく兆しを早期に検知しましょう。

設計で守った 3 つの原則

マルチスタック構成を考える際は、次の原則から始めます。

  1. 安定した境界: リソース数ではなくドメイン(ネットワーク、アイデンティティ、データ、アプリ、観測基盤)で区切ります。組織変更にも耐える境界を用意しましょう。
  2. コンストラクトは共有、状態は分離: L2/L3 コンストラクトは共通ライブラリで再利用しつつ、VpcHostedZone など保持すべき状態は専用スタックに閉じ込めます。
  3. 依存を明文化: コードとパイプラインの両方で依存関係を見える化します。クロススタックの参照や SSM 参照、エクスポートに意図を残しましょう。

参考となるスタック分割パターン

25 以上のスタックを破綻なく運用できた構成例です。

レイヤー 代表スタック 目的 典型的な更新頻度
基盤 NetworkStack, SharedSecurityStack VPC、ルーティング、IAM ガードレール、KMS キー 月 1 回
共通サービス ObservabilityStack, ArtifactStoreStack, MessagingStack SNS/SQS、EventBridge、ログ基盤、アーティファクトバケット 隔週
業務ドメイン PaymentsApiStack, IdentityServiceStack, BatchJobsStack アプリコード、Lambda/ECS、データベース 毎日
エッジ/UX PublicWebStack, CloudFrontStack CloudFront、WAF、Route 53 週 1 回

更新頻度の高いアプリケーションコードと、めったに変更しない共有基盤を切り離すのがポイントです。

クロススタック参照を痛みなく扱うには

CloudFormation の Export は有効ですが、エクスポート元のスタック状態に依存するため、リファクタ時に足かせになることがあります。

おすすめのパターンは次のとおりです。

  • CDK コンテキスト出力: めったに変わらない ID(VPC ID など)はブートストラップ時に cdk.context.json へ出力し、Vpc.fromLookup などで参照します。
  • SSM パラメータレジストリ: サブネット ID やセキュリティグループ ID など重要値を Parameter Store に記録し、ssm.StringParameter.fromStringParameterName で参照します。
  • 専用の SharedExportsStack: Export が必要なら変更頻度の低いスタックに隔離し、Fn.importValue で参照範囲を狭めます。
  • イベント駆動の連携: アプリ間連携は EventBridge や SNS で疎結合に保ち、直接的なリソース共有を避けます。

スケールするデプロイ順序の作り方

オーケストレーションなしに cdk deploy を乱発すると、デプロイ順序が属人化します。私たちが導入したガードレールは次の通りです。

  1. 依存グラフをコードに埋め込む:
const network = new NetworkStack(app, "Network", { env });

const shared = new SharedServicesStack(app, "Shared", {
  env,
  vpc: network.vpc,
});
shared.addDependency(network);

const payments = new PaymentsStack(app, "Payments", {
  env,
  sharedResources: shared.outputs,
});
payments.addDependency(shared);

addDependency を使えば、手動デプロイでも順序を自動で守れます。

  1. 環境単位のマトリクスビルド: CI/CD(GitHub Actions、CodePipeline、GitLab CI など)で環境ごとにステージを分けます。例: { env: dev, stacks: [network, shared, payments] } の完了を確認してから { env: prod, stacks: [...] } へ進む、と設定します。

  2. 構成ファイルで順序を管理: yamljson のマニフェストにスタック一覧を記録し、人と自動化が同じ順序を共有します。

dev:
  - network
  - shared
  - payments

prod:
  - network
  - shared
  - payments
  1. 失敗を局所化: cdk deploy stackA stackB のように対象を明示し、失敗したスタック以降を走らせません。

  2. ドリフト検出: 長寿命スタックには cdk diffcloudformation detect-stack-drift を定期実行して、非同期変更を早期に発見します。

共有アーティファクトとパイプラインの運用

スタック分割を行うと、アプリコードとインフラコードの所有範囲が際立ちます。以下のワークフローが有効でした。

  • モノレポ + パッケージ分割: アプリとインフラを同一リポジトリに置きつつ、packages/infrastructure のように共通コンストラクトをまとめます。
  • コンストラクトのセマンティックバージョニング: 内部 npm レジストリ(例 @company/platform-network)へ公開し、アプリ側はバージョン固定で利用します。
  • ドメインごとのパイプライン: プラットフォームチームが基盤スタックを担当し、プロダクトチームは自分のスタックのみをデプロイするパイプラインを持つ形が最も安定しました。
  • 変更検知の自動化: git diff --name-only などで変更ファイルを検知し、影響するスタックだけをデプロイ対象に含めます。

運用での学び

  • 100 スタック規模のログ基盤運用: ログ系のワークロードでは単一の CDK アプリから 100 を超えるスタックを生成しました。リージョン分割や保持期間、分析パイプラインごとにスタックを切り替える構成です。アプリ本体は共通のまま、スタックごとに読み込む設定値だけを変えることでスケールさせられました。
  • KMS は専用スタックへ分離: KMS キーは専用の CDK アプリで管理しています。キー削除には最短 7 日の待機が必要で、DR 用レプリカは元キーと同じ ID を共有します。非メインリージョンでデプロイが失敗すると ID が残り、プライマリーキーを破棄し再デプロイしない限り更新できません。そのため最初にメインリージョンのキーをデプロイし、検証後に DR スタックを第二段として展開しています。
  • アカウントごとに同一バージョンのブートストラップ: 異なる CDK ブートストラップテンプレートが混在すると、ロール引き受けに失敗します。
  • ネストスタックは控えめに: 整理には便利ですが、500 リソースのカウント対象です。あくまで補助的な手段として扱います。
  • タグ付けをアスペクトで強制: コストセンターやオーナーなど必須タグは Aspect で自動付与します。
  • 出力のドキュメント化: スタック出力をドキュメント化し、深夜対応でも VPC エンドポイント ID などをすぐ調べられるようにします。
  • ロールバックの計画性: 複数スタックを扱う場合、以前のバージョンへ戻す順序をマニフェスト化し、最後に成功したスタック組み合わせを記録しておきます。

設定ファイルで 4 つの環境を切り替える

ユニットテスト、結合テスト、外部連携テスト、本番という 4 環境を運用していても、CDK アプリを環境数だけ複製する必要はありません。私たちは 環境別の設定モジュール(TypeScript の .ts ファイル)にアカウント ID や VPC CIDR、フィーチャーフラグなど可変値をまとめ、デプロイ前に環境変数を切り替える方式を採用しました。

export NODE_ENV=prod
npx cdk deploy

エントリーポイントで process.env.NODE_ENV(デフォルトは dev)を読み、config/prod.tsconfig/dev.ts を動的にロードします。これによりインフラコードは単一のまま、環境固有の設定だけを差し替えられます。CI/CD では環境変数を変更するだけで dev → test → prod と同一テンプレートを昇格でき、複製アプリによるドリフトを防げました。

マルチスタック CDK を出荷する前のチェックリスト

  • [ ] 各スタックは明確な業務ドメインを持っていますか?
  • [ ] 定常時のリソース数は 400 以下に収まっていますか?
  • [ ] addDependency、SSM、コンテキストなどで依存関係を明文化していますか?
  • [ ] CI/CD が環境ごとに決まった順序でスタックをデプロイしていますか?
  • [ ] 個別のスタックを単体で再デプロイできますか?
  • [ ] ブートストラップロール、タグ、ログ基盤などが環境間で揃っていますか?
  • [ ] オンコールがスタック出力やデプロイマニフェストを即座に参照できますか?

四半期ごとのアーキテクチャレビューでこのチェックリストを回すと、スタックの健全性を継続的に保てます。スタック分割は単なる上限回避ではなく、レジリエントで監査しやすいインフラ運用の基盤です。