クラウドアーキテクチャの呪縛:CloudFormationの循環依存を断ち切り、SSM Parameter Storeで「真の疎結合」を手に入れるリファクタリング手法
テックリードの私たちが、大規模なAWS環境をCloudFormation(以下、CFn)で運用する中で、最も恐れるエラーの一つがこれだ。
> “Circular dependency between resources: [StackA, StackB]”
アーキテクチャが複雑化し、スタックを分割した瞬間にこの罠に嵌まる。`Fn::ImportValue` と `Outputs: Export` を使ったクロススタック参照は、一見すると綺麗に依存関係を定義できているように思えるが、実は「コンパイル時(デプロイ前)の硬い鎖」でスタック同士を縛り付けているに過ぎない。
今回は、この循環依存のメカニズムの本質を解き明かし、デプロイパイプラインを止めることなく安全に依存関係を断ち切る実戦的なリファクタリング手法と、AWS Systems Manager(SSM)パラメータストアを活用したモダンな疎結合アーキテクチャへの移行パターンを解説する。
—
1. なぜ循環依存は発生するのか?(悲劇のメカニズム)
まず、アンチパターンを確認しよう。よくある構成として、以下のようなケースがある。
- Networkスタック: VPCやサブネットを管理
- Appスタック: セキュリティグループやECS/EC2を管理
ここで、セキュリティグループ(App側)に「VPC ID(Network側)」が必要なのは当然だ。しかし、もし「Network側のリソース(例:VPC EndpointやVPN接続など)の一部が、App側のセキュリティグループIDを参照しなければならない要件」が生じたとき、悲劇が始まる。
[Network Stack] –(Export: VpcId)–> [App Stack]
^ |
|————(Export: SgId)———| (ここで循環依存が発生)
CFnはスタック間のインポート/エクスポートを静的に解決する。そのため、AがBをインポートし、同時にBがAをインポートしている状態(循環参照)検知すると、AWS側はどちらのスタックを先に作成・更新すべきか判断できなくなり、デプロイは無慈悲にロールバックされる。
—
2. 開発スピードを最大化するツール環境とVSCode設定
リファクタリング作業に入る前に、私たちの開発スピードを限界まで引き上げるためのエディタ環境を整えておこう。CFnのYAML地獄を生き抜くためには、道具の選定が生死を分ける。
必須VSCodeプラグイン
1. AWS CloudFormation Linter (cfn-lint): 構文エラーやリソースの不整合をリアルタイムで検知。
2. YAML: インデントのズレによるパースエラーを防止。
3. AWS Toolkit for Visual Studio Code: テンプレートのバリデーションやスタックの状態をIDEから一撃で確認。
チームで共有すべき `.vscode/settings.json`
インデントの乱れやYAMLの不整合は、コードレビューの時間を無駄に奪う。以下の設定をプロジェクトルートの `.vscode/` に配置し、チーム全員の環境を強制的に統一する。
{
“editor.tabSize”: 2,
“editor.insertSpaces”: true,
“editor.formatOnSave”: true,
“files.associations”: {
“.yaml”: “yaml”,
“.yml”: “yaml”,
“template..yml”: “cloudformation”
},
“yaml.schemas”: {
“https://raw.githubusercontent.com/awslabs/goformation/master/schema/cloudformation.schema.json”: “.template.{yml,yaml}”
},
“[yaml]”: {
“editor.defaultFormatter”: “redhat.vscode-yaml”
}
}
—
3. 安全に依存関係を断ち切るリファクタリング戦略
循環依存を解消するためのアプローチは一つではない。システムのダウンタイムをゼロにし、リソースの削除・再作成(Replacement)を回避するための3つの移行ステップを提示する。
アプローチの全体像
1. 動的解決へのシフト: `Fn::ImportValue` を廃止し、実行時(Run-time)に値を参照する仕組みへ移行する。
2. SSM Parameter Storeの導入: スタック間の値の受け渡しを、グローバルなKVS(Key-Value Store)経由に変更する。
3. 段階的デプロイ(段階的移行): 依存関係を一度「疎」な状態(Parameter Store読み取り)に置き換えてから、古いExportを削除する。
—
4. 実践:SSM Parameter Storeを活用した疎結合アーキテクチャへの移行
具体的なコード例を見ていこう。今回は、循環依存を引き起こしやすい「VPC ID」と「セキュリティグループID」の関係を、SSM Parameter Storeを介した参照に書き換える。
Step 1: Networkスタック(パラメータの書き込み)
Networkスタック側で、必要なリソースIDをSSMパラメータストアに書き込む。これにより、他のスタックからの「Export」依存を排除する。
`network-stack.yaml`
AWSTemplateFormatVersion: ‘2010-09-09’
Description: ‘Network Stack – Decoupled with SSM’
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: production-vpc
# VPC IDをSSM Parameter Storeに保存
VpcIdParameter:
Type: AWS::SSM::Parameter
Properties:
Name: /infra/prod/network/vpc-id
Type: String
Value: !Ref VPC
Description: “VPC ID for cross-stack reference”
Outputs:
# 既存の依存関係を急に断つと壊れるため、移行期間中はOutputsを残す(後で削除)
VpcIdOutput:
Description: “Legacy Export for backward compatibility”
Value: !Ref VPC
Export:
Name: ProdVpcId
Step 2: Appスタック(パラメータの動的読み取り)
Appスタック側では、`Fn::ImportValue` の代わりに、CFnのビルトイン関数ではなく動的参照(Dynamic References)、あるいはデプロイパイプライン経由でSSMから値を取得する。
> 💡 プロの知見: CFnの動的参謀 (`{{resolve:ssm:…}}`) は便利だが、スタック更新時の依存関係追跡が弱いため、パラメータをリソースのプロパティとして直接渡す設計が堅牢である。ここではテンプレートパラメータとしてSSM値を受け取るか、動的参照を利用するパターンを示す。
`app-stack.yaml`
AWSTemplateFormatVersion: ‘2010-09-09’
Description: ‘Application Stack – Decoupled with SSM’
Parameters:
# SSMパラメータストアから値を受け取る(パイプラインやCI/CDツール、または動的参照で注入)
VpcIdParam:
Type: AWS::SSM::Parameter::Value
Default: /infra/prod/network/vpc-id
Description: “VpcId retrieved from SSM Parameter Store”
Resources:
AppSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: “Security Group for Application”
VpcId: !Ref VpcIdParam # SSM経由で取得したVPC IDを使用
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
# App側で生成したIDをSSMに書き込むことで、Network側が参照できるようにする
AppSgIdParameter:
Type: AWS::SSM::Parameter
Properties:
Name: /infra/prod/app/sg-id
Type: String
Value: !Ref AppSecurityGroup
Description: “App Security Group ID for Network Stack reference”
Outputs:
AppSecurityGroupId:
Value: !Ref AppSecurityGroup
Step 3: 循環していた逆方向の参照(Network側でのAppリソース読み取り)
Networkスタック側で、App側が生成したセキュリティグループIDが必要な場合も、同様にSSMパラメータストアから参照する。
`network-stack.yaml` の追記部分
Parameters:
AppSgIdParam:
Type: AWS::SSM::Parameter::Value
Default: /infra/prod/app/sg-id
Resources:
# 例:VPC EndpointにAppからのSGをアタッチするなどのケース
VpcEndpointSecurityGroupAssociation:
Type: AWS::EC2::SecurityGroupVpcAssociation # (擬似的な例)
Properties:
VpcId: !Ref VPC
SecurityGroupId: !Ref AppSgIdParam
これで、`Network` ➔ `App` ➔ `SSM` ➔ `Network` という静的な循環依存(Export/Importの鎖)が完全に消滅し、各スタックは「SSMパラメータストアを読む/書く」という単方向の疎な関係へと生まれ変わる。
—
5. デプロイ運用の注意点とマエストロの心得
SSMパラメータストア方式へ移行した際、一つだけ絶対に守らなければならない運用の鉄則がある。
> 「デプロイ順序の制御(Dependency Ordering)」
クロススタック参照(Export/ImportValue)を使っていた時は、CFnが自動的にスタックのビルド順序を計算してくれた。しかし、SSMを介した疎結合にすると、CFn単体では「どちらのスタックを先にデプロイすべきか」が分からなくなる。
対策:CI/CDパイプラインでの明示的な順序制御
GitHub ActionsやAWS CodePipeline、AWS CDK / AWS Step Functionsなどのオーケストレーションツール側で、明示的にデプロイのDAG(有向非循環グラフ)を定義すること。
GitHub Actionsのワークフロー例
jobs:
deploy-network:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v2
- name: Deploy Network Stack
run: aws cloudformation deploy –template-file network-stack.yaml –stack-name prod-network
deploy-app:
needs: deploy-network # 必ずNetworkの後に実行
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v2
- name: Deploy App Stack
run: aws cloudformation deploy –template-file app-stack.yaml –stack-name prod-app
このひと手間を惜しまないこと。それこそが、障害に強く、変更容易性の高いクラウドインフラストラクチャを維持する唯一の道だ。
—
終わりに:アーキテクチャの美しさを取り戻す
循環依存のエラーは、インフラストラクチャの設計上の「不純物」が表面化したシグナルに過ぎない。その場しのぎでスタックを無理やり統合したり、ダミーのパラメータでごまかしたりするのではなく、SSM Parameter Storeを共通のインターフェース(API契約のようなもの)として活用せよ。
モリスの「用の美」ではないが、優れたインフラコードは、構造が美しく、結合度が低く、そして何より「変更することに恐怖を感じさせない」。今日のコードから、その硬直した鎖を断ち切ろう。