🍆

Terragrunt入門:公式Quickstartを実践してみた

に公開

Terragruntの公式ドキュメントにあるQuickstartチュートリアルを実際に手を動かしながら進めてみました。この記事では、Terragruntの基本的な概念から依存関係管理まで、段階的に学んだ内容をまとめます。

Terragruntとは

TerragruntはTerraform/OpenTofuのラッパーツールで、以下のような課題を解決します:

  • DRY(Don't Repeat Yourself): 複数環境での設定の重複を排除
  • 依存関係管理: モジュール間の依存関係を自動的に解決
  • 一括操作: 複数のTerraformモジュールを一度に操作

前提条件

  • Terraform または OpenTofu がインストール済み
  • Terragrunt がインストール済み
  • Unix系OS(macOS, Linux等)

1. 最初のTerragruntプロジェクト

基本構成

まず、fooディレクトリにシンプルなTerraformファイルを作成します。

foo/
├── main.tf
└── terragrunt.hcl

foo/main.tf

variable "content" {}

resource "local_file" "file" {
  content  = var.content
  filename = "${path.module}/hi.txt"
}

foo/terragrunt.hcl

inputs = {
  content = "Hello from foo, Terragrunt!"
}

実行

cd foo
terragrunt apply

Terragruntを使う利点:

  • terraform init を自動実行(Auto-init機能)
  • inputsブロックで変数を宣言的に管理(-varフラグ不要)

2. ユニットの追加と共有モジュール

ユニットとは

Terragruntにおける「ユニット」は、terragrunt.hclを含むディレクトリのことで、単一のインフラストラクチャを表します。

共有モジュールの作成

fooをコピーしてbarを作成し、共通ロジックをsharedディレクトリに切り出します。

quickstart/
├── foo/
│   ├── main.tf
│   └── terragrunt.hcl
├── bar/
│   ├── main.tf
│   └── terragrunt.hcl
└── shared/
    └── main.tf

shared/main.tf

variable "content" {}

resource "local_file" "file" {
  content  = var.content
  filename = "${path.module}/hi.txt"
}

foo/main.tf

variable "content" {}

module "shared" {
  source  = "../shared"
  content = var.content
}

この構成でも動作しますが、foo/main.tfとbar/main.tfに重複があります。

3. DRY構成への進化

terraformブロックでソースを指定

各ユニットのmain.tfを削除し、terragrunt.hclのterraformブロックでソースを指定します。

quickstart/
├── foo/
│   └── terragrunt.hcl
├── bar/
│   └── terragrunt.hcl
└── shared/
    └── main.tf

foo/terragrunt.hcl

terraform {
  source = "../shared"
}

inputs = {
  content = "Hello from foo, Terragrunt!"
}

bar/terragrunt.hcl

terraform {
  source = "../shared"
}

inputs = {
  content = "Hello from bar, Terragrunt!"
}

ポイント

  • Terraformコードはshared/にのみ存在
  • 各環境の差分はterragrunt.hclのinputsで管理
  • .tfファイルの重複が完全に排除される

4. スタック管理(複数ユニットの一括操作)

スタックとは

「スタック」は、一緒に管理されるユニットの集合です。dev/staging/prod環境やプロジェクト全体を指します。

run-allコマンド

親ディレクトリから複数ユニットを一括操作できます。

cd quickstart
terragrunt run-all plan   # 全ユニットでplan
terragrunt run-all apply  # 全ユニットでapply

実行例:

The stack at . will be processed in the following order for command apply:
Group 1
- Module ./bar
- Module ./foo

Are you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n)

Terragruntは並列実行し、各ユニットの出力を区別して表示します。

5. DAG(依存関係)管理

依存関係の設定

barがfooの出力に依存する場合を考えます。

shared/output.tf(追加)

output "content" {
  value = local_file.file.content
}

bar/terragrunt.hcl

terraform {
  source = "../shared"
}

dependency "foo" {
  config_path = "../foo"
}

inputs = {
  content = "Foo content: ${dependency.foo.outputs.content}"
}

DAGによる実行順序の自動決定

run-all実行時、Terragruntは依存関係を分析してDAG(有向非巡回グラフ)を構築します。

Group 1: foo  (先に実行)
Group 2: bar  (fooの後に実行)

これにより、barはfooの出力を確実に参照できます。

6. mock_outputs(依存先のoutputがない場合のフォールバック)

問題

fooがまだapplyされていない状態でbarをplanしようとするとエラーになります。

../foo/terragrunt.hcl is a dependency of ./terragrunt.hcl but detected no outputs.
Either the target module has not been applied yet, or the module has no outputs.

解決:mock_outputs

mock_outputsを設定することで、依存先のoutputがない場合でもplanが可能になります。

bar/terragrunt.hcl

terraform {
  source = "../shared"
}

dependency "foo" {
  config_path = "../foo"
  mock_outputs = {
    content = "Mocked content from foo"
  }
}

inputs = {
  content = "Foo content: ${dependency.foo.outputs.content}"
}

mock_outputsの動作

フェーズ fooのstate barで使われる値
plan(foo未apply) なし mock_outputsの値
apply あり fooの実際のoutput
plan(foo apply済み) あり fooの実際のoutput

run-all applyの場合、DAGに従ってfooが先にapplyされるため、barのapply時には実際の値が使用されます。

7. mock_outputs_allowed_terraform_commands

用途

CIパイプラインなどで「plan時はmockでOKだが、apply時に依存先が未applyの状態は危険なので禁止したい」場合に使用します。

bar/terragrunt.hcl

terraform {
  source = "../shared"
}

dependency "foo" {
  config_path = "../foo"
  mock_outputs = {
    content = "Mocked content from foo"
  }
  mock_outputs_allowed_terraform_commands = ["plan"]  # planのみ許可
}

inputs = {
  content = "Foo content: ${dependency.foo.outputs.content}"
}

動作確認

fooのstateがない状態で:

コマンド mock使用 結果
terragrunt plan 許可 成功(mock値使用)
terragrunt apply 禁止 エラー
terragrunt run-all apply 不要 成功(DAG順で実行)

8. .terragrunt-cache と path.module

ファイルが作成される場所

${path.module}/hi.txtを使用すると、ファイルは.terragrunt-cache内に作成されます。

foo/.terragrunt-cache/xxxxx/yyyyy/hi.txt  ← ここに作成される

理由

Terragruntの動作フロー:

  1. foo/でterragrunt applyを実行
  2. shared/のコードをfoo/.terragrunt-cache/xxxxx/yyyyy/にコピー
  3. そのキャッシュディレクトリ内でTerraformを実行

このため、path.moduleはキャッシュディレクトリを指します。

get_terragrunt_dir()で解決

terragrunt.hclが存在するディレクトリにファイルを作成したい場合は、get_terragrunt_dir()を使用します。

shared/main.tf

variable "content" {}
variable "output_dir" {}

resource "local_file" "file" {
  content  = var.content
  filename = "${var.output_dir}/hi.txt"
}

foo/terragrunt.hcl

terraform {
  source = "../shared"
}

inputs = {
  content    = "Hello from foo, Terragrunt!"
  output_dir = get_terragrunt_dir()  # "/path/to/foo" が渡される
}

関連する組み込み関数

関数 説明
get_terragrunt_dir() terragrunt.hclが存在するディレクトリのパス
get_parent_terragrunt_dir() 親のterragrunt.hclが存在するディレクトリのパス
get_repo_root() Gitリポジトリのルートディレクトリのパス

まとめ

Terragruntを使うことで:

  1. DRY構成: terraform { source = "..." }で共通モジュールを参照し、inputsで環境差分を管理
  2. スタック管理: run-allで複数ユニットを一括操作
  3. DAG: dependencyブロックで依存関係を定義し、実行順序を自動決定
  4. mock_outputs: 依存先が未applyでもplanを実行可能
  5. mock_outputs_allowed_terraform_commands: mockの使用を特定コマンドに制限

Terragruntは、Terraform/OpenTofuプロジェクトをスケールさせる際の複雑さを大幅に軽減してくれるツールです。

参考

Discussion