/var/www/yatta47.log

/var/www/yatta47.log

やったのログ置場です。スクラップみたいな短編が多いかと。

Terraform infra-modulesの分割: カテゴリ別 vs ワークロード別の選び方

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に限らず、マイクロサービスの分割やデータベースのスキーマ設計にも通じます。

参考