Terraform だけだとハードモードなので Terragrunt を使おう

## Terraform はそのままだと管理が大変

みなさん IaC (Infrastructure as Code) してますか？パブリッククラウドをIaCするなら、 [Terraform](https://www.terraform.io/) が便利ですね！

しかし、本格的に使い始めると、こういう問題がすぐに出てきます。

- 複数環境の楽な分け方を知りたい
    - ワークスペースはなんか嫌だ
    - とはいえ、環境間で共通するボイラープレートをどうにかしたい
- 環境内で適用するモジュールを細分化・分岐したいけど面倒
    - 環境ごとに使うモジュールを切り替えたい
        - テスト環境はAuroraではなく安いRDSにしたい
    - モジュール(tfstate)を分割して小さい範囲で適用したい
        - 大きなモジュールは影響範囲がわからないし、差分計算にそれなりに時間がかかってしまう
    - 分けたモジュールを一括適用するのが面倒
        - モジュール間の依存関係がわからない
    - モジュール(tfstate)間での値参照が面倒
        - `terraform_remote_state` の呪文が冗長すぎて厳しい
- 環境ごとに適用するモジュールのバージョンを固定化したい
    - 開発環境は常に最新、本番は特定バージョンのモジュール定義参照で固定したい

定期的にテックブログでTerraformのこういった環境差分の管理方法が話題になりますが、**Terragruntを使えば割と楽に解決できます。**


## Terragrunt とは？

[Terragrunt](https://terragrunt.gruntwork.io/)は [Gruntwork社](https://gruntwork.io/about/) がメンテしている**Terraformのラッパーで、Terraformにちょっと足りないマクロ的な機能を追加してくれます。**

[GitHub](https://github.com/gruntwork-io/terragrunt)にてMIT Licenseで公開されています。

インストールはメジャーどころのリポジトリには既に存在しているので下記でインストール可能です。

- `brew install terragrunt` (macOS)
- `scoop install terragrunt` (Windows)

使い方は簡単で、 `terraform` コマンドのかわりに `terragrunt` コマンドを使うようにするだけです。内部的にはTerragruntがお膳立てをした後、最終的にTerraformが呼び出されるだけです。

基本的にはTerraformの使い方を知っていればTerragrunt自体は難しいことはありません。


## Terragruntプロジェクトの基本的なディレクトリ構造

実際のTerragruntプロジェクトのディレクトリ構造を見ていきたいと思います。

- `modules/` の下にモジュールごと(e.g., `vpc`, `db` など)ディレクトリを掘る
    - モジュール作り方は通常のterraformモジュールと同じ
- `envs/` の下に環境ごと(e.g., `stg`, `prod` など)ディレクトリを掘る
    - 直下に環境全体の変数定義を収める `env.hcl` を置く
    - 参照するモジュールのディレクトリをそれぞれ下に掘って、その中にモジュールの参照定義となる `terragrunt.hcl` を作成する

```
modules/
  modA/
    *.tf
  modB/
  modC/
envs/
  terragrunt.hcl
  stg/
    env.hcl
    modA/
      terragrunt.hcl
    modB/
    modC/
  prod/
    modA/
    modB/
    modC/
```

それぞれの詳しい中身を見ていきましょう。


### モジュールの定義 (`modules/*/*.tf`)

通常のTerraformのモジュール定義と同じです。私の場合は下記のようにファイルを切ることが多いです。

- `input.tf`: 入力パラメータ
- `output.tf`: 出力値
- `機能名.リソース種別.tf`: e.g., `featureA.route53.tf`

このモジュールのディレクトリはそれぞれ `envs/*/*/terragrunt.hcl` （後述） から参照します。


### 環境全体の定義 (`envs/terragrunt.hcl`)

下記のような環境全体の定義ファイルを置きます。実際に各環境ごとのモジュールを適用する際に実行されます。

やっていることは簡単で、各環境のプロバイダやTerraform自体の設定のファイル [^1] のメタテンプレート定義をしているだけです。

[^1]: 皆さんのリポジトリに `backend.tf`/ `main.tf`/`provider.tf` が居座ってないでしょうか？

```hcl:envs/terragrunt.hcl
# 各環境ごとの *.tfstate の入れ方についての定義
remote_state {
  backend = "s3"

  config = {
    bucket = "foobar-tfstate"

    # 下記のようにしておくと、例えば
    # `envs/stg/modA` の tfstate は
    # `foobar/stg/modA.tfstate` に格納されるようになる
    key    = "foobar/${path_relative_to_include()}.tfstate"

    region = "ap-northeast-1"

    profile = "foobar-terraform"
  }

  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"

  contents = <<EOF
terraform {
  required_version = ">= 1.3.7"

  required_providers {
    aws = {
      # See https://github.com/terraform-providers/terraform-provider-aws
      version = "~> 4.50.0"
    }
  }
}

provider "aws" {
  profile = "foobar-terraform"
  region  = "ap-northeast-1"

  default_tags {
    tags = {
      Project = "foobar"
    }
  }
}
EOF

}

```


### 環境ごとの定義 (`envs/*/env.hcl`)

環境全体にまたがる定義（変数など）を配置します。

```hcl:envs/stg/env.hcl
# 検証環境

locals {
  # 環境名
  env = "stg"
  # バックアップ保持日数
  backup_retention_days = 3660
}
```


### 環境とモジュールごとの定義 (`envs/*/*/terragrunt.hcl`)

各環境ごとに利用するモジュールの呼び出し定義を配置します。

```hcl:envs/stg/modA/terragrunt.hcl
# 環境の定義 (`env.hcl`) を local.env.locals として参照できるようにする
locals {
  env = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}

# 全環境の定義 (`envs/terragrunt.hcl`) をインクルードする
include {
  path = find_in_parent_folders()
}

# モジュールを参照する
terraform {
  source = "../../../modules//modA"
}

# 他のモジュールの出力値を参照する
# (素のTerraformだと面倒な作業の一つ)
dependency "modB" {
  config_path = "../modB"
}

# モジュールの入力値を指定する
inputs = {
  env                   = local.env.locals.env
  backup_bucket         = dependency.modB.outputs.backup_bucket
  backup_retention_days = local.env.locals.backup_retention_days
}
```


## Terragrunt を適用する

さて、上記のようなディレクトリ（や前提となるS3やIAMプロファイル等）を用意したら、あとは下記コマンドで Terragrunt を適用するだけです。

実は `modA` と `modC` が、 `modB` に依存しているので、 `modB` から最初に適用します。

```console
$ cd envs/stg/modB
$ terragrunt apply
```

`terraform apply` のかわりに `terragrunt apply` を実行するだけです。**ちなみに `terraform init` はTerragruntが勝手にやってくれるので不要です。** terragruntの綴りでミスタイプしそうになる以外は簡単です。

とはいえ、いちいちモジュールのフォルダを開いてこの作業をやるのは面倒です。

Terragruntは勝手に各モジュールの依存順を計算してくれるので [^2]、いちいち環境ごとにモジュールフォルダを順番に開いて apply せずとも、下記のように **環境ごとのディレクトリで `terragrunt run-all apply` すれば適用順序を勝手に計算して全て init & apply してくれます。非常に楽ですね。**

[^2]: `terragrunt.hcl`の`dependency`で計算

```console
$ cd envs/stg
$ terragrunt run-all apply
The stack at /Users/ksaitou/foobar/envs/stg will be processed in the following order for command apply:
Group 1
- Module /Users/ksaitou/foobar/envs/stg/modB/

Group 2
- Module /Users/ksaitou/foobar/envs/stg/modA/
- Module /Users/ksaitou/foobar/envs/stg/modC/

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

そのまま `y` とタイプすれば順番に全モジュールについて `terraform init` & `terraform apply` してくれます。

あくまでTerragrunt自体はTerraformのマクロ＆ラッパーであるため、思ったような動作にならない場合は `envs/stg/*/.terragrunt-cache` を見れば実際に `terraform apply` に渡した生成ソースコードを確認してトラブルシュートすることも可能です。


## 環境ごとにモジュールのバージョンを固定する

下記のように検証環境のみTerraformモジュールの最新版で開発し、本番環境はリリースタイミングまでは古いバージョンで固定したいケースがあると思いますが、そういったニーズもTerragruntは実現可能です。

- stg: 常に `main` ブランチの内容を適用したい
- prod: 特定のタグの内容を適用したい

環境ごとのモジュールのファイル `terragrunt.hcl` の `terraform { ... }` のモジュールのパス参照部分を下記のように書き換えるだけです。

```hcl:envs/prod/modA/terragrunt.hcl
terraform {
  // 直接ディレクトリを参照する場合
  // source = "../../../modules//modB"

  // 特定のgitリポジトリを参照する場合 (リモートリポジトリの参照のみ可能)
  // source = "git::gitリポジトリURL//リポジトリ内のディレクトリパス?ref=コミットの参照"
  source = "git::ssh://git@github.com:ssc-ksaitou/foobar-modules.git//modules/modB?ref=1.0.0"
}

```

[マニュアル](https://terragrunt.gruntwork.io/docs/features/keep-your-terraform-code-dry/#remote-terraform-configurations) によると `source = "..."` のパスについては [Hashicorp の go-getter](https://github.com/hashicorp/go-getter) を利用して取得されるので、特にgitに限らずMercurialやzipファイル、Amazon S3をソースにすることもできます。（実際にやるかどうかは別として）

ただこれ、リモートのgitではなく、ローカルリポジトリ（自身）のmodulesディレクトリをタグ参照したいという要求が潜在的にはあると思うのですが、前述のgo-getterで現状ローカルのgitリポジトリを参照する機能が無いので、gitの特定タグを参照させるにはあらかじめリモートリポジトリを用意しておく必要があります。やや面倒ですが、ちゃんと回る運用ルールを考えておいたほうがいいでしょう。

## まとめ

Terraform だけで中〜大規模なIaCを展開すると非常に面倒になるのですが、 Terragrunt を使うと楽になれます。