はじめに
この記事では、Terraform と Terragrunt の構成で実際に経験した失敗と、その背景・振り返りをまとめています。
同じような構成に取り組む方の参考になれば幸いです。
前提(環境)
- 利用している Terraform Provider:AWS / Snowflake
- Terraform version:1.11.4
- Terragrunt version:0.77.17
(※version は当時のものです)
想定していたアーキテクチャ / 理想の構成
-
live/で環境ごとに Terragrunt を分離 -
modules/で共通化 - 環境差分を最小化(minimize)
- CI/CD とローカルで認証方法を切り替える
- できるだけ共通化
当初は「整った Terragrunt 構成」を目指して、極力抽象度を上げた再利用可能な構成を想定していました。
実際に構築していたディレクトリ構成
(構成図はほぼそのままですが、Qiita向けに体裁を整えています)
前提として、事業所が複数存在しており、そのため AWS アカウントや Snowflake アカウントも複数管理している状況でした。
├── generate_provider_aws.hcl # AWS provider 設定
├── generate_provider_snowflake.hcl # Snowflake provider 設定
├── live
│ ├── aws
│ │ ├── iam_user/terragrunt.hcl
│ │ ├── s3_bucket/terragrunt.hcl
│ │ └── ...
│ └── snowflake
│ ├── database/terragrunt.hcl
│ ├── network_policy/terragrunt.hcl
│ ├── schema/terragrunt.hcl
│ ├── warehouse/terragrunt.hcl
│ └── ...
├── modules
│ ├── aws
│ │ ├── iam_user/
│ │ ├── s3_bucket/
│ │ └── ...
│ └── snowflake
│ ├── database/
│ ├── network_policy/
│ ├── schema/
│ ├── warehouse/
│ └── ...
├── params # 事業所ごとのパラメータ
│ ├── 事業所A
│ │ ├── aws/*.jsonl
│ │ └── snowflake/*.jsonl
│ ├── 事業所B
│ └── ...
├── params_loader.hcl # params 配下を terragrunt.hcl で読むための設定
├── provider_version.yaml
└── tfstate.hcl # tfstate 関連設定
失敗したポイント①:DRY を強く意識しすぎた結果、module / state の粒度を誤った
今回の構成では、「同じ記述を繰り返さない」ことを強く意識し、
再利用可能な module を作ることを優先しました。
その結果、リソース単位で module を分割し、state も細かく切る設計になりましたが、
結果として 強く結びついたリソース同士まで分断されてしまう構成になっていました。
それにより、次のような問題が発生しました。
- ファイル数が増えて構成が煩雑化
- Terragrunt 側の
dependencyが増え、依存関係が複雑化 - リソース間の関係性がコードから読み取りづらくなった
依存関係が複雑化したことによる問題(起きたこと)
特に困ったのは依存関係です。まとめると以下のような “問題” が起きました。
-
本来は Terraform の依存グラフだけで完結できた依存が、state を跨いだため表現できなくなった
- 例:IAM ポリシーとアタッチメントのように「強く結びついたリソース」が別 state に分かれてしまい、Terraform 内の参照だけではつながらない状態になった
-
Terragrunt の
dependencyを書いて “順序” だけは作ったが、値の流れ(outputs→inputs)が設計されておらず、依存が実質解決できていなかった- 依存しているはずなのに、依存先の値を inputs に取り込んでいないため、参照として成立していないケースが出た
-
apply 前に outputs を参照してエラーになりがちで、
mock_outputsで誤魔化す場面が増えた- 依存先がまだ apply 済みでないと outputs が存在せずエラー
-
mock_outputsで回避はできるが、その場しのぎになりやすかった
結果として「state を細かく割ったせいで Terraform 単体では依存を表現できないのに、Terragrunt 側でも依存(値の流れ)を設計できていない」状態になっていました。
背景には、「再利用性」をどこまでの単位で考えるべきかを誤ったことがありました。
module については DRY / 再利用性を意識して最小単位に分ける判断は妥当でしたが、
その考え方を state やディレクトリといったデプロイ単位にまで持ち込んでしまったことで、
依存関係の表現や apply の単位が崩れていったと考えています。
振り返り①:どうすれば良かったか(依存関係の設計)
自分の中での結論はこれです。
-
強く結びついたリソースは同じ state にまとめて、Terraform の参照だけで完結させる
- 例:ポリシー & アタッチメント、ロール & ポリシー、DB & スキーマなど
-
どうしても state を跨ぐなら、
dependency+outputs → inputsで “値の流れ” を明示する- 「順序」ではなく「参照関係」を設計する意識が必要だった
-
mock_outputsに逃げない- 使うなら“移行期間”のように目的を限定し、恒常運用にしない
DRY 自体が悪かったわけではなく、
「どの単位で共通化すべきか」「どこまで分離すべきか」の判断を誤っていました。
インフラコードでは、DRYよりもリソース同士の関係性(凝集度)を保つことのほうが重要になる場面が多いと感じました。
失敗したポイント②:1 行でリソースを見たいがために jsonl を採用した
リソース数が多くなると YAML は行数が増え見づらいと感じ、1 行で視認できる jsonl を採用しました。
しかし結果として次の問題に繋がりました。
- 値の文字数が多いリソースは横に長くなり、逆に読みづらい
- jsonl はコメントが書けないため、説明や区分を明記できない
- YAML と比べて差分(diff)が読みづらい
- 構造を表現しづらく、パラメータの抜け漏れやミスに気づきにくい
before / after(イメージ)
before(jsonl:1 行で済むが、長くなる&コメント不可)
{"name":"example_policy","description":"example","statements":[{"effect":"Allow","actions":["s3:GetObject","s3:PutObject"],"resources":["arn:aws:s3:::example-bucket/*"]}]}
after(yaml:縦に読めて、コメントも書ける)
# S3 バケットへの読み書き権限(事業所A向け)
name: example_policy
description: example
statements:
- effect: Allow
actions:
- s3:GetObject
- s3:PutObject
resources:
- arn:aws:s3:::example-bucket/*
振り返り②:どうすれば良かったか(パラメータ表現)
jsonl は一覧性こそ高いものの、構造を持った設定データを表現するには不向きでした。
一方 YAML は、
- ネスト構造を自然に表現できる
- パラメータの抜け漏れやミスに気づきやすい
- コメントで意図を補足できる
- diff が読みやすく、レビューしやすい
といった点で、運用時の可読性・保守性に優れていました。
結果として、一覧性よりも「構造の分かりやすさ」を優先すべきだったと感じています。
まとめ
- インフラコードは「状態の記述」であり、抽象化しすぎると逆効果になり得る
- リソースごとの細かすぎるモジュール化 / state 分割は、一見きれいでも運用面で破綻しやすい
- DRY を意識すること自体は重要だが、適用範囲と粒度の判断がより重要
- 強く結びついたリソースは同じ state にまとめ、跨ぐなら outputs→inputs まで含めて依存を設計する
- パラメータは一覧性よりも 運用性・可読性(コメント/差分/マルチライン) を基準に判断すべき
参考
Terraform / Terragrunt の設計を振り返るうえで、以下の発表資料がとても参考になりました。