1. はじめに
CloudFormationを使ってインフラを管理していると、スタックの分割単位についてよく聞かれることがあります。
実際の案件だと機能ごと(監視やネットワーク)に分割するのか、リソース単位で分割するのか、はたまた分割しないで1つに集約するのかなど、いろいろな選択肢があると思います。
特に現在であればCoding Agentを使ってコードを書くこと自体のハードルはかなり下がっている一方で、スタックの分割単位などの設計思想の部分はあまり語られていないような気がしております。今回の記事では私が普段案件でCFnを採用しているときに何を考えてスタック単位を分割しているのかを書こうと思います。
2. スタック分割で起きがちな問題
かれこれ5年ほどCloudFormationを使ってインフラ管理を行ってきましたが、その中で以下のようなことを経験・見聞きする機会がありました。
- テンプレートが数千行になって読むだけで辛い
- ちょっとした変更でもChange Setの作成・確認対象が増え時間がかかる
- 1つのリソースの変更失敗でスタック全体がロールバックされる
スタックをどの単位で分割するかは最初に考えた人の匙加減で決まってしまい、その後に与える影響が非常に大きいです。というのも一度作ってしまったテンプレートを分割することはできなくはないのですが、特に本番環境のように現在問題なく稼働している基盤に対してがっつりリファクタリングしようという判断がなされることはほぼないでしょう。※
このように将来的な拡張性・運用性まで考慮した上でスタックを分割するというのは中々に難しい問題なのです。
※私もこの記事を書いている時に知ったのですが、Stack Refactoringという機能があるみたいですね。今後はこういった機能を使って徐々にリファクタリングを行うことも増えてくるのでしょうか。
3. 想定読者・想定シナリオ
想定読者
本記事は以下の読者を想定しています。
- CloudFormationの基本的な使い方は理解しているインフラエンジニア
- スタック分割をこれから検討する、または既存の分割方針を見直したい方
- 「なんとなく分けている」状態から「根拠を持って分ける」状態にしたい方
※CloudFormationの基本的な構文やデプロイ方法は既知のものとして扱います。
想定シナリオ
以下の構成を前提に話を進めます。
| 項目 | 内容 |
|---|---|
| システム規模 | 中規模Webアプリケーション(数十リソース程度) |
| 構成要素 | VPC / ALB / ECS / RDS / S3 / CloudFront / IAM / CloudWatch など |
| 環境 | 本番・開発の2環境 |
| デプロイ頻度 | インフラ変更は月1〜2回 |
上記のような一般的なWebアプリケーション構成において、どのようにスタックを分割するかを考えていきます。
構成図に起こすと以下のようになるでしょうか。
4. スタック分割の考え方
スタック分割を考えるとき、私が1番重視しているのはテンプレートをシンプルに保ち、初心者でも読めるようにすることです。ほぼここに書いてあることが今回お話ししたい内容の9割と言っても過言ではありません。
このアプローチを取る背景にあるのが、「CloudFormationなどのIaCを採用しようとした際にそのスキルセットを元々持っている人がそこまでいない」ということが挙げられます。
新人などを除いて大概の人は以下のようなスキルセットであることが多い印象です。
- AWSは過去の案件でもやってきているのである程度の知識はある
- Ansibleなどの経験はあるけど、CloudFormationやTerraformは使ったことない
- ある程度のスクリプトを書いてきたことはあるけど、YAMLは初めて聞いた
新人の場合、ここに「AWSはこれから学びます」という条件が追加されていきます。
このようなメンバー状況の時にループ処理や条件分岐などを入れてしまうと、現状あるコードを理解するだけでもかなり負荷がかかります。運用の段階に入っても同様のことが起きるでしょう。そのため、私が1番に考えていることは「テンプレートをシンプルに保つ」ということだけなのです。
なので、多少非効率なテンプレートであったとしても「コードのシンプルさ、読みやすさ」を優先しています。恐らく、CloudFormationを長く書いている方からするとみっともないようなコードだと思いますが、これでオペミスが減って誰かが辛い思いをしなければ十分なのです。
「シンプルに保つ」とは言っても最低限の効率化は行います。例えば、Ref、GetAtt、ImportValueなどは私の目指すシンプルなコードを書くために絶対に必要になる組み込み関数なので利用します。
一方で、以下のような組み込み関数や機能は基本的に使いません。
-
Fn::ForEach:ループ処理。
AWS::LanguageExtensionsの利用が前提で便利だが、展開後の結果を頭で追いかけるのが難しい - Conditions:条件分岐。環境差分を吸収しようとすると、どんどんネストが深くなる
- DependsOn:依存関係の明示。スタック分割で解決できるなら、そちらを優先する
- Fn::If のネスト:条件分岐の入れ子。読み解くのに時間がかかる
これらを使わずにどうやってインフラを構築するのか。答えは「スタックの分割と作成順序で解決する」です。
テンプレート内で複雑なロジックを書く代わりに、スタックを適切な単位で分割し、正しい順番でデプロイすることで同じことを実現します。こうすれば、テンプレート自体は「何が作られるか」が一目でわかるシンプルな状態を保てます。
以降のセクションでは、この考え方に基づいた具体的な分割方針を説明していきます。
4.1 まずは最小単位で作る
テンプレートをシンプルに保つための第一歩として、私は最初にスタックを最小単位まで分割して作ります。
ここで言う「最小単位」とは、AWS公式が提供しているCloudFormationスニペット(または公式サンプル)を見たときに「この機能を成立させる最小セット」としてまとまっている粒度です。
なぜ最小単位で作るのか。それは「テンプレート内に複雑なロジックを書かなくて済む」からです。具体的には以下の2点が実現できます。
- DependsOnを書かなくても、作成順序で依存関係を解決できる
- Conditionをテンプレートに埋め込まずに済ませ、初心者でも読める形を保つ
例えばECSでWebアプリケーションを構築する場合、最初は以下のように「作成順序そのものを分割で表現」します。
vpc.yaml # VPC
subnet.yaml # サブネット
igw.yaml # インターネットゲートウェイ、VPCへのアタッチ
route-table.yaml # ルートテーブル、ルート
nat-gateway.yaml # NAT Gateway、Elastic IP
security-group.yaml # セキュリティグループ
target-group.yaml # ターゲットグループ
alb.yaml # ALB
alb-listener.yaml # ALBリスナー
ecs-cluster.yaml # ECSクラスター
ecs-taskdef.yaml # タスク定義
ecs-service.yaml # ECSサービス
rds.yaml # RDS、サブネットグループ、パラメータグループ
waf.yaml # WAF、IPセット
monitoring-ecs.yaml # ECS用のCloudWatchアラーム
monitoring-waf.yaml # WAF用のCloudWatchアラーム
ECSのようにリソース間の依存関係が強い構成(クラスター → タスク定義 → サービス)では、最初から1スタックにまとめると、参照や条件分岐が増えてテンプレートが読みにくくなりがちです。
一方で、最小単位で分けておけば「順番に積み上げれば動く」形になり、テンプレート側に複雑さを持ち込みにくくなります。
この段階では細かすぎても問題ありません。後から統合するのは比較的簡単ですが、巨大なスタックを後から分割するのは、リソース移動・Export/Import整理・置き換えの発生などが絡み、難易度が一気に上がるからです。
※毎回案件のたびにこの最小単位のテンプレートを書いているわけではありません。今は基本的に過去案件のものをベースにしながら少し手を加える程度です。
4.2 動作確認ができたら運用単位にまとめる
最小単位で作成して動作確認ができたら、次は運用しやすい単位にまとめていきます。
ここでも「シンプルさ」が判断基準になります。重要なのは「全部まとめる」のではなく、まとめてもテンプレートの可読性が落ちないものだけをまとめるという判断です。
例えば、VPC・サブネット・インターネットゲートウェイ・NAT Gatewayは依存関係が一方向で単純なため、ある程度1つのネットワークスタックに統合しても問題ありません。ALB・ターゲットグループ・リスナーも同様です。RDSとパラメータグループ、サブネットグループも一緒に管理するのが自然です。
一方で、セキュリティグループやルートテーブルは後ほど触れますが他のネットワークリソースと比較して、変更頻度が高い(ルール追加・削除など)ため分けておくほうが運用しやすくなります。
# 統合後
network.yaml # VPC、サブネット、IGW、NAT Gateway
route-table.yaml # ルートテーブル
security-groups.yaml # セキュリティグループ
alb.yaml # ALB、ターゲットグループ、リスナー
ecs-cluster.yaml # ECSクラスター
ecs-service.yaml # ECSサービス、タスク定義
rds.yaml # RDS、サブネットグループ、パラメータグループ
waf.yaml # WAF、IPセット
monitoring-ecs.yaml # ECS用のアラーム
monitoring-rds.yaml # RDS用のアラーム
monitoring-waf.yaml # WAF用のアラーム
統合するかどうかは以下の観点で判断します。
統合しても良いケース
- 依存関係が単純で、
DependsOnを書かなくても順序が決まるもの - 変更頻度が近いもの同士
- コード量が少なく、統合しても可読性が落ちないもの
統合してはいけないケース
- コードが長くなりそうなもの(監視など)
- 変更頻度が大きく異なるもの
特に監視(CloudWatchアラーム、ダッシュボード)は他のリソースと統合してはいけません。アラームを数十個定義するとそれだけで数百行になり、他のリソースと混ぜると「どこに何があるのか」がわからなくなります。
監視は1つのスタックにまとめるのではなく、監視対象のリソースごとに分けます。
monitoring/
├── ecs-alarms.yaml # ECS用のアラーム
├── rds-alarms.yaml # RDS用のアラーム
├── waf-alarms.yaml # WAF用のアラーム
こうしておくと、ECSの構成を変更したときに関連するアラームだけを修正すれば済みます。また、アラームの追加・削除で他のリソースに影響を与えるリスクもなくなります。
変更頻度の目安は以下の通りです。
| 変更頻度 | リソース例 |
|---|---|
| 高(月1以上) | WAFのIP登録・削除、CloudWatchアラーム閾値などの設定変更 |
| 中(数ヶ月に1回程度) | ルートテーブルの設定変更、セキュリティグループのルール追加 |
| 低(半年〜年1回) | RDSインスタンスタイプ変更 |
| ほぼ変わらない | VPCのCIDR、AZ構成、基盤IAMロール |
変更頻度が大きく異なるものを一緒にすると、頻繁な変更のたびに「本当は触りたくない」安定したリソースもChange Setの対象になり、確認コストが増えます。
スタックの更新後に「他リソースに影響がないこと」という確認観点を本番作業などで作らないためにもある程度の分割は必要になってきます。
4.3 循環参照を作らない
テンプレート単体をシンプルに保つのと同様に、スタック間の依存関係もシンプルに保つ必要があります。スタックを分割するとスタック間で値を参照する必要が出てきますが、このときに注意すべきなのが循環参照です。
CloudFormationのスタック間参照(Export/ImportValue)で循環参照が発生すると、依存関係を切り離すまでスタック削除やExport変更ができなくなり、更新の自由度も大きく下がります。これは本当に厄介で、一度ハマると解消するのに結構な時間がかかります。
[良い例:一方向の依存]
ネットワークスタック ──Export──→ コンピューティングスタック ──Export──→ 監視スタック
[悪い例:循環参照]
スタックA ←Export/Import→ スタックB
(お互いのOutputsを参照し合う)
そのため依存関係は常に一方向になるように設計します。
4.1で「最小単位で作り、順番に積み上げる」という方針を取っていれば、自然と一方向の依存関係になります。逆に言えば、循環参照が発生しそうになったら、それはスタックの分割単位が適切でないサインです。
どうしても相互参照が必要になった場合は、以下のいずれかで解決します。
- スタックをマージする(そもそも分割しない)
- Parameter渡しに切り替える(Exportを使わない)
- 参照の方向を見直す(設計を再検討する)
4.4 命名規則で事故を防ぐ
テンプレートをシンプルに保っても、それを運用する際に混乱が生じては意味がありません。スタックを分割すると当然スタックの数が増えるため、命名規則で「わかりやすさ」を担保する必要があります。
「どのシステムの、どの環境の、何のリソースか」が名前から判断できないと、Aというお客さんのALBに付与されているWAFを触ろうと思ったら別のWAFを触っていたなんてことが起きます。
私は以下の形式を基本としています。
{システム名}-{環境名}-{用途}-{リソース種別}-{※必要な場合リージョン}
用途が1つしかない場合は省略しても構いませんが、ECSやALBのように複数立てることが多いリソースは用途を入れておくと区別がつきやすくなります。
例:
# ネットワーク(用途は1つなので省略)
myapp-prd-network
myapp-prd-security
# ECS(API用とバッチ用で2つある場合)
myapp-prd-api-ecs-cluster
myapp-prd-api-ecs-service
myapp-prd-batch-ecs-cluster
myapp-prd-batch-ecs-service
# ALB(外部公開用と内部通信用で2つある場合)
myapp-prd-public-alb
myapp-prd-internal-alb
# RDS(用途は1つなので省略)
myapp-prd-rds
# WAF(用途ごとに分ける)
myapp-prd-api-waf
myapp-prd-alb-waf
# 監視(対象リソースごと)
myapp-prd-api-monitoring-ecs
myapp-prd-batch-monitoring-ecs
myapp-prd-monitoring-rds
この形式のメリットは以下の通りです。
- 一覧で見たときに環境が明確:マネジメントコンソールでスタック一覧を見たときに、どの環境の何のスタックかがすぐわかる
-
誤操作の防止:
prdという文字列が入っていれば「本番だ」と意識できる - 用途の区別:同じリソース種別でも「どの用途のものか」がわかる
- ソート時に見やすい:同じシステム・環境・用途のスタックが並んで表示される
命名規則は分割単位と密接に関係します。「どう分けるか」と同時に「どう名付けるか」も最初に決めておくと、後々の運用がスムーズになります。この部分をちゃんとやっておくと「既存スタックを更新すべきなのか」、「新規スタックを作成すべきなのか」が容易に判断できるようになります。
何も考えずに同じリソースだから同じテンプレートに足せばいいという考えやめておきましょう。
4.5 デプロイ順序の管理
スタックを分割したら、次に気になるのは「どうやって正しい順序でデプロイするか」です。
ここでも「シンプルさ」を優先します。理想を言えばCI/CDパイプラインで依存関係を定義して自動化したいところですが、現実的にはマネジメントコンソールから手動・CLIでデプロイするケースもあり得るでしょう。複雑なCI/CDを構築するよりも、誰でもわかるシンプルな方法でデプロイ順序を管理するほうが、チーム全体で運用しやすくなります。
私の場合、デプロイ順序の管理は以下のようにしています。
1. READMEにデプロイ順序を明記する
## デプロイ順序
以下の順番でスタックをデプロイしてください。
1. myapp-prd-network
2. myapp-prd-security
3. myapp-prd-rds
4. myapp-prd-alb
5. myapp-prd-ecs-cluster
6. myapp-prd-ecs-service
7. myapp-prd-waf
8. myapp-prd-monitoring-ecs
9. myapp-prd-monitoring-rds
10. myapp-prd-monitoring-waf
シンプルですが、これがあるだけで「どこから手をつければいいか」が明確になります。
補助的にExcelなどで管理する方法もあります。このあたりは慣れている人だと感覚的に順番を判断できますが、初心者には難しいため、最低限の手順はドキュメント化しておくべきだと考えています。
5. スタック間の依存関係の扱い方
スタックを分割すると、スタック間で値を受け渡す必要が出てきます。
私のような分割方法をしているとこの部分はほぼ避けては通れません。
5.1 Export/ImportValueの使いどころ
基本的な使い方
# network-stack.yaml(Export側)
Outputs:
VpcId:
Value: !Ref VPC
Export:
Name: !Sub ${AWS::StackName}-VpcId
PublicSubnet1:
Value: !Ref PublicSubnet1
Export:
Name: !Sub ${AWS::StackName}-PublicSubnet1
PublicSubnet2:
Value: !Ref PublicSubnet2
Export:
Name: !Sub ${AWS::StackName}-PublicSubnet2
# application-stack.yaml(Import側)
Resources:
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Subnets:
- !ImportValue network-stack-PublicSubnet1
- !ImportValue network-stack-PublicSubnet2
Export/ImportValueが適しているケース
- 参照元が安定していて変更頻度が低い(VPC ID、サブネットIDなど)
- 複数のスタックから同じ値を参照する
- 依存関係を明示的に表現したい
5.2 Export/ImportValueの落とし穴
他スタックからImportされると、Exportの変更・削除が困難になる
これがExportの最大の落とし穴です。
1. network-stackでVpcIdをExportする
2. app-stackでVpcIdをImportする
3. network-stackのVpcIdをExportから削除したい
→ エラー:app-stackの方でVpcIdが参照されているため、修正したい場合にはapp-stackの該当箇所の修正・削除を実施する必要があります。
Exportを削除するには、参照しているすべてのスタックからImportを削除する必要があります。これは大規模環境では非常に面倒な作業になります。
対策
- 本当に複数スタックから参照されるものだけをExportする
- Export名には必ずスタック名を含めて一意にする
- 変更の可能性があるものはExportしない
1点注意ですが、Export/ImportValueは同一アカウント・同一リージョン内のみで利用可能です。
5.3 Parameter渡しとの使い分け
Parameter渡しの例
# application-stack.yaml
Parameters:
VpcId:
Type: AWS::EC2::VPC::Id
Description: VPC ID from network stack
SubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: Subnet IDs from network stack
マネジメントコンソールからスタックを作成する際、「パラメータの指定」画面でVpcIdやSubnetIdsの値を入力します。AWS::EC2::VPC::IdやList<AWS::EC2::Subnet::Id>のようなパラメータ型を使うと、ドロップダウンから既存のリソースを選択できるようになるので便利です。
Parameter渡しが適しているケース
- Exportの依存関係を作りたくない
- 同じテンプレートを複数の環境・VPCで使いまわしたい
Export vs Parameter の判断基準
| 観点 | Export/ImportValue | Parameter渡し |
|---|---|---|
| 依存関係の明示性 | 明示的(CFnが依存を認識) | 暗黙的(人が管理) |
| 変更の柔軟性 | 低い(削除が面倒) | 高い(いつでも変更可能) |
| デプロイの独立性 | 低い(Export元に依存) | 高い(どこでもデプロイ可能) |
| 複数スタック参照 | 得意 | 毎回入力が必要 |
私の経験上、以下の使い分けをすることが多いです。
- Export:VPC ID、サブネットID、共通セキュリティグループなど、本当に安定していて複数スタックから参照されるもの
- Parameter:環境変数的に使う値(prd/stg/devなど)、アプリケーション設定(インスタンスタイプなど)
1点注意なのですが、パラメータの変更を行うと当然ですが更新による影響が発生します。(場合によってはリソースの置換)
ここで記載している内容は「Export由来の削除不能問題を回避する方法」ということになります。
6. 実案件での判断事例
ここでは私が実際に担当した案件での判断事例を紹介します。
事例1:中規模Webアプリケーション(ALB + ECS + RDS構成)
今回の例で挙げていた構成と1番近い形になります。
要件
- 単一のWebアプリケーション
- インフラチーム3名
- デプロイ頻度:インフラは月1回程度
採用した分割方針
stacks/
├── network.yaml # VPC, Subnet, IGWなど
├── route-table.yaml # Route Table
├── alb.yaml # ALB
├── security.yaml # Security Group, NACL
├── database.yaml # RDS, Parameter Group
├── application.yaml # ECS Service, TaskDefinition, Service Auto Scaling
└── monitoring/
├── ecs-alarms.yaml # ECS用のアラーム
└── rds-alarms.yaml # RDS用のアラーム
判断の根拠
| スタック | 分割理由 |
|---|---|
| network | 構築後ほぼ変更なし。複数システムで共有の可能性あり |
| route-table | 変更頻度が他のネットワークリソースと異なるため、ルートテーブルだけ別出しで管理 |
| alb | Sorryページなどでターゲットグループ、リスナールールを追加することはあれど更新はほぼなし |
| security | ネットワークとは独立して変更したい。セキュリティレビューの単位として分離 |
| database | ミスの影響が大きいので他スタックとの統合は基本NG |
| application | 変更頻度が高い |
| monitoring | アラームは対象リソースごとに分割。コード量が多くなるため1ファイルにまとめない |
結果
- 担当者がapplication.yamlを気軽に変更できるようになった
- インフラ変更時の影響範囲が明確になり、運用段階での変更へのハードルを下げることができた
事例2:マルチテナントSaaS(ECS + Aurora構成)
要件
- テナントごとに独立したECSサービスとALBリスナールールが必要
- DBは共通のAuroraを使用し、テナントはスキーマやテーブルで分離
- テナントの追加が比較的頻度高めで発生
採用した分割方針
stacks/
├── foundation/
│ ├── network.yaml # VPC, Subnet(共通)
│ ├── database.yaml # Aurora(共通)
│ ├── alb.yaml # ALB本体, HTTPS Listener, ALB SG(共通)
│ ├── ecs-cluster.yaml # ECS Cluster(共通)
│ └── ecr.yaml # ECR
│
└── tenants/
├── tenant-a/
│ ├── service.yaml # ECS Service, TaskDefinition, Security Group
│ └── routing.yaml # Target Group, ALB Listener Rule
├── tenant-b/
│ ├── service.yaml
│ └── routing.yaml
└── tenant-template/ # テナント追加時に流すスタックをまとめておく
判断の根拠
- DBは共通基盤に配置:テナントごとにAuroraを立てるとコストが膨大になる
-
テナント追加時の作業を簡略化したい:
tenant-template配下に資材をまとめることで誰でも追加作業が行えるようにする
苦労した点
- スタック一覧が見づらくなるため、命名規則の統一が重要だった
- Exportを使うとテナント削除時にImportの解除が面倒なため、
ListenerArnなどはParameter渡しを多用した - Listener Rule / Target Groupの命名に顧客名を含める規則を徹底し、どのテナントの経路かを即座に判別できるようにした
7. アンチパターン
ここまでの内容を踏まえて「これはやらないほうがいい」と感じたパターンを整理しておきます。
7.1 全部1スタックに詰め込む
問題点
- テンプレートが肥大化して可読性が低下する
- 小さな変更でも全リソースのChange Setが走る
- 1つのリソースの変更失敗でスタック全体がロールバックされる
- デプロイ時間が長くなる(500リソース超えると10分以上かかることも)
CloudFormationの制限値
| 項目 | 上限 |
|---|---|
| テンプレートサイズ(リクエスト本文) | 51,200 bytes |
| テンプレートサイズ(S3経由) | 1 MB |
| スタックあたりのリソース数 | 500 |
| スタックあたりのOutput数 | 200 |
| スタックあたりのParameter数 | 200 |
特にリソース数500の上限は意外と早く到達します。CloudWatch Alarmを大量に定義したり、セキュリティグループのルールを細かく定義したりすると、すぐに上限に近づきます。
過去に私もCloudWatch Alarmのテンプレートで3000行を超えるものを保守していた時がありましたが、二度とやりたくはないですね...
途中から新しいスタックに分割したことで、古いスタックは更新時のみ触る運用になり、心理的な安全性は保たれるようになりました。
7.2 細かく分割しすぎる
問題点
- スタック間の依存関係が複雑になる
- デプロイ順序を人間が管理する必要が出てくる
- Export/Importの依存関係により、Export側スタックの削除やExport値の変更ができなくなる
- 全体像が把握しづらくなる
具体例
# やりすぎな分割例
stacks/
├── vpc.yaml
├── public-subnet-1a.yaml
├── public-subnet-1c.yaml
├── private-subnet-1a.yaml
├── private-subnet-1c.yaml
├── internet-gateway.yaml
├── nat-gateway-1a.yaml
├── nat-gateway-1c.yaml
├── route-table-public.yaml
├── route-table-private-1a.yaml
├── route-table-private-1c.yaml
... (続く)
1つのリソースに1つのスタックというのは分割のしすぎです。論理的にまとまりのある単位で分割しましょう。
7.3 Export乱用で変更できなくなる
問題点
1. 「将来使うかもしれないから」とりあえず全部Exportする
2. 複数のスタックからImportされる
3. Export元のリソースを変更したい
4. エラー:Export is still in use by xxx-stack, yyy-stack, zzz-stack...
5. 全部の参照を解除するのが大変で変更を諦める
対策
- 本当に必要なものだけをExportする
- 「とりあえずExport」は避ける
- 変更の可能性があるものはParameter渡しを使う
- Exportする場合は、参照元スタックを把握しておく
7.4 Conditionsによる条件分岐を多用する
これはスタック分割とは少し異なる話ですが、IaCの可読性という観点で触れておきたいポイントです。
私のスタンス:条件分岐はできるだけ避ける
CloudFormationのConditionsは便利な機能ですが、私は基本的に条件分岐を避ける方針を取っています。理由はシンプルで、チームに新しく入ったメンバーがテンプレートを読んだときに、一発で理解できるかどうかが怪しくなるからです。
# Conditionsが多すぎて読めない例
Conditions:
IsProd: !Equals [!Ref Environment, prod]
IsStg: !Equals [!Ref Environment, stg]
IsDev: !Equals [!Ref Environment, dev]
IsProdOrStg: !Or [!Condition IsProd, !Condition IsStg]
IsNotDev: !Not [!Condition IsDev]
NeedsMultiAZ: !And [!Condition IsProd, !Condition EnableHA]
# ... まだまだ続く
Resources:
Database:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceClass: !If
- IsProd
- db.r5.large
- !If
- IsStg
- db.r5.medium
- db.t3.medium
MultiAZ: !If [NeedsMultiAZ, true, false]
# ... Ifのネストが深くなる
上記のテンプレートを見て「本番環境ではDBInstanceClassは何になるのか?」「MultiAZはどういう条件でtrueになるのか?」をすぐに答えられるでしょうか。私は正直、頭の中でConditionsを展開しながら追いかけないと分かりません。
「理解できないコードを本番環境に適用しない」という原則
最近はAIにコードを書かせることも増えてきましたが、だからこそ「自分(またはチームメンバー)が理解できないコードを本番環境に入れない」という原則が重要になってきていると思います。
Conditionsを駆使した「賢い」テンプレートよりも、環境ごとにテンプレートを分けた「愚直な」テンプレートのほうが、以下の点で優れています。
- 新しいメンバーがすぐに理解できる
- 「本番環境では何がデプロイされるか」を即座に確認できる
- デバッグ時に条件を追いかける必要がない
- レビューで「この条件分岐、合ってる?」という確認が不要
余談ですが私は新卒入社して以来ずっとインフラ領域いたこともあり「コードをいっぱい書いてきた経験」があるかと言われるとそうではないです。ただ、コードを大量に書いてきた経験がないことが功を奏して「愚直な」テンプレートを作る元になっているのかもしれません。
代替案
環境差分を吸収したい場合は、以下の方法を検討してください。
-
パラメータで値だけを切り替える
- 同じテンプレートを使い、マネジメントコンソールのパラメータ入力画面で環境ごとに値を変える
- テンプレートはシンプルに保ち、環境差分はパラメータに閉じ込める
-
環境ごとにテンプレートを分ける
stacks/ ├── prd/ │ └── rds.yaml # 本番用(MultiAZ: true, db.r5.large) └── dev/ └── rds.yaml # 開発用(MultiAZ: false, db.t3.medium)重複は発生しますが、各環境の構成が明確
-
どうしてもConditionsを使う場合は最小限に
- ネストした
!Ifは避ける - 1つのテンプレートに3つ以上のConditionが出てきたら分割を検討する
- ネストした
IaCは「インフラをコードで管理する」ための道具であって、「プログラミングの技巧を見せる場所」ではありません。シンプルで誰でも読めるテンプレートを心がけましょう。
8. まとめ
本記事で扱った内容を整理します。
スタック分割の基本方針
-
まず最小単位で作る
- AWS公式スニペットの粒度を目安に、機能を成立させる最小セットで分割
- 依存関係が複雑なリソース(ECSなど)は特に細かく分けておく
-
動作確認ができたら運用単位にまとめる
- 依存関係が単純で、変更頻度が近いものだけを統合
- 監視は絶対に他リソースと統合しない(リソース種別ごとに分割)
-
循環参照を作らない
- 依存関係は常に一方向に
- 最小単位で作り順番に積み上げれば自然と一方向になる
-
命名規則で事故を防ぐ
-
{システム名}-{環境名}-{用途}-{リソース種別}の形式で統一 - どの環境の何のリソースかが一目でわかるようにする
-
-
デプロイ順序は運用側で管理する
- READMEにデプロイ順序を明記する
- ドキュメントに残しておくことで初心者でも迷わない
統合の判断基準
| 観点 | 統合しても良い | 統合してはいけない |
|---|---|---|
| 依存関係 | 単純でDependsOn不要 |
複雑で!RefやDependsOnが増える |
| 変更頻度 | 近いもの同士 | 大きく異なる |
| コード量 | 統合しても可読性が落ちない | 長くなりそう(監視など) |
Export vs Parameter の使い分け
| 使い分け | Export/ImportValue | Parameter渡し |
|---|---|---|
| 使う場面 | VPC ID、サブネットIDなど安定した共通リソース | 環境設定、アプリケーション固有の値 |
| 避ける場面 | 変更の可能性があるリソース | 多数のスタックから同じ値を参照する場合 |
避けるべきこと
-
DependsOnやConditionの多用(初心者が読めなくなる) - 全部を1スタックに詰め込む(肥大化、Change Setの遅延)
- 細かく分割しすぎる(依存関係の爆発)
- とりあえずExport(後から変更・削除できなくなる)
9. おわりに
CloudFormationのスタック分割に正解はありません。
ただ、私が大事にしているのは「IaCに慣れていない人でもテンプレートを読んで理解できる状態を保つ」ことです。
条件分岐や複雑な依存関係をテンプレートに書き込むのではなく、スタックの分割と作成順序で解決する。そうすれば、新しくチームに入った人も「上から順番に作れば動く」とわかります。
本記事の考え方が、皆さんのスタック設計の参考になれば幸いです。

