Terraformモジュール設計の難しさとどう向き合うか
こんにちは。@putcho01 です。
普段の業務では新規サービスの開発に従事しており、インフラ構築で Terraform を使う機会が多くあります。
Terraform を書いていて悩ましいのが、モジュールの切り方です。
「複数環境で同じ構成を使いたい」「変更の影響範囲を限定したい」こうした理由でモジュール化を考えるものの、どこまでモジュール化すべきか、どの粒度が適切かの判断は難しいです。
この記事では、僕がモジュール設計を考えるときの思考プロセスを、ソフトウェア設計の概念と絡めながら整理してみます。ベストプラクティスの解説というより、一人のエンジニアの考え方を共有する記事として読んでいただければ幸いです。
なぜモジュール設計は難しいのか
「複雑性」の本質
『A Philosophy of Software Design』では、複雑性について以下のように書かれています。
Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.
(複雑性とは、ソフトウェアシステムの構造に関連するもので、システムの理解と変更を困難にするものすべてを指す)
つまり、「理解しづらい」「変更しづらい」と感じたら、それは複雑性のサインです。
同書では、複雑性は次の 3 つの症状として現れると説明されています。
1. 変更の増大(Change Amplification)
1 つの変更が複数箇所に波及する状態
2. 認知負荷(Cognitive Load)
理解すべき情報量が多すぎる状態
3. 未知の未知(Unknown Unknowns)
何がわからないかがわからない状態
複雑性を生み出すもの
複雑性は、主に 2 つの要因から生まれます。
依存性(Dependencies)
コード間の結びつき。あるコードを変更したら、別のコードも変更が必要になる関係性。
不明瞭性(Obscurity)
意図が読み取れない設計。「なぜこうなっているのか」がわからない状態。
Terraform での場合
これらを Terraform の文脈で考えると、
変更の増大
- モジュールに新しい変数を追加したとき、そのモジュールを呼び出している全ての箇所を修正しないといけない
- 環境ごとに同じような設定をコピペしていると、新しい設定を追加するたびに全環境のファイルを修正しないといけない
認知負荷
- 変数が多すぎて、何を設定すればいいかわからない
- 暗黙の依存関係があって、変更の影響範囲がわかりにくい
未知の未知
- ドキュメントがなく、なぜその設計になっているのかわからない
- data source で他のリソースを参照していて、依存関係が見えない
などが当てはまります。
こうした複雑性にどう向き合えばいいのでしょうか。僕が設計を考えるときに参照している概念を紹介していきます。
設計を考えるときの概念
結合と凝集
ソフトウェア設計の古典的な概念ですが、Terraform のモジュール設計にもそのまま適用できます。
結合強度
モジュール間の依存の強さ。結合が強いほど、一方の変更が他方に影響を与えやすい。
| 結合の種類 | 説明 | Terraform での例 |
|---|---|---|
| 内容結合 | 他モジュールの内部実装に直接依存 | --- |
| 共通結合 | グローバルなデータを共有 | 複数モジュールが同じ data source を参照 |
| 外部結合 | 外部で定義されたフォーマットに依存 | 特定の命名規則に依存 |
| 制御結合 | フラグで他モジュールの動作を制御 |
enable_xxx 変数で条件分岐 |
| スタンプ結合 | 必要以上のデータ構造を渡す | オブジェクト全体を渡して一部だけ使用 |
| データ結合 | 必要なデータのみをやり取り(理想的) | 必要な値だけを変数として渡す |
目指すのは「データ結合」です。必要最小限のデータだけをやり取りすることで、変更の影響範囲を限定したいです。
凝集度
モジュール内の要素がどれだけ関連しているか。
| 凝集の種類 | 説明 | 例 |
|---|---|---|
| 論理的凝集 | 似ているものを集める |
security_groups.tf にすべての SG を詰め込む |
| 機能的凝集 | 一緒に変わるものを集める(理想的) |
api_gateway.tf に API GW 関連リソースをまとめる |
自分がよく意識するのは、機能的凝縮です。
# 論理的凝集:種類で分ける
# security_groups.tf
resource "aws_security_group" "web" { ... }
resource "aws_security_group" "api" { ... }
resource "aws_security_group" "db" { ... }
# 機能的凝集:機能で分ける
# web.tf
resource "aws_security_group" "web" { ... }
resource "aws_instance" "web" { ... }
resource "aws_lb_target_group_attachment" "web" { ... }
Security Group をリソースタイプで集めても、実際の変更は「web の設定を変えたい」「api の設定を変えたい」という機能単位で行われます。それなら最初から機能で分けたほうが、変更時に触るべき範囲が明確になります。
Deep Modules vs Shallow Modules
こちらも『A Philosophy of Software Design』で紹介されている概念です。
Deep Module
シンプルなインターフェース(少ない変数)で、複雑な実装を隠蔽している。利用者は内部の詳細を知らなくても使える。
Shallow Module
複雑なインターフェース(多くの変数)なのに、実装は単純。抽象化のメリットがほとんどない。
Terraform のモジュールで例えると、以下のようになります。
# Shallow Module:変数をそのまま渡しているだけ
module "s3" {
source = "./modules/s3"
bucket_name = var.bucket_name
acl = var.acl
versioning_enabled = var.versioning_enabled
lifecycle_rule = var.lifecycle_rule
server_side_encryption = var.server_side_encryption
# 内部ではこれらをそのまま aws_s3_bucket に渡すだけ
}
# Deep Module:意味のある抽象化
module "static_website_bucket" {
source = "./modules/static-website-bucket"
bucket_name = "my-website"
domain_name = "example.com"
# 内部で CloudFront, ACM, Route53 なども適切に設定
}
モジュール化する際は、「このモジュールを使えば ○○ が実現できる」という明確な価値を提供できるかがポイントです。
設計方針
ここまでの概念を踏まえて、僕が Terraform を書くときには以下のことを意識しています。
できるだけシンプルに
過度な抽象化は避けます。「将来こうなるかもしれないから」という理由で複雑にしても、その将来が来ないまま複雑さだけが残ることが多いからです。
# 過度な抽象化:将来のために汎用的にしすぎ
module "compute" {
source = "./modules/compute"
compute_type = "ec2" # or "ecs" or "lambda"
# ... 大量のオプション
}
# シンプル:今必要なものだけ
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
# ... 必要な設定のみ
}
無理にモジュール化しない
「本当にモジュール化すべきか?」という問いは常に意識しています。以下のような場合は、あえてモジュール化しないことが多いです。
- 1 回しか使わない
- 再利用の予定がないなら、直接書けばいい - 抽象化のメリットがない
— 単なるラッパーになるなら意味がない - 柔軟性が必要
— 各環境で大きく異なる場合、無理に共通化しても辛くなる
YAGNI(You Aren't Gonna Need It)
「いつか必要になるかも」で今複雑にしない。
# YAGNI 違反:使うかわからない機能のフラグを用意
variable "enable_feature_flags" { default = false }
# YAGNI:今必要な機能だけ実装
# 必要になったときに追加する
機能的凝集を意識
「一緒に変更されるもの」をまとめる。「見た目が似ているもの」でまとめない。
パラメータはできるだけ少なく
変数の数はそのままインターフェースの複雑さになります。プロジェクトに適したデフォルト値を設定することで、module を利用する際の負担を減らせます。
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.medium" # 組織の標準サイズ
}
variable "enable_enhanced_monitoring" {
description = "Enable enhanced monitoring"
type = bool
default = true # セキュリティポリシーとして有効化
}
ファイル分割
論理的凝集ではなく機能的凝集を意識
# 論理的凝集:リソースタイプで分割
├── security_groups.tf
├── iam_roles.tf
├── s3_buckets.tf
# 機能的凝集:機能で分割
├── api.tf # API 関連のリソースをまとめる
├── worker.tf # Worker 関連のリソースをまとめる
├── storage.tf # ストレージ関連のリソースをまとめる
モジュール化の判断基準
モジュール化するかどうか迷ったときは、以下の観点で判断しています。
| 観点 | 質問 |
|---|---|
| 再利用性 | 複数の場所で使われるか? |
| 認知負荷の軽減 | 抽象化することで理解しやすくなるか? |
| 変更頻度 | 頻繁に変更されるか?安定しているか? |
| 独立性 | 他のリソースと独立してデプロイできるか? |
結局のところ、「モジュール化で得られるメリットが、複雑性の増加を上回るか」 という問いに帰着します。
参考リソース
設計を考えるときに参考になるドキュメントを挙げておきます。
- Standard Module Structure - モジュールの標準的なファイル構成
- Module Creation - Best Practices - モジュール作成のベストプラクティス
- Google Cloud Terraform Best Practices - Google Cloud 公式のベストプラクティス
おわりに
この記事では、Terraform モジュール設計に対する考え方について書きました。
意識しているポイントは次のとおりです。
- 結合と凝集の概念を意識して、低結合・高凝集を目指す
- 意味のある抽象化をする
- シンプルに保つことを心がけて、必要になるまで複雑にしない
- 迷ったら「この設計で複雑性は増えるか?減るか?」考える
Terraform モジュール設計について、みなさんが普段意識していることも気になります。より良い設計につながる知見があれば、ぜひ教えてください。
Discussion