「このテンプレート、もう誰も読めません」を防ぐ — IaCスタック分割の5つの設計原則
はじめに — IaCの「技術的負債」は静かに積み上がる
IaC(Infrastructure as Code)を導入したプロジェクトで、こんな光景を見たことはないだろうか。
- テンプレートが数千行に膨れ上がり、読むだけで1時間かかる
- 1つのリソース変更で Change Set の確認対象が数十個に膨らむ
- セキュリティグループの修正ミスでスタック全体がロールバックされる
- 新メンバーが「どこから読めばいいですか?」と途方に暮れる
コードを書くこと自体のハードルはCoding Agentの登場で大きく下がった。しかし、スタックの分割単位をどう設計するかという判断は、ツールでは解決きない。そして一度作ったスタック構成を後から変えるのは、本番稼働中のシステムでは極めて難しい。
本記事では、CloudFormationやTerraformといったIaCツールに共通するスタック分割の5つの設計原則を、実例を交えて解説する。
対象読者
- IaCの基本的な使い方は理解しているインフラエンジニアaaa
- スタック分割をこれから設計する、または既存の方針を見直したい方
- 「なんとなく分けている」状態から根拠を持って分割したい方
原則1: 可読性を最優先にする — 新メンバーが読めるか?
スタック分割で最も重視すべきは、テンプレートの可読性だ。
IaCを採用するチームのメンバー構成を考えてみよう。実際のプロジェクトでは、以下のようなスキルセットの人が混在していることが多い。
- AWSの実務経験はあるが、CloudFormation/Terraformは初めて
- シェルスクリプトは書けるが、YAML/HCLの記法に慣れていない
- インフラ経験自体がこれからという新メンバー
こうした状況で、ループ処理や複雑な条件分岐をテンプレートに詰め込むと、コードを理解するだけで大きな負荷がかかる。
判断基準: 1ファイル200行以内を目安にする
テンプレート1ファイルの目安は200行以内。これを超えたら分割を検討する。200行なら、エディタでスクロールせずに全体像を把握できる。
# 悪い例: 1ファイルに全部入り(network + compute + monitoring)
# 2000行超のテンプレートは誰も読みたくない
AWSTemplateFormatVersion: '2010-09-09'
Resources:
VPC: ... # 50行
Subnets: ... # 100行
SecurityGroups: ... # 80行
EC2Instances: ... # 200行
ALB: ... # 150行
RDS: ... # 100行
CloudWatch: ... # 200行
SNS: ... # 50行
# ... まだまだ続く
# 良い例: 関心事ごとに分割
# network.yaml — VPC, Subnet, RouteTable(80行)
# security.yaml — SecurityGroup, WAF(60行)
# compute.yaml — EC2, ALB, AutoScaling(120行)
# database.yaml — RDS, ElastiCache(80行)
# monitoring.yaml — CloudWatch, SNS(70行)
原則2: ライフサイクルで分割する — 変更頻度が違うものは別スタックにする
インフラリソースには、それぞれ固有の変更ライフサイクルがある。
VPCやIAMロールは一度作ったら滅多に変更しない。一方、ECSのタスク定義やLambda関数はデプロイのたびに更新される。これらを同一スタックに入れると、Lambda関数の更新で万が一ロールバックが走った場合に、VPCまで巻き添えになるリスクがある。
Terraformでの実践
Terraformの場合、ディレクトリ構成でライフサイクルを表現する。
infrastructure/
01-network/ # VPC, Subnet, NAT Gateway
main.tf
variables.tf
outputs.tf
02-security/ # IAM, Security Groups
main.tf
03-database/ # RDS, ElastiCache
main.tf
04-compute/ # ECS, ALB, Auto Scaling
main.tf
05-monitoring/ # CloudWatch, SNS
main.tf
ディレクトリ名に連番を付ける。これだけでデプロイ順序が自明になる。READMEに長々と手順を書くよりも、ファイルシステムの構造自体がドキュメントになる。
原則3: 依存関係は一方向に保つ — 循環参照を絶対に作らない
スタックを分割すると、スタック間で値を参照する必要が出てくる。ここで最も注意すべきは循環参照だ。
CloudFormationのスタック間参照(Export / Fn::ImportValue)で循環参照が発生すると、依存関係を解消するまでスタックの削除もExportの変更もできなくなる。一度ハマると解消に膨大な時間を取られる。
循環参照を防ぐルール
- 依存の方向を固定する: 下位レイヤ → 上位レイヤの一方向のみ
-
Export名に規約を設ける:
${AWS::StackName}-VpcIdのように、どのスタックが提供する値か明確にする - スタック間の参照数を制限する: 1スタックからのExportが10個を超えたら、分割粒度を見直す
# network.yaml の Outputs
Outputs:
VpcId:
Value: !Ref VPC
Export:
Name: !Sub "${AWS::StackName}-VpcId"
PublicSubnetA:
Value: !Ref PublicSubnetA
Export:
Name: !Sub "${AWS::StackName}-PublicSubnetA"
# compute.yaml から参照
Resources:
WebServer:
Type: AWS::EC2::Instance
Properties:
SubnetId: !ImportValue "network-stack-PublicSubnetA"
SecurityGroupIds:
- !ImportValue "security-stack-WebSG"
Terraformの場合は terraform_remote_state データソースや、SSM Parameter Store経由の参照で同様の一方向依存を実現できる。
原則4: 条件分岐よりテンプレート分割を選ぶ
環境ごとの差異(dev / stg / prd)をどう扱うかは、IaCの設計で最も議論になるポイントの一つだ。
結論から言うと、Conditions による条件分岐は極力避けたほうがいい。
# 避けたい例: Conditionsの多用
Conditions:
IsProd: !Equals [!Ref Env, "prd"]
IsNotProd: !Not [!Equals [!Ref Env, "prd"]]
NeedsMultiAZ: !Or
- !Equals [!Ref Env, "prd"]
- !Equals [!Ref Env, "stg"]
Resources:
DBInstance:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceClass: !If [IsProd, "db.r6g.xlarge", "db.t3.medium"]
MultiAZ: !If [NeedsMultiAZ, true, false]
StorageEncrypted: !If [IsProd, true, false]
# ... 条件分岐だらけで何が実際にデプロイされるか読めない
条件分岐が増えるほど、「このテンプレートをデプロイすると実際に何が作られるのか?」が読み取れなくなる。新メンバーがテンプレートを見て、頭の中で条件分岐を全部展開しないと構成がわからない状態は避けるべきだ。
代替案: パラメータファイルで差異を吸収する
# template.yaml — 構造はシンプルに保つ
Resources:
DBInstance:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceClass: !Ref DBInstanceClass
MultiAZ: !Ref MultiAZ
StorageEncrypted: !Ref StorageEncrypted
# params/prd.json
[
{"ParameterKey": "DBInstanceClass", "ParameterValue": "db.r6g.xlarge"},
{"ParameterKey": "MultiAZ", "ParameterValue": "true"},
{"ParameterKey": "StorageEncrypted","ParameterValue": "true"}
]
# params/dev.json
[
{"ParameterKey": "DBInstanceClass", "ParameterValue": "db.t3.medium"},
{"ParameterKey": "MultiAZ", "ParameterValue": "false"},
{"ParameterKey": "StorageEncrypted","ParameterValue": "false"}
]
テンプレート本体はどの環境でも同じ。差異はパラメータファイルに閉じ込める。「prdだけ存在するリソース」が必要な場合は、最低限の Condition を使うか、専用のスタックとして分離する。
原則5: デプロイ順序を「構造」で表現する
スタックを分割したら、次に問題になるのがデプロイ順序だ。CI/CDパイプラインで依存関係を定義して自動化するのが理想だが、実際にはマネジメントコンソールやCLIから手動デプロイするケースもある。
デプロイ順序は、複雑なCI/CDを組むよりも誰でもわかるシンプルな方法で管理するほうが運用しやすい。
実践: ディレクトリ名とREADMEの二重管理
infrastructure/
README.md # デプロイ順序を明記
01-network/
02-security/
03-database/
04-compute/
05-monitoring/
# デプロイ順序
| 順序 | スタック | 依存先 | 備考 |
|------|--------------|--------------|------------------------|
| 1 | 01-network | なし | VPC, Subnetを作成 |
| 2 | 02-security | 01-network | SG, IAMを作成 |
| 3 | 03-database | 01, 02 | RDSはMulti-AZ注意 |
| 4 | 04-compute | 01, 02, 03 | ECS + ALB |
| 5 | 05-monitoring | 04 | アラーム閾値は要確認 |
ディレクトリの連番が「構造としてのドキュメント」になり、READMEが「補足説明」になる。新メンバーが ls するだけでデプロイ順序がわかる状態が理想だ。
ありがちなアンチパターン
最後に、実際のプロジェクトで見かけるアンチパターンをまとめる。
| アンチパターン | 問題 | 対策 |
|---|---|---|
| 全部入りモノリス | テンプレートが数千行。変更影響が読めない | ライフサイクルベースで分割 |
| 過剰な細分化 | リソース1個ずつスタック化。スタック数が50超 | 関連リソースはまとめる(VPC+Subnetなど) |
| Conditions地獄 | 環境分岐が10個以上。実態が読み取れない | パラメータファイルで差異を吸収 |
| 循環参照 | スタック間でExportが双方向 | 依存は一方向。下位→上位の流れを固定 |
| 暗黙のデプロイ順序 | 「Aさんしか順番を知らない」 | ディレクトリ連番 + READMEで明示化 |
まとめ — 「上から順番に作れば動く」を目指す
IaCテンプレートの分割に唯一の正解はない。しかし、判断の軸は明確だ。
- 可読性: 新メンバーがテンプレートを読んで理解できるか
- ライフサイクル: 変更頻度が異なるリソースは別スタックにする
- 依存方向: スタック間の参照は一方向に保つ
- 明示性: 条件分岐を減らし、パラメータで差異を吸収する
- 順序の構造化: デプロイ順序をディレクトリ構造で表現する
すべてに共通するのは、**「IaCに慣れていない人でもテンプレートを読んで理解できる状態を保つ」**という方針だ。
凝った機能を使いこなすことよりも、チームの誰もが安心してインフラを変更できる状態を作ること。それがIaC運用の成功を左右する。
Discussion