🐈

なぜTerraformがつらくなるのか?モジュール設計とその解決策

に公開

※本記事は Terraform を AWS のインフラ構築に利用する前提で書いています。
※ここで挙げる内容は、あくまで筆者の経験ベースの指針であり、唯一の正解というわけではありません。


Terraform は「初期構築のときに作って、その後必要なときだけ触るもの」になりがちです。
しかし時間が経つにつれて、次のような状況に直面します。

  • どのファイルに何が書かれているか分からない
  • 変更の影響範囲が読めず terraform apply が怖い
  • 消したい機能に関連する module が分離できず、安全に削除できない

そして、動いている環境がある以上、改善はどうしても後回しになり、最終的には Terraform 自体が負債化してしまいます。

この記事では、その中でも 「module の設計」 に絞って、長期運用でも破綻しにくい考え方をまとめます。


結論:module 設計で特に意識していること

Terraform を長期運用する上で、module 設計で特に意識しているのは次のとおりです。

  • module は機能単位で作る
    例: EC2 + IAM + SecurityGroup + APIGW → WebServer
    → 機能ごとに削除・交換・複製・移植がしやすくなる。

  • AWS Service 単位では module にしない
    → IAM だけ、S3 だけ、EC2 だけの module だと「単体では意味を持たない構造」になりがち。

  • module は可能な範囲で小さく保つ
    → 可読性・テスト性が上がり、変更時の影響範囲が読みやすくなる。

このあとで、なぜこの3つが効いてくるのかを具体例とあわせて見ていきます。


module 設計の考え方

一般的な「モジュール」の定義は、だいたい次のようなものです。

  • ひとまとまりの機能を持った部品
  • インターフェース(入力/出力)が明確
  • 交換・削除・追加が容易

Terraform の module も、できるだけこのイメージをそのまま当てはめて設計したいところです。

よくあるアンチパターン(Service単位での分割)

modules/
 ├─ iam/
 ├─ s3/
 ├─ ec2/

一見きれいに分類されているように見えますが、実際には次のような問題が出やすいです。

  • module 単体では「何をしているのか」意味を持たない
  • root module 側に依存関係が散らばりやすい
  • module 同士の依存が循環しやすく、構造が複雑になる

結果として、「Web サーバ機能を消したい」「API 基盤だけ別環境にコピーしたい」といったときに、
どの module をどう触ればいいのか分かりづらくなります。


望ましいパターン(Feature単位での分割)

modules/
 ├─ ec2_web_server/
 │   ├─ ec2.tf
 │   ├─ iam.tf
 │   ├─ security_group.tf
 │   ├─ apigw_route.tf

このように「機能(feature)」単位で module を切ると、次のようなメリットがあります。

  • module 単体で「何のための塊か」が分かる
  • module を消せば、その機能に紐づくリソースがまとめて消える
  • module を他のシステムに持っていく、といった移植がしやすい

つまり、機能単位で module を切る = 変更の単位・削除の単位をそのままコードに落とし込む ことになります。


実際に起きた問題例(設計まわり)

初期構築では、module の切り方が原因で次のような問題がありました。

  • IAM module が巨大化し用途が不明

    • どの IAM がどの機能のためのものか追いづらい
    • 機能を削除するときに、どの IAM を消してよいか判断が難しい
  • Required variable が 10 個以上必要な module

    • 外から大量の値を渡さないと動かない
    • module 側にほとんど主体性がなく、「ただの入れ物」になっている
  • locals.tf が肥大化し意図が隠れる

    • どの値がどこから来ているか追いづらい
    • 環境ごとの差分や命名ロジックが locals の中に埋もれる

これらはすべて、

  • 機能単位ではなく Service 単位で module を切っていた
  • module の責務が広すぎた

ことの副作用でした。


解決方針

改善を進める中で、設計面では次のような方針をとりました。

1. IAM は「機能 module」の中で作る

IAM を独立した module にまとめるのではなく、各機能 module の中に含めるようにしました。

例:

modules/
 ├─ ec2_web_server/
 │   ├─ ec2.tf
 │   ├─ iam.tf        # ec2_web_server 用の IAM
 │   ├─ security_group.tf
 │   ├─ apigw_route.tf

こうすることで、

  • 「この IAM はこの機能のためのもの」とすぐに分かる
  • 機能を削除すれば、その機能専用の IAM も一緒に削除できる

という状態に近づきます。


2. Required variable を減らす方向で設計する

外部でしか決められない値だけを variable とし、それ以外は module 内部で組み立てるようにしました。

  • 外部で決めるべきものの例

    • env(dev / stg / prod など)
    • 既存 VPC ID や Subnet ID など、インフラ全体の都合に依存するもの
  • module 内で決めてしまうものの例

    • naming rule("${var.env}-${local.service_name}-web" など)
    • 共通の Tag(Name / System / Service など)

feature 単位で module を切っていくと、その module の中だけで完結する情報が増え、
外から与える値(Required variable)は自然と少なくなっていきます。


3. locals を小さく維持する

locals にロジックを詰め込みすぎると、次のような問題が出ます。

  • 設計の意図が locals の中に隠れてしまう
  • 変更時に「この値どこから来てるんだっけ?」となりやすい

そこで、

  • 「可読性のための名前をつける」程度にとどめる
  • 設計の依存関係はなるべく variable / output として明示する

という方針に切り替えました。

module の責務を小さくしていくと、そもそも locals に頼らなくてもよい場面が増えていきます。


まとめ

いろいろ書きましたが、最終的には次の一文に集約されます。

module を feature 単位で設計すると、その他の設計ルールが自然とついてくる。

この前提に立つと、

  • Required variable は「結果として」減っていく
  • locals に頼らなくてもシンプルに書ける
  • 「この機能を消したい/移植したい」がコード構造と素直に対応する

といった状態に近づきます。

Terraform は一度書いて終わりではなく、
システムと共に更新され続けるコードです。

その入口となる module の切り方から、少しずつ見直していくと、
半年後・一年後の自分やチームがだいぶ楽になるはずです。

Discussion