Terraformの三層構造(compositions / infra-modules / resource-modules)でinfra-modulesをどう分割するか。結論はワークロード別をデフォルトにする。カテゴリ別にすると terraform plan の影響範囲が不必要に広がって、レビューもデプロイも辛くなります。
何を比較したか
Terraformのモジュール設計で三層構造を採用したときに、中間層のinfra-modulesをどう切るかで悩んだんですよね。
┌─────────────────────────────────────────┐ │ Composition Layer │ │ (環境別のエントリーポイント) │ │ 例: envs/prod/main.tf │ ├─────────────────────────────────────────┤ │ Infra-Modules Layer ← ここの話 │ │ (リソースモジュールを束ねる中間層) │ │ 例: infra-modules/xx-server/ │ ├─────────────────────────────────────────┤ │ Resource-Modules Layer │ │ (単一リソース群の再利用ブロック) │ │ 例: modules/vpc/, modules/sg/ │ └─────────────────────────────────────────┘
Resource-Modulesはリソース種別ごとの再利用ブロック、Composition Layerは環境ごとのエントリーポイント。その間のInfra-Modulesを「どういう単位で束ねるか」が設計判断のポイントです。
選択肢の整理
カテゴリ別 — リソース種別でまとめる
infra-modules/ ├── network/ # VPC + Subnet + Route Table ├── security/ # SG + IAM ├── compute/ # EC2 + ASG └── endpoints/ # VPC Endpoints
ディレクトリを見ただけで「ネットワーク系はここ」とわかるので、最初は直感的に見えます。
ただ運用してみるとこうなりがち。
- カテゴリ別だとmodule間でoutputを参照し合う構成になりやすい(computeがnetworkのsubnet_idを参照、など)。Terraformはこれを依存グラフとして扱うので、
network/を変更すると依存先のcompute/も一緒にplanの評価対象になる - modules間の相互参照(output → variable の受け渡し)が増えて、依存関係がスパゲッティ化する
- 「xx-serverのSGだけ変えたい」のに
security/全体がplan対象になる
ワークロード別 — 動くものの単位でまとめる
infra-modules/ ├── api-server/ # EC2 + SG + ALB Target Group ├── batch-processor/ # Lambda + SQS + IAM Role ├── shared-base/ # VPC + Subnet + NAT(共有リソース) └── monitoring/ # CloudWatch + SNS
「api-serverを構成するリソース一式」のように、実際に動くワークロード単位でまとめる方式。共有リソースは shared-base に逃がします。
比較表
| 観点 | カテゴリ別 | ワークロード別 |
|---|---|---|
| 直感的なわかりやすさ | 高い(種別で探せる) | 中(何が動いてるかで探す) |
| planの影響範囲 | 広がりがち | ワークロードで閉じる |
| modules間の依存 | 多い(相互参照) | 少ない(shared-base経由) |
| blast radiusの明確さ | 曖昧 | 明確 |
| 変更頻度の一致 | バラバラになりがち | 揃いやすい |
| 適するケース | 権限境界が明確な大組織 | それ以外ほぼ全部 |
どっちを選んだか・なぜか
ワークロード別をデフォルトにしました。決め手はライフサイクル(変更頻度)の一致です。ワークロード別に切った上で、compositions側でもワークロードごとに独立したroot module(= 独立したstate)を持つことで、planの対象範囲をワークロード内に閉じられます。
カテゴリ別で切ると…
networkを変更
→ network/ がplan対象
→ でもVPCは触りたくない、Subnetだけ変えたい
→ 関係ないリソースもplanに出る
ワークロード別で切ると…
api-serverを変更
→ api-server/ がplan対象
→ api-serverに関係するリソースだけplanに出る
同じタイミングで変更されるリソースは同じモジュールに入れる。違うタイミングで変更されるリソースは分ける。VPCとEC2は変更頻度がまるで違うのに同じ network/ に入れたら、片方を触るたびにもう片方がplanに巻き込まれます。
迷ったらこのフローで判断できます。
infra-module どう切る? │ ├─ 変更がワークロード単位で閉じる │ → ワークロード別(デフォルト) │ ├─ 組織の権限境界に合わせたい │ (ネットワークチームと開発チームで分割など) │ → カテゴリ別 │ └─ 迷ったら → ワークロード別
カテゴリ別にする場合は「組織の権限境界に合わせた」等の明示的な理由を求める、というルールにしておくと「なんとなくカテゴリ別にしちゃって後で辛くなる」パターンを防げます。
shared-baseの扱い
ワークロード別にしても、VPCやSubnetのように複数ワークロードから参照される共有リソースは出てきます。これは shared-base として切り出します。
infra-modules/ ├── shared-base/ # VPC, Subnet, NAT, Route Table │ # → 変更頻度: 低(初期構築後ほぼ触らない) ├── api-server/ # → shared-base の output を参照 ├── batch-processor/ # → shared-base の output を参照 └── monitoring/ # → shared-base の output を参照
shared-base はライフサイクルが「初期構築後ほぼ不変」なので、ワークロードと一緒にしない。結果として、日常的な変更は各ワークロードモジュール内で閉じます。
まとめ
infra-modulesの分割はデフォルトでワークロード別。判断基準は「変更頻度(ライフサイクル)が揃うかどうか」。この考え方はTerraformに限らず、マイクロサービスの分割やデータベースのスキーマ設計にも通じます。