TerragruntでTerraformをDRY化する設計入門|includeとdependencyで環境差分を管理する方法

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Terraform > TerragruntでTerraformをDRY化する設計入門|includeとdependencyで環境差分を管理する方法
「Terraformで環境を3つ(dev・stg・prod)に分けたら、backend設定が3ファイルに重複してしまった……」
「moduleを使っても、呼び出し側のroot moduleが環境ごとにほぼ同じ内容で増えていく……」

Terraformは設計の自由度が高い反面、backendブロックにはTerraform変数を使えないという制約があります。環境ごとにbackend設定を書き分けると、dev・stg・prodで同じS3バケット名とDynamoDBテーブル名の指定が3ファイルに散らばり、変更のたびに全ファイルを手動で更新しなければなりません。

この記事では、そのギャップを埋めるツールとして現場で広く使われているTerragruntの設計パターンを解説します。includeブロックで共通設定を一元管理し、dependencyブロックでモジュール間データを受け渡す方法を、実際のディレクトリ構成とHCLコードで説明します。

この記事のポイント

・TerraformのbackendにはHCL変数が使えないため、環境差分が重複しやすい
・Terragruntのincludeブロックで親設定を継承し、backend設定を1箇所に集約できる
・dependencyブロックでVPC→EC2のようなモジュール間outputを安全に参照できる
・run-allコマンドで依存グラフを自動解決しながら複数モジュールを一括実行できる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜTerraformだけではDRYな設計が難しいのか

Terraformでマルチ環境を管理する場合、まず思い浮かぶのがworkspaceによる切り替えディレクトリ分割かという判断です。workspaceはtfstateを分けてくれますが、backend設定は共用になるためリソース設定の分離が甘くなります。ディレクトリ分割(environments/dev・environments/stg・environments/prod)はより堅牢ですが、今度は重複の問題が浮上します。

Terraformのbackendブロックは、HCL変数(var.*やlocal.*)を使うことができません。次のような書き方はエラーになります。

# NG: backendブロックで変数は使えない terraform { backend "s3" { bucket = var.tfstate_bucket # エラー key = "${var.env}/terraform.tfstate" # エラー } }

そのため、環境ごとにbackend設定を書き分けたbackend.tfを置くか、-backend-configフラグで外から渡すかを選ぶことになります。前者は重複、後者はコマンド実行時の引数管理が複雑になります。

さらに、moduleを呼び出すroot module(main.tf)も、環境ごとに同じmoduleブロックをほぼ同じ内容で書くことになりがちです。変数ファイル(-var-file)で差分を吸収できる部分もありますが、backend設定だけはどうにもなりません。この構造的な制約を解決するのがTerragruntの役割です。

Terragruntの基本構造——terragrunt.hclとディレクトリレイアウト

Terragruntは、Terraformのラッパーツールです。terragrunt.hclという設定ファイルを読み込み、backend設定の動的生成やモジュール間の依存管理を肩代わりします。インストールはバイナリを1つ配置するだけです。

# Terragruntのインストール(v0.67.x) curl -L https://github.com/gruntwork-io/terragrunt/releases/download/v0.67.4/terragrunt_linux_amd64 \ -o /usr/local/bin/terragrunt chmod +x /usr/local/bin/terragrunt terragrunt --version # terragrunt version v0.67.4

Terragruntを使ったリポジトリの典型的なディレクトリ構成は次のとおりです。

infra/ ├── terragrunt.hcl # ルートの共通設定(backend・provider) ├── modules/ # 再利用可能なTerraformモジュール │ ├── vpc/ │ │ ├── main.tf │ │ ├── variables.tf │ │ └── outputs.tf │ └── ec2/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf └── environments/ ├── dev/ │ ├── vpc/ │ │ └── terragrunt.hcl # 環境ごとのterragrunt設定 │ └── ec2/ │ └── terragrunt.hcl ├── stg/ │ ├── vpc/ │ │ └── terragrunt.hcl │ └── ec2/ │ └── terragrunt.hcl └── prod/ ├── vpc/ │ └── terragrunt.hcl └── ec2/ └── terragrunt.hcl

このレイアウトのポイントは、Terraformのmodule定義(modules/配下)とTerragruntの実行設定(environments/配下)を分離している点です。moduleのコードは1箇所に置き、Terragruntが各環境から参照します。

includeブロックで共通設定を一元管理する

ルートのterragrunt.hclに、全環境共通のbackend設定とprovider設定をまとめます。

# infra/terragrunt.hcl(ルート共通設定) locals { # ディレクトリ構造からaws_regionを自動検出する場合の例 aws_region = "ap-northeast-1" } generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <

path_relative_to_include()は、ルートのterragrunt.hclからみた相対パスを返す組み込み関数です。environments/dev/vpcディレクトリから実行した場合、tfstateのkeyはenvironments/dev/vpc/terraform.tfstateになります。環境名とモジュール名を含む一意のパスが自動で決まるのが大きな利点です。

次に、環境側のterragrunt.hclでルート設定を継承します。

# infra/environments/dev/vpc/terragrunt.hcl include "root" { path = find_in_parent_folders() } terraform { source = "../../../../modules/vpc" } inputs = { vpc_cidr = "10.0.0.0/16" environment = "dev" }

find_in_parent_folders()は、現在のディレクトリから親ディレクトリをさかのぼってterragrunt.hclを探す関数です。ルートの設定が自動で見つかります。include "root"ブロックを書くだけで、backend設定とprovider設定の両方を継承できます。

stg環境の設定は、変数の値を変えるだけです。

# infra/environments/stg/vpc/terragrunt.hcl include "root" { path = find_in_parent_folders() } terraform { source = "../../../../modules/vpc" } inputs = { vpc_cidr = "10.1.0.0/16" environment = "stg" }

backend設定は1箇所(ルートのterragrunt.hcl)にだけ書かれており、S3バケット名を変更するときはルートファイルを1か所だけ変更すれば全環境に反映されます
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、TerraformとTerragruntの実践設計から本番運用まで、ハンズオン形式で習得できるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

dependencyブロックでモジュール間データを受け渡す

実際のインフラ設計では、VPCを作ってからEC2を作る、というようにモジュール間に依存関係があります。TerraformのみではルートmoduleにVPCとEC2を同居させるか、terraform_remote_stateを使う方法しかありませんでした。Terragruntのdependencyブロックはこれをすっきり解決します。

# infra/environments/dev/ec2/terragrunt.hcl include "root" { path = find_in_parent_folders() } terraform { source = "../../../../modules/ec2" } dependency "vpc" { config_path = "../vpc" # terragrunt plan時(vpc未applyの状態)のフォールバック値 mock_outputs = { vpc_id = "vpc-00000000" private_subnet_ids = ["subnet-00000000"] } mock_outputs_allowed_terraform_commands = ["validate", "plan"] } inputs = { vpc_id = dependency.vpc.outputs.vpc_id private_subnet_ids = dependency.vpc.outputs.private_subnet_ids instance_type = "t3.micro" environment = "dev" }

dependency.vpc.outputs.*でVPCモジュールのoutputを直接参照できます。mock_outputsは、VPCがまだapplyされていない状態でEC2のplanを実行したいときのダミー値です。CI/CDのplan-onlyフェーズで依存モジュールのtfstateがない場合に必要になります。

依存関係の方向はconfig_pathに記述した相対パスで決まります。Terragruntはrun-allコマンド実行時にこの依存グラフを解析し、VPCが完了してからEC2を実行する順序を自動で決定します。

1. mock_outputsが必要なケース

mock_outputsを使う主なケースは次の2つです。

CI/CD のPlanフェーズ: PRレビュー時にEC2のplanだけを確認したい。VPCはすでに別ブランチでapply済みだが、CI環境からはtfstateを参照できない
新規環境の初回plan: 全モジュールを初めてapplyするとき、VPCのtfstateが存在しない状態でEC2のplanを先に確認したい

# planとvalidateのみmock_outputsを許可する設定 mock_outputs_allowed_terraform_commands = ["validate", "plan"] # applyでmock_outputsを使うとダミーIDがリソースに渡るため危険。applyは除外する

inputsとlocalsで環境差分を表現する

環境ごとの差分(環境名・CIDRレンジ・インスタンスタイプ等)はterragrunt.hclのinputsブロックに記述します。さらに、共通処理をlocalsにまとめることで、ファイル内の重複も削減できます。

# infra/environments/dev/ec2/terragrunt.hcl include "root" { path = find_in_parent_folders() } locals { # 環境共通変数をまとめたYAMLファイルを読み込む(任意) env_vars = yamldecode(file(find_in_parent_folders("env_vars.yaml"))) env = local.env_vars.environment # "dev" region = local.env_vars.aws_region # "ap-northeast-1" } terraform { source = "../../../../modules/ec2" } inputs = { instance_type = "t3.micro" # dev環境は小さいインスタンス environment = local.env region = local.region }

find_in_parent_folders("env_vars.yaml")を使うと、環境ディレクトリに置いたenv_vars.yamlを自動検索して読み込めます。

# infra/environments/dev/env_vars.yaml environment: dev aws_region: ap-northeast-1

stg・prod環境のenv_vars.yamlはenvironment: stgに変えるだけで、terragrunt.hcl本体は同一のコードで動きます。

run-all コマンドで複数モジュールを一括実行する

terragrunt run-allは、ディレクトリツリー以下の全terrrgrunt.hclを検出し、依存関係に沿った順序でterraform plan / apply / destroyを実行します。

# dev環境全体をplanする cd infra/environments/dev terragrunt run-all plan # 実行ログ例(抜粋) [terragrunt] INFO Found 2 modules in total [terragrunt] INFO Module environments/dev/vpc has no dependencies [terragrunt] INFO Module environments/dev/ec2 depends on environments/dev/vpc [terragrunt] INFO Running 'terraform plan' in environments/dev/vpc ... # ... vpc planの出力 ... [terragrunt] INFO Running 'terraform plan' in environments/dev/ec2 ... # ... ec2 planの出力 ...

run-all apply非対話環境(CI/CD)では確認プロンプトが出ます。自動化するときは--terragrunt-non-interactiveフラグか環境変数TG_NON_INTERACTIVE=trueを設定します。

# CI/CDでの一括apply(確認プロンプトをスキップ) TG_NON_INTERACTIVE=true terragrunt run-all apply # 特定ディレクトリだけをapply(通常のterraformと同様) cd infra/environments/dev/vpc terragrunt apply

よくあるエラーと対処手順

1. "Error finding parent terragrunt files above the current directory"

find_in_parent_folders()がルートのterragrunt.hclを見つけられていません。実行ディレクトリとルートのterragrunt.hclの配置場所を確認します。

# 確認コマンド find . -name "terragrunt.hcl" | sort # 想定出力(ルートにterragrunt.hclがあることを確認) ./environments/dev/ec2/terragrunt.hcl ./environments/dev/vpc/terragrunt.hcl ./terragrunt.hcl ← これが見つからない場合は配置ミス

2. "dependency cycle detected"

dependencyブロックで循環参照が発生しています。モジュールAがBを参照し、モジュールBがAを参照する構成はエラーになります。dependencyブロックの方向を図に起こし、矢印が一方向になるように再設計します。

3. applyでmock_outputsの値がリソースに使われてしまう

mock_outputs_allowed_terraform_commands"apply"が含まれているのが原因です。applyはリストから除外してください。

# 修正後の設定 mock_outputs_allowed_terraform_commands = ["validate", "plan"] # "apply" は絶対に含めない

4. S3バックエンドに接続できない

AWSクレデンシャルが設定されていないか、IAMポリシーでS3バケットへのアクセスが拒否されています。

# クレデンシャル確認 aws sts get-caller-identity # { # "UserId": "AIDA...", # "Account": "123456789012", # "Arn": "arn:aws:iam::123456789012:user/terraform-ci" # } # S3バケットへのアクセス確認 aws s3 ls s3://my-tfstate-bucket

Terragruntが本当に必要なプロジェクトの判断基準

Terragruntは便利なツールですが、すべてのプロジェクトに導入すべきではありません。以下の基準で判断します。

環境数 × モジュール数が多い: dev/stg/prodの3環境 × 5モジュール以上であれば導入コストに見合う効果が得られます
backendの重複が痛い: 環境ごとにbackend.tfを手動で管理していて、更新忘れが発生している場合は導入の明確なメリットがあります
モジュール間の依存関係がある: VPC→EC2→ALBのような依存チェーンをrun-allで自動管理できます

逆に、環境が1つだけ、またはシンプルなリソース構成の場合はTerraformのworkspaceとvar-fileで十分です。ツールを追加するとそのツール自体の学習コストと運用コストが増えます。チームメンバーのTerraform習熟度を考慮した上で導入を判断してください。

本記事のまとめ

機能 TerragruntのHCL記法 解決する課題
共通設定の継承 include "root" { path = find_in_parent_folders() } backend・provider設定の重複排除
dynamic backend key path_relative_to_include() 環境ごとに一意のtfstate pathを自動生成
モジュール間参照 dependency { config_path = "../vpc" } outputを別モジュールから安全に参照
CI/CDフォールバック mock_outputs = { ... } 依存モジュール未applyでもplanを実行可能
一括実行 terragrunt run-all plan/apply 依存グラフを自動解決して複数モジュールを順次実行
Terragruntは「Terraformの何を解決するか」を理解してから導入するのが成功の鍵です。backendの動的化とモジュール間依存の管理という2点に絞って最初は使い始め、慣れてきたらlocals・YAMLファイル読み込み・run-allを組み合わせた本格的な設計へと発展させていきましょう。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、TerraformとTerragruntを使った実践的なIaC設計から本番運用まで、ハンズオン形式で習得できるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。