はじめに
Terraform のディレクトリ構成にはさまざまなパターンがあり、どれを採用するかは要件によって異なります。
本記事では、わたしが実務で使い分けている 5 つのディレクトリ構成パターンを紹介します。
パターンごとに結合度を落としていき、最終的には下記の形になることを目指します。
前提
各パターンを相対的に比較するため、以下の 6 つの評価基準を設定します。
-
環境の分離
- 特定の環境への変更が他の環境に影響を与えないこと
-
DRY 原則
- 同じコードを繰り返し書かないこと
-
state の分割
- プロビジョニングの高速化のため、state ファイルが肥大化しないこと
-
モジュールと環境の独立性
- 明示的にモジュールを更新しない限り、環境へのデプロイが常に冪等であること
-
環境複製
- 既存のワークロードと同一の新しい環境を容易に作成できること
-
学習コスト
- 新しいメンバーが参加した際に、容易に理解できること
これらの多くは結合度と密接に関係しています。
そして、各パターンで構築するインフラ構成は以下のシンプルな構成を想定します。
リソースはCloudFrontとS3(およびそれに付随するリソース)で、環境は本番環境(production)とステージング環境(staging)の 2 つを作成します。
実践
紹介するディレクトリ構成パターンは以下の 5 つです。
- 単一 state に全環境を含むパターン
- 環境ごとに state を分割するパターン
- 環境ごとに state を分割し、モジュールをバージョニングするパターン
- Terragrunt を使用した環境・モジュール分離パターン
- Terragrunt Stack を使用したパターン
すべてのサンプルコードは以下のリポジトリで確認できます。
1. 単一 state に全環境を含むパターン
最もシンプルな構成であり、Terraform を初めて使用する際の選択肢となることが多いパターンです。
すべての環境とリソースを単一の Terraform state で管理します。
以下のコードは、staging と production を単一の Terraform state で管理する例です。
便宜上 1 つのファイルに記述していますが、ファイル分割やモジュール化を行っても、同じ state で管理することに変わりはありません。
すべての環境のリソースが単一の state で管理されることは一見便利に思えますが、最大の問題点は production と staging が独立していない ことです。staging のリソースを変更するつもりが、誤って production のリソースを変更してしまう可能性があります。
plan で差分を確認すれば未然に防げるように思えますが、管理するリソース数が増加するにつれて、見落としのリスクも高まります。
ただし、小規模な開発であればこの構成で問題ない場合もあります。
私自身も、シンプルで依存の少ないLambdaをデプロイする際はこの構成を採用することがあります。
これは下記のような評価になるのではないでしょうか。
| 環境の分離 | DRY 原則 | state の分割 | 独立性 | 環境複製 | 学習コスト |
|---|---|---|---|---|---|
| × | × | × | × | △ | ○ |
次の記事でこの構成を採用した際に発生しうる障害について詳しく述べられています。
USE SEPARATE STATE FILES. FOR EVERY ENVIRONMENT.
まさにそのとおりで、次のパターンでは環境ごとに state を分割する方針を紹介します。
2. 環境ごとに state を分割するパターン
この構成では、それぞれの環境が独自の state を持ち、相互に干渉することはありません。同時にモジュールも分離し、CloudFront と S3 の 2 つのモジュールを作成しています。
これにより、意図しない環境のリソースを誤って変更・削除してしまうリスクが減少します。また、各環境の state が独立しているため、個別に apply が可能となり、プロビジョニングや変更適用の時間が短縮されます。
.
└── environments
├── production
│ └── main.tf
└── staging
└── main.tf
コードは下記のようになります:
前のパターンと比較すると、各リソースをモジュールに集約することで、コード全体の量が削減され、見通しが良くなっています。
多くのプロジェクトでこの方針が採用されており、私自身も多くの案件でこの方法を使用しています。
評価は以下のとおりです。
| 環境の分離 | DRY 原則 | state の分割 | 独立性 | 環境複製 | 学習コスト |
|---|---|---|---|---|---|
| ○ | △ | △ | × | ○ | ○ |
以下所感です:
① state の分割は環境レベルでは実現していますが、モジュール単位では行われていません。S3 と CloudFront が単一の state で管理されています。そのため管理するモジュールが増えるにつれ、state ファイルが肥大化します 。さらに state を分割する(ディレクトリを分割し、backend を個別に定義する)ことも可能ですが、設定の重複が発生し、管理が煩雑になります。
② 次に、異なる環境間で依存関係を持たせることが難しい点です。 例えば、検証用の環境で同じリソースを共有したい場合はよくあるかなと思います。terraform_remote_stateを活用することで実現は可能ですが、依存関係が見えにくく、依存先が未作成の場合、apply に失敗するため、開発速度が低下するおそれがあります。
③ 最も重要な問題は、環境がモジュールを相対パスで参照している点です。作業中のモジュールを意図しない環境に apply してしまうリスクがあります。例えば、モジュールを更新してテストのために staging に apply を実行した後、同じ状態で production に apply を実行すると、テスト中のモジュールが production に混入する恐れがあります。
前2つの問題は後回しにして、まずは最後の問題を解決しましょう。
3. 環境ごとに state を分割し、モジュールをバージョニングするパターン
前と基本構成は同じですが、モジュールの source にバージョニングを追加しています。
例えば下図のように、production 環境では v1.1.0 使用し、staging 環境では 新しい v1.2.0 を使用することが可能です。
この手法を活用することで、モジュールのバージョンを明示的に指定できるため、変更が意図しない環境に影響を与えるリスクを軽減できます。
ただし、staging への反映が完了しているにもかかわらず、production への反映が遅れる状況が発生する可能性があります。
この問題は自動化で対処する必要がありますが、テスト中のモジュールが production に混入するリスクと比較すれば、はるかに安全です。
評価は以下のとおりです。
| 環境の分離 | DRY 原則 | state の分割 | 独立性 | 環境複製 | 学習コスト |
|---|---|---|---|---|---|
| ○ | △ | △ | ○ | ○ | ○ |
ここまでで多くの問題を解決できましたが、パターン 2 で示した以下の課題が残っています。
① さらなる state の分割
② 環境・モジュール間の依存関係
次のパターンでこれらの課題に対処します。
4. Terragrunt を使用した環境・モジュール分離パターン
このパターンでは、Terragrunt を活用します。
Terragrunt では環境・モジュールごとに state を自動的に分割するため、自然と state の肥大化を抑制できます。さらに、dependency機能を使用することで、モジュール間の依存関係を明確に定義できます。
コード例は以下のとおりです。
注目すべき点は、provider の定義を隠蔽できることです。
これは Terragrunt の generate 機能によるもので、root.hclに以下のように記述することで、自動的に backend の設定が適用されます。
つまり、以下のような構成で Terragrunt をapplyすると、対応する state ファイル が自動的に同じツリー状に作成されます。
$ tree
# .
# ├── environments
# │ └── staging
# │ ├── env.hcl
# │ ├── cloud-front
# │ │ └── terragrunt.hcl
# │ └── s3
# │ └── terragrunt.hcl
# └── root.hcl
$ terragrunt apply --all
$ s3-tree <state-bucket-name>
# <state-bucket-name>
# └── environments
# └── staging
# ├── cloud-front
# │ └── terraform.tfstate
# └── s3
# └── terraform.tfstate
これで「① さらなる state の分割」という課題を解決できました。CloudFront と S3 を個別に apply でき、state ファイルのサイズも最小限に抑えられます。
続いて、異なる state 間の依存関係については、dependencyを活用することで以下のように実現できます。
また、mock_outputsを活用することで、依存対象となるモジュールが未実装でも開発を進められます。
https://terragrunt.gruntwork.io/docs/features/stacks/#unapplied-dependency-and-mock-outputs
その他にも、backend となる S3 の自動作成、--sourceオプションによる一時的なモジュールソースの切り替えなど、Terragrunt には多くの便利な機能があります。
評価は以下のとおりです。
| 環境の分離 | DRY 原則 | state の分割 | 独立性 | 環境複製 | 学習コスト |
|---|---|---|---|---|---|
| ○ | ○ | ○ | ○ | ○ | △ |
環境の複製は environments 以下をコピーすることで可能ですが、やや手間がかかります。
最後のパターンでこの課題を解決します。
5. Terragrunt Stack を使用したパターン
いよいよ最後のパターンです。
前のパターンの課題は環境の複製でした。ディレクトリ構造をまとめてコピーする必要があることがネックであり、作成する環境が数十となってくると非常に面倒です。
ここで発想を転換します。
これまでは「環境ごとにインフラ定義を書く」アプローチでしたが、Stackでは「インフラのブループリント(設計図)を定義し、環境はそれを実体化するだけ」というアプローチを取ります。
この構成は以下のとおりです。
用語定義
- Unit - 単一のデプロイ単位。直接 Terraform モジュールを参照し、最小のインフラコンポーネントを表します
-
Stack - 複数の Unit(または他の Stack)を組み合わせて構成されるインフラのサブセット(例:
CloudFront+S3)
事前に Stack として、共通で使用するモジュール群(設計書)を定義し、各環境は、その Stack に対して実行時の変数を注入することで環境をプロビジョニングします。モジュールの組み合わせを Stack 化することで、よく使うパターン(例: CloudFront + S3 や SQS + Lambda)を再利用可能にします。
つまり 設計の詳細をStackに閉じ込め、環境は値の注入を行うだけで良い ということとなります。
この分離により得られる最大の利点は「設計の一貫性が保証される」ことです。
100個の環境があっても、すべて同じStackから生成されるため、「stagingで動いたのにproductionで動かない」という差異が原理的に発生しません。
従来は環境ごとにコピペして管理していた200個のファイル(100環境×2モジュール)が、1個のStackファイルと100個の環境用ファイルだけで済むようになります。
実装例は下記です:
Stack の定義は以下のようになります。
ここでは前述の組み合わせを定義します。
環境の複製も単一の terragrunt.stack.hcl ファイルのコピーのみで完了します。
非常にシンプルになりました。
評価は以下のとおりです。
| 環境の分離 | DRY 原則 | state の分割 | 独立性 | 環境複製 | 学習コスト |
|---|---|---|---|---|---|
| ○ | ◎ | ○ | ○ | ◎ | × |
初期の学習コストが高いことがデメリットといえるでしょう。
まとめ
本記事では、5 つの Terraform ディレクトリ構成パターンを紹介しました。
それぞれのパターンに長所と短所があり、決して密結合=悪ではないため、プロジェクトの規模や要件に応じて適切に選択することが重要かなと思います。
判断基準ですが、チーム規模が小さく、インフラ構成がシンプルであればパターン 1 や 2 を選択し、チーム規模が大きく、複雑なインフラ構成を管理する場合はパターン 4 や 5 を選択するのが良いでしょう。
AIによる比較
Co-Authored-By: Claude noreply@anthropic.com
5 つのパターンの総合比較表
| パターン | 環境の分離 | DRY 原則 | state の分割 | 独立性 | 環境複製 | 学習コスト | 推奨プロジェクト規模 |
|---|---|---|---|---|---|---|---|
| 1. 単一 state | × | × | × | × | △ | ○ | 小規模(1-2 名、リソース 10 個未満) |
| 2. 環境ごとに state 分割 | ○ | △ | △ | × | ○ | ○ | 中規模(3-5 名、リソース 10-30 個) |
| 3. モジュールバージョニング | ○ | △ | △ | ○ | ○ | ○ | 中規模(5-10 名、リソース 30-50 個) |
| 4. Terragrunt 環境・モジュール分離 | ○ | ○ | ○ | ○ | ○ | △ | 大規模(10 名以上、リソース 50 個以上) |
| 5. Terragrunt Stack | ○ | ◎ | ○ | ○ | ◎ | × | エンタープライズ(複数チーム、マルチテナント) |
評価基準の凡例
- ◎:非常に優れている
- ○:良好
- △:やや課題あり
- ×:課題が多い
各パターンの特徴まとめ
| パターン | 主な利点 | 主な課題 | 適用場面 |
|---|---|---|---|
| 1. 単一 state | シンプルで理解しやすい | 環境間の影響リスクが高い | プロトタイプ、小規模 Lambda |
| 2. 環境ごとに state 分割 | 環境の独立性を確保 | モジュールの意図しない混入リスク | 単一プロダクトの本番運用開始時 |
| 3. モジュールバージョニング | 変更管理が明確 | バージョン管理の運用負荷 | 厳密な変更管理が必要な環境 |
| 4. Terragrunt 環境・モジュール分離 | 完全な分離と依存管理 | Terragrunt の学習が必要 | 複数環境・複数モジュールの管理 |
| 5. Terragrunt Stack | 環境複製が極めて容易 | 初期学習コストが最も高い | 同一パターンの大量展開が必要な環境 |
わたし 1 人の時はパターン1です。なぜなら楽なので。
ここで紹介した全ての例は以下のリポジトリで確認できます。
ここまでご高覧いただきありがとうございました!
