AWS CDK の開発に必要な CloudFormation の基礎についてまとめています。

1. CloudFormation スタックの基本原則

CloudFormation の挙動を理解する上で、基盤となる 2 つの原則があります。

  • 原子性(アトミシティ)
    • スタック操作(作成・更新・削除)はトランザクションとして扱われます。結果は「すべて成功」か「すべて失敗(ロールバック)」のいずれかとなり、中途半端な状態でリソースが残るのを防ぎます。
    • 例外もあります。cdk deploy --no-rollback を指定した場合や、削除に失敗して DELETE_FAILED になった場合は、途中の状態が残ります。
  • 参照制約(依存関係の自動解決)
    • リソース間の参照関係を解析して依存関係グラフを構築し、作成・更新・削除の適切な順序を自動で決定します。

参照制約における 2 つの制約・仕様

  • 同一スタック内の制約(循環参照の禁止)
    • 組み込み関数(!Ref, !GetAtt)で他リソースを参照できますが、リソース間で互いに参照し合う 循環参照 が発生すると順序を解決できずデプロイエラーになります
  • 他スタックからの参照(クロススタック参照)
    • あるスタックの出力を他スタックが参照(!ImportValue)している場合、CloudFormation はデータの整合性を保つため、参照されている側の Export の削除や値の変更をロックします。

2. 原則が崩れる場合:ロールバックの失敗

原則として「原子性」によって元の状態に戻りますが、ロールバック自体が失敗した場合(ROLLBACK_FAILEDUPDATE_ROLLBACK_FAILED 状態)、リソースが中途半端に作成・変更された状態でスタックの操作が止まります。

主な失敗原因:AWS外のリソース競合、手動操作による構成ドリフト、またはリソース削除権限の不足など。

ただし、復旧手段はあります。

状態 発生場面 復旧方法
UPDATE_ROLLBACK_FAILED 更新のロールバックに失敗 原因を解消して continue-update-rollback を実行する。原因を解消できないリソースは --resources-to-skip でスキップする
ROLLBACK_FAILED 作成のロールバックに失敗 スタックを削除する。削除に失敗して DELETE_FAILED になったリソースは、--retain-resources を指定して再削除する
aws cloudformation continue-update-rollback \
  --stack-name  \
  --resources-to-skip  \
  --profile default

CloudFormation スタックのロールバックの詳細は スタックのロールバック を参照してください。

3. クロススタック参照の挙動と制約

ロックの対象は「リソース」ではなく「Export(名前と値)」

CloudFormation がロックをかけるのは、あくまで他から参照されている Export の「名前(Name)」や「値(Value)」そのものを削除・変更しようとした時です。リソースの更新自体がすべて拒否されるわけではありません。

具体例:EC2インスタンスのタグ変更(成功するケース)

  • スタック A(Export側): MyEC2InstanceId という名前で、EC2のインスタンス ID(i-xxxxxxxxxxxxxxxx)を Export
  • スタック B(Import側): !ImportValue MyEC2InstanceId で参照

この状態でスタック Aの EC2 の Name タグのみを変更した場合、EC2 のインスタンス ID 自体は変わりません。Export の名前も値も変化しないため、CloudFormation はインポート側(スタックB)への影響がないと判断し、スタックAの更新を正常に承認・完了させます。

変更がブロックされるケース(できないこと)

  • エクスポートの削除: Outputsから該当のExport定義を消す
  • エクスポート値の変更: S3 バケット名が変わり、ARN(Export値)が変わるような変更
  • スタック自体の削除: 参照されている Export を持つスタックを丸ごと削除する

AWS CDKにおける最大のリスク

CDK では、論理IDid)の変更や一部のプロパティ変更によって、裏側でリソース置換(Replace)が発生します。 置換は 新リソース作成旧リソース削除 の順で行われるため、Export 値が書き換わります。 これが原因でクロススタック参照のブロックに引っかかり、デプロイが失敗して UPDATE_ROLLBACK_FAILED に陥る危険なパターンが多発します。

4. 同一スタック内更新 vs クロススタック参照

同一スタック内での !Ref!GetAtt による参照は、クロススタック参照のような「値が変わるからブロック」という硬直化(ロック)は起きません。しかし、別のリスクが存在します。

同一スタック内における置換の連鎖(カスケード)

親リソースの置換が発生すると、それを参照している子リソース、さらに孫リソースへと更新・置換が連鎖(カスケード)します。 参照先が置換されても、参照側が必ず置換されるとは限りません。参照側のプロパティが更新可能なら更新で済み、更新不可なら置換になります。

[MyVpc (VPC)] ──(CIDR の変更などで置換が必要)
│
▼ !Ref: MyVpc
[MySubnet / MySecurityGroup] ──(VpcId が変わるため置換が必要)
│
▼ !Ref / !GetAtt
[MyEcsService] ──(Subnet や SG の ID が変わるため更新が必要。更新できないプロパティなら置換)

この連鎖の途中で1つでもエラーが発生すると、ロールバックも含めてスタック全体が UPDATE_ROLLBACK_FAILED するリスクがあります。

比較まとめ

項目 同一スタック(!Ref / !GetAtt クロススタック(!ImportValue
参照中のリソース更新 原則可能(連鎖的な置換・更新) 値が変わる更新はブロックされる
参照中のリソース削除 依存関係の順に自動処理 Export が使われている間は削除不可
主なリスク 置換連鎖の失敗によるスタック破壊 依存関係が解けず、身動きが取れなくなる

したがって、クロススタック参照はシステム全体を硬直化(デッドロック)させやすいと言えます。

5. 設計アンチパターンと解決策

アンチパターン1:過度なスタック分割

単に「リソースの種類(ネットワーク、DB、アプリ)」だけで機械的に分割すると、スタック間の結合度が密になり逆効果です。 解決策:「管理境界(誰が管理するか)」と「変更頻度(どれくらいの周期で変わるか)」を基準に分割します。

アンチパターン2:手動作業の混在による構成ドリフト

IaC 管理画面以外からリソースを直接触ると、CloudFormation の管理情報と乖離し、ロールバック失敗の直接原因になります。 ドリフト検出(CloudFormation のドリフト検出機能)で、乖離を定期的に確認します。

アンチパターン3:密結合な相互参照(クロススタック参照の多用)

スタック間で安易にオブジェクトを渡し合うと、暗黙的な !ImportValue が大量に生成され、リファクタリングが不可能なシステムになります。

解決策 1:依存の向きを一方向にして、参照を意識する

CDK で別スタックのリソースを props で渡すと、実際に参照された値(VPC ID、サブネット ID など)が、裏側で自動的に Export / !ImportValue になります。 オブジェクトを渡すこと自体が問題なのではなく、暗黙の !ImportValue が生成されて依存関係に気づきにくいことが問題です。

やりがちな例(参照した値ごとに、暗黙の Export が増えていく):

const networkStack = new NetworkStack(app, 'NetworkStack');
// vpc の中で参照された値ごとに、Export / !ImportValue が自動生成される
const dataStack = new DataStack(app, 'DataStack', {
  vpc: networkStack.vpc
});

対策:

  • 依存の向きは「変更頻度が低い基盤 → 変更頻度が高いアプリ」の一方向のみにする
  • 参照する値を必要最小限にする
  • 参照される側で、論理 ID の変更や置換を起こすプロパティ変更をしない。cdk diff で確認する
  • 参照を確実に切りたい場合は、解決策 2 を採用する

注意(exportValueFn.importValue): CDKの stackA.exportValue() は、Export を明示的に残したいとき(参照元を先に外して段階的に移行する場合など)に使います。Fn.importValue() で手動記述しても、生成される CloudFormation テンプレート上は !ImportValue の密結合のままです。真に疎結合にするには、次の解決策 2 を採用してください。

解決策2:SSM Parameter Store や環境変数ファイルを介した疎結合化

スタック間の直接参照を断ち切り、静的な設定値や動的なリソースIDの受け渡しを分離します。

  • 静的な値(ドメイン名、環境名など): TypeScriptによる環境変数ファイルから注入する
  • 動的な値(VPC ID、共通 SG ID など): SSM パラメータストアを経由して受け渡す
// 参照される側(NetworkStack)
new ssm.StringParameter(this, 'VpcIdParam', {
  parameterName: '/myapp/network/vpc-id',
  stringValue: vpc.vpcId,
});

// 参照する側(DataStack)
const vpcId = ssm.StringParameter.valueForStringParameter(this, '/myapp/network/vpc-id');

SSM 経由にすると、CloudFormation のロックは無くなりますが、依存関係の検知もされなくなります。

  • デプロイ順(参照される側が先)は、dataStack.addDependency(networkStack) などで自分で保証する
  • パラメータの値が変わっても、参照する側は次回デプロイまで反映されない

6. CDKにおける環境変数・設定管理のベストプラクティス

環境ごとの静的な設定値は、.env ではなく TypeScriptファイル で管理します。 解決策 2 の「静的な値を注入する」側の実装にあたります。

// config/env.ts
export interface EnvConfig {
  readonly envName: 'dev' | 'prod';
  readonly domainName: string;
  readonly desiredCount: number;
}

export const config: Record<string, EnvConfig> = {
  dev: { envName: 'dev', domainName: 'dev.example.com', desiredCount: 1 },
  prod: { envName: 'prod', domainName: 'example.com', desiredCount: 2 },
};

なぜ TypeScript ファイルで管理すべきなのか:

  • 型安全性の確保
    • コンパイル時点で「文字列と数値の混同」や「必須キーの不足」といった初歩的なミスを検知できます
  • IDE の強力なサポート
    • 自動補完(コードコンプリーション)が効くため、設定値のタイポや参照ミスを未然に防げます
  • 柔軟なロジック定義
    • 単純なキー&バリューだけでなく、環境に応じた型定義の拡張が安全に行えます

7. AWS CDK での管理に注意が必要なリソース(IaC 境界線の設計)

すべてのリソースをAWS CDK(IaC)で画一的に管理すると、かえってデプロイ効率の低下やセキュリティリスクを招くことがあります。リソースの特性や更新頻度に応じて、IaCの管理対象から「切り離す」「外出しする」「参照に留める」といった判断が必要です。

7.1 アプリケーションコードとコンテナイメージ

方針:インフラ(土台)のみをCDKで作成し、アプリケーションのデプロイは専用のCI/CDパイプラインに委譲する。

  • 理由1:更新頻度と関心の分離 インフラの変更頻度(週〜月単位)と、アプリケーションの変更頻度(1日〜週に複数回)は大きく異なります。アプリの軽微な修正のために、毎回CDK全体の差分チェック(cdk diff)やスタック全体の更新を走らせるのは非効率です。
  • 理由2:タスク定義の巻き戻りの防止 CDK 側のタスク定義には dummy などのイメージタグが入っています。CDK でタスク定義に変更が入る(CPU・メモリや環境変数の変更など)と、dummy のイメージで新しいリビジョンが作られ、CI/CD でデプロイ済みのアプリが巻き戻る恐れがあります。
  • 補足:デプロイ戦略について ECS のローリングアップデートは CloudFormation の標準機能で動きます。CodeDeploy を使った ECS の Blue/Green や、Lambda のカナリアデプロイ(LambdaDeploymentGroup)も CDK で構成できます。ただし、アプリのリリースのたびにインフラのデプロイを通すと、上記の 2 つの問題が起きるため、分離が推奨されます。

💡 Q. ECS Service のコンテナイメージタグは AWS CDK で管理しないということ? A. はい、原則として管理しません。 CDK側には dummylatest(または初期構築用の特定のタグ)を設定してサービスを起動するだけに留めます。その後のコンテナイメージの更新は、GitHub Actions等のCI/CDパイプラインから ecspresso などの専門ツールを使用してデプロイします。AWS CLI で行う場合は、aws ecs register-task-definition で新しいイメージタグのタスク定義リビジョンを作成し、aws ecs update-service --task-definition でそのリビジョンを指定します。update-service 単体ではイメージタグを変更できません。

7.2 セキュリティ統制・基盤ネットワークリソース

対象:組織共通の IAM リソース(権限境界、共通ロールなど)、VPC(コアネットワーク)、WAFなど 方針:共通基盤(プラットフォーム)スタックとして完全に分離するか、組織によっては手動・専用パイプラインで管理する。

  • 理由:広範囲への影響度と組織の職務分掌 これらのリソースは、システムの「土台」であり、一度作成するとめったに変更されません。しかし、万が一変更を誤った場合の被害(全システム通信停止、権限の全損など)が甚大です。 アプリケーション開発用のCDKスタックにこれらを含めてしまうと、アプリ側の意図しない変更(論理IDの変更によるVPCの再作成など)に巻き込まれてシステムが崩壊するリスクがあります。また、組織における「セキュリティ管理者」と「アプリ開発者」の権限を分離(職務分掌)するためにも、スタックを完全に分けるべきです。
  • 補足:タスクロールなど、アプリ固有の IAM ロールは、アプリのスタックに含めます。

7.3 ドメイン・外部認証連携に関わるリソース

対象:Route 53(DNSレコード)、Certificate Manager(ACM証明書)、SES(ドメイン認証)など 方針:初回構築時のみCDKで管理するか、あらかじめ手動(または共通基盤)で作成したリソースをCDK側で「参照」する。

  • 理由:外部手続きによるデプロイのブロッキング 例えば、ACM証明書の検証(外部 DNS での DNS 検証や、Eメール検証)や、他社管理ドメインからRoute 53へのネームサーバー切り替えは、AWS外部での操作や数分〜数時間の待ち時間(伝播待ち)を伴います。 これらをアプリのCDKスタックに組み込むと、検証が完了するまでCDKのデプロイ処理自体が途中で待機状態になり、最悪の場合はタイムアウトでデプロイエラーになります。依存関係をクリーンに保つため、ドメイン基盤は事前に外側で用意しておくのが鉄則です。
  • 補足:同一アカウントの Route 53 ホストゾーンなら、CertificateValidation.fromDns(hostedZone) で DNS 検証が自動化されるため、この問題は起きにくくなります。
  • 補足:ALB 用の証明書は ALB と同じリージョンで、CloudFront 用の証明書は us-east-1 で発行します。

7.4 機密情報(シークレット値)

対象:KMSキー、Systems Manager パラメータストア、Secrets Managerなど 方針:メタデータ(箱)はIaCで作成し、実際の「秘密値(シークレット)」はAWSに自動生成・運用を委譲するか、手動で後入れする。

機密情報の管理は、「AWSに生成・ローテーションまで委譲できるか」によって方針が変わります。共通の鉄則として、「IaCのソースコード、デプロイを実行するターミナル、生成されるテンプレート(cdk.out や CloudFormation のテンプレート)に、平文のパスワードを絶対に載せない」という点があります。

パターンA:AWSに秘密値の生成・管理を委譲する場合(推奨)

  • 例:RDSのマスターパスワード管理など
  • 設計:CDKで Secrets Manager のリソース(箱)を作成する際、AWS側に「ランダムな文字列を自動生成する」ように指示します(CDK の SecretStringGenerator や、RDS の Credentials.fromGeneratedSecret() などを使用)。
  • メリット:コード上には一切文字列が現れず、AWS内で完結するため安全です。定期的なローテーション(自動更新)もAWSに安全に委譲できます。

パターンB:外部の機密情報をAWSに埋め込む必要がある場合

  • 例:外部SaaSのAPIトークン、サードパーティ製ツールのライセンスキーなど
  • 設計:あらかじめマネジメントコンソール等から手動(または専用のセキュアなスクリプト)で Secrets Manager にシークレット値を書き込んでおきます。CDK側ではそのシークレットのリソース(ARN や名前)を参照するコードのみを記述します(Secret.fromSecretNameV2() など)。
  • メリット:機密情報がGitリポジトリ(コード)に混入したり、CDKのビルドログ(キャッシュ)にプレーンテキストで出力されるリスクを完全に排除できます。

注意:SSM パラメータストアの SecureString

CloudFormation と CDK では、SSM パラメータストアの SecureString を作成できません(StringStringList のみ)。 SecureString はコンソールや CLI で作成し、CDK からは StringParameter.fromSecureStringParameterAttributes() などで参照します。機密値は Secrets Manager に置くのが扱いやすいです。