巨大化しすぎたCloudFormationテンプレートの処刑:一枚岩(Monolithic)スタックを無停止で分割・再モジュール化する極限の技術
インフラの規模が拡大するにつれ、あらゆるリソースが数千行の単一のCloudFormationテンプレート(通称:一枚岩スタック)に詰め込まれる現象は、SREにとって最も悪質な技術的負債の一つだ。
変更を加えるたびに発生する長大な変更セット(Change Set)の生成待ち、単一リソースの障害が全スタックを巻き込む恐怖、そして何より、1つのミスで数百のリソースが意図せず置換(Replacement)される恐怖。これらはチームのデプロイ速度を完全に殺す。
本稿では、「すでに稼働している本番環境の巨大スタックから、特定のリソース群を安全に切り離し、別スタックへ無停止で移行(リファクタリング)する」ための、AWSの深淵を突いた実践的アプローチを解説する。
—
1. 移行を阻む「3つの悪夢」とアーキテクチャ上の罠
一枚岩スタックを分割しようとしたエンジニアが最初に直面する壁は、CloudFormationの厳格な依存関係管理とリソースのライフサイクルだ。
1. 暗黙的依存関係(Implicit Dependencies)の迷宮
テンプレート内であちこちに散らばる `!Ref` や `!GetAtt` が見えない鎖となり、リソースを単純に切り離すことを拒絶する。
2. 削除方針(DeletionPolicy: Retain)の誤用による孤児化
リソースを古いスタックから消すために `Retain` を使うと、物理リソースがAWSアカウント内に残り続け、新しいスタックでインポートしようとした際に「Resource already exists」の呪いにかけられる。
3. ステートの不整合とダウンタイム
既存リソースの削除と新規作成の間に数秒のタイムラグが発生し、ロードバランサーやデータベースの接続断を引き起こす。
これらを克服するには、AWSが提供する `Resource Import` 機能 と クロススタック参照(`Fn::ImportValue`) を外科手術のメスのように正確に使いこなす必要がある。
—
2. 移行プロセスの全体像:ゼロダウンタイム・リファクタリングの4ステップ
安全な分割の鉄則は、「物理リソースを一切削除・再作成せず、CloudFormationの管理台帳(State)だけを付け替える」 ことだ。
[Step 1] 依存関係の洗い出しとインターフェース(Outputs)の定義
[Step 2] 移行先(新)スタックの作成と Resource Import による取り込み
[Step 3] 移行元(旧)スタックからのリソース定義の安全な削除
[Step 4] クロススタック参照への切り替えと配線の検証
言葉で言うのは簡単だ。だが、実務では一歩間違えばサービスが吹き飛ぶ。ここから、具体的なスクリプトとコードを用いた極限の実装手順に入ろう。
—
3. 実践:VPCとセキュリティグループを別スタックへ切り離す
例として、一枚岩の `Monolithic-App-Stack` から、基盤リソースである `VPC` と `SecurityGroup` を独立した `Network-Stack` へ移行するシナリオを考える。
Step 1: 移行先(新)テンプレートの準備と Import 宣言
まずは、移行先となる `Network-Stack.yaml` を作成する。ここで重要なのは、新規作成ではなく「インポート前提」の構文設計だ。
AWSTemplateFormatVersion: ‘2010-09-09’
Description: ‘Extracted Network Stack with Import Support’
Parameters:
Environment:
Type: String
Default: production
Resources:
# 既存の物理VPCをインポートするための定義
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub ‘${Environment}-vpc’
# 既存のセキュリティグループをインポートするための定義
MySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ‘Managed by Network-Stack’
VpcId: !Ref MyVPC
GroupName: !Sub ‘${Environment}-app-sg’
Outputs:
VpcId:
Description: The VPC ID
Value: !Ref MyVPC
Export:
Name: !Sub ‘${Environment}-VpcId’ # 移行元スタックから参照させるためのエクスポート
SecurityGroupId:
Description: The Security Group ID
Value: !Ref MySecurityGroup
Export:
Name: !Sub ‘${Environment}-SecurityGroupId’
Step 2: 資源マッピングファイル(Import Resource Mapping)の作成
AWS CLIを使って既存のリソースを新しいスタックに紐付けるには、リソースマッピングファイル(JSON)が不可欠だ。これこそが、CloudFormationのバックエンドに「新しく作るな、既存の物理リソースをこの論理IDにバインドせよ」と命令する低レイヤの指令書である。
`import-mapping.json`:
[
{
“ResourceType”: “AWS::EC2::VPC”,
“LogicalResourceId”: “MyVPC”,
“ResourceIdentifier”: {
“VpcId”: “vpc-0123456789abcdef0”
}
},
{
“ResourceType”: “AWS::EC2::SecurityGroup”,
“LogicalResourceId”: “MySecurityGroup”,
“ResourceIdentifier”: {
“GroupId”: “sg-0123456789abcdef0”
}
}
]
Step 3: AWS CLIによるアトミックなインポート実行
スタックを作成しつつ、既存リソースを取り込むコマンドを実行する。ここでの `create-stack` ではなく `create-change-set` の `IMPORT` タイプを使うのがプロの技だ。
!/usr/bin/env bash
set -euo pipefail
STACK_NAME=”Network-Stack”
TEMPLATE_FILE=”network-stack.yaml”
MAPPING_FILE=”import-mapping.json”
CHANGE_SET_NAME=”ImportNetworkResources-$(date +%s)”
echo “==> Creating Change Set for Resource Import…”
aws cloudformation create-change-set \
–stack-name “${STACK_NAME}” \
–template-body “file://${TEMPLATE_FILE}” \
–resources-to-import “file://${MAPPING_FILE}” \
–change-set-name “${CHANGE_SET_NAME}” \
–change-set-type “IMPORT” \
–parameters ParameterKey=Environment,ParameterValue=production
echo “==> Waiting for Change Set to be ready…”
aws cloudformation wait change-set-create-complete \
–stack-name “${STACK_NAME}” \
–change-set-name “${CHANGE_SET_NAME}”
echo “==> Executing Change Set…”
aws cloudformation execute-change-set \
–stack-name “${STACK_NAME}” \
–change-set-name “${CHANGE_SET_NAME}”
echo “==> Waiting for stack import to complete…”
aws cloudformation wait stack-import-complete \
–stack-name “${STACK_NAME}”
echo “Successfully imported resources into ${STACK_NAME}!”
このスクリプトが正常に完了した瞬間、AWS上の物理リソースは一切停止することなく、新しい `Network-Stack` の管理下に移行される。
—
4. 移行元(旧)スタックからの安全な「切り離し」ハック
新しいスタックでリソースの管理が始まっただけでは不十分だ。移行元の `Monolithic-App-Stack` 側にも、まだ同じリソースの定義(あるいはゾンビ参照)が残っている。
ここで単純にテンプレートからリソースを削除すると、CloudFormationは「物理リソースの削除」を実行してしまう。VPCやDBが消し飛ぶ瞬間である。これを防ぐには以下の手順を踏む。
1. 移行元テンプレートの書き換え(ImportValueへの置換)
移行元スタックのテンプレート内で、直接参照していた部分を `Fn::ImportValue` に書き換える。
変更前 (Monolithic-App-Stack)
VpcId: !Ref MyVPC
変更後 (Monolithic-App-Stack)
VpcId: !ImportValue production-VpcId
2. DeletionPolicy: Retain の適用とスタック更新
移行元テンプレートから削除したいリソース定義に対し、一時的に `DeletionPolicy: Retain` を付与してスタックを更新する。これにより、CloudFormationがそのリソースの管理権を手放す(スタックから切り離す)。
MyVPC:
Type: AWS::EC2::VPC
DeletionPolicy: Retain # ← これによりスタック削除時に物理リソースが保護される
Properties:
# … (既存のプロパティ)
この状態で移行元スタックを更新(Update Stack)し、CloudFormationの管理下から対象リソースのステートを安全に「パージ」する。その直後、移行元テンプレートから完全にそのリソースの記述を削除する。
—
5. 大規模運用における自動化スクリプトとCI/CDパイプライン統合
人間が手動でJSONマッピングファイルを書いたりCLIを叩いたりするミスは、障害の温床となる。本物のSREは、このプロセスを完全にコード化する。
以下は、タグや既存リソースのメタデータを動的にスキャンし、移行用マッピングJSONを自動生成するPythonスクリプトの一部である。
!/usr/bin/env python3
import boto3
import json
import sys
def generate_import_mapping(vpc_id, sg_id):
mapping = [
{
“ResourceType”: “AWS::EC2::VPC”,
“LogicalResourceId”: “MyVPC”,
“ResourceIdentifier”: {“VpcId”: vpc_id}
},
{
“ResourceType”: “AWS::EC2::SecurityGroup”,
“LogicalResourceId”: “MySecurityGroup”,
“ResourceIdentifier”: {“GroupId”: sg_id}
}
]
return json.dumps(mapping, indent=2)
if __name__ == “__main__”:
# 実運用ではAWS Systems Manager (SSM) パラメータストアやタグから動的に取得する
vpc_id = sys.argv[1] if len(sys.argv) > 1 else “vpc-0123456789abcdef0”
sg_id = sys.argv[2] if len(sys.argv) > 2 else “sg-0123456789abcdef0”
print(generate_import_mapping(vpc_id, sg_id))
このスクリプトをGitHub ActionsやGitLab CIのパイプラインに組み込み、IaCのモジュール分割を宣言的に実行できるようにする。
—
6. エキスパートの知見:クロススタック参照の限界と「次の一手」
`Fn::ImportValue` を用いた分割は強力だが、大規模環境(100を超えるマイクロサービス)になると、「エクスポートの削除ロック問題(Export cannot be deleted as it is in use)」という強烈なジレンマに直面する。基盤スタックのVPC IDを変更しようとした際、それを参照している無数のアプリスタックが存在するため、更新が一切通らなくなる現象だ。
この限界を突破するためのアーキテクチャ上の極意を授けよう。
1. SSMパラメータストア(Parameter Store)の活用
クロススタック参照の代わりに、ネットワークスタックの出力値を `AWS::SSM::Parameter` に書き込み、各アプリスタックは動的にSSMから値を取得(あるいはコンテナビルド時に注入)する。これにより、CloudFormationのハードな依存関係グラフから解放される。
2. スプリット・ブレインの回避(ネストしたスタックの利用禁止)
よくあるアンチパターンとして「Nested Stacks(入れ子スタック)」で分割を試みる者がいるが、結局親スタックが巨大化する悪夢から逃れられない。真のモジュール化は、独立した独立ライフサイクルを持つ「Root Stacks」同士を疎結合に連携させることだ。
—
結びにかえて
巨大化したCloudFormationテンプレートの分割は、手術で言えば「動いている心臓の血管をバイパスする」ようなものだ。恐怖心を伴うが、適切な道具(Resource Import)と厳密な手順を踏めば、システムを停止させることなく、美しくスケーラブルなインフラストラクチャへと生まれ変わらせることができる。
技術的負債を放置するな。コードの美しさは、システムの堅牢性とデプロイの俊敏性に直結する。今すぐその一枚岩のテンプレートにメスを入れろ。