🦆

俺はもう繰り返さない!Terragruntで実現するDRYな管理

2024/09/28に公開

対象読者

  • Terraform(以下tf)を使い始めてplan時間の長さに悩んでいる方
  • tfstateの分割を検討している方
  • tfの設定ファイルの管理を楽にしたい方など

Terragruntとは?

TerragruntはGruntwork.ioが出しているOSSで、tfのラッパーツールです。
ファイルやコードの重複を減らしてDRYに保ちtfを効率的に管理できるようになります。
https://terragrunt.gruntwork.io/

アイコンが可愛いですね

Trraformだけじゃダメなの?

tfで管理するリソースが増えて1つのtfstateファイルが大きくなると、planなどの実行時間がどんどん長くなります。これは例えばplanが以下の流れになっているからです。

  1. tfstateファイルから値を読み込む
  2. tfstateとマッピングする実際のインフラとの差分検知(ドリフト検知)
  3. 実行dir以下全ての.tfファイルのソースコードを解析
  4. 2と3の結果を基に差分を出力

https://developer.hashicorp.com/terraform/cli/commands/plan

tfstateを制する者がtfを制すると言いますが、
普段開発する際にはtfstateを意識しなくてOKでも裏ではこの流れになっています。
つまりtf管理しているリソースを沢山見に行くことになるため、実行時間が長くなってしまうのです。(-targetオプションで回避も可能ですが毎回の指定はかなり手間です)

The terraform plan command evaluates a Terraform configuration to determine the desired state of all the resources it declares, then compares that desired state to the real infrastructure objects being managed with the current working directory and workspace. It uses state data to determine which real objects correspond to which declared resources, and checks the current state of each resource using the relevant infrastructure provider's API.

ProviderのAPIを使用して管理するインフラを確認しにいく感じですね。
https://developer.hashicorp.com/terraform/cli/run

そんなあなたにTerragrunt

上記を解消するためにはtfstateファイルの分割が有効です。1つのtfstateファイルに管理するリソースが減れば実行時間の短縮が期待できます。

しかしこれを単純にやってしまうと...

  • tfだけの世界
.
├── environmets/
│   ├── dev/
│   │   ├── ecr/
│   │   │   ├── main.tf
│   │   │   ├── backend.tf # これと
│   │   │   ├── provider.tf # これと
│   │   │   └── terraform.tf # これが全ての{env}/{service}/以下に必要
│   │   └── ecs/
│   ├── stg/
│   └── prd/
└── modules/
    ├── ecr/
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── ecs/

設定ファイルが増えすぎて管理が大変になります...殆ど内容は同じなのに😭
(tf公式のディレクトリ構成)

そこでTerragruntを使うと以下のような構成に変化します!

  • Terragruntが有る世界
.
├── environmets/
│   ├── dev/
│   │   ├── ecr/
│   │   │   └── terragrunt.hcl # 子
│   │   ├── ecs/
│   │   └── terragrunt.hcl # 親
│   ├── stg/
│   └── prd/
└── modules/
    ├── ecr/
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── ecs/

親のhclファイルの設定を子のhclファイルで継承したり変数で値も渡せるため、必要なファイルを劇的に減らせます。これだけでもかなりハッピーですが他にもメリットが沢山あるので後述します。

使ってみる!(準備)

Macの場合はhomebrewでインストール可能です。
tfenvと同様にtgenvが存在するためこちらを使うのがよさそうに思いました。
https://terragrunt.gruntwork.io/docs/getting-started/install/

具体的な記述内容は以下のようになります。

environments/dev/terragrunt.hcl
# 親
remote_state { # backend.tfの設定
  backend = "s3"

  generate = {
    path      = "_backend.tf"
    if_exists = "overwrite_terragrunt"
  }

  config  = {
    # Default versioning setting is enabled.
    bucket               = "terragrunt-${local.env}-${local.name}-terraform-tfstate-s3-bucket"
    key                  = "${path_relative_to_include()}/terraform.tfstate"
    region               = "${local.region}"
    encrypt              = true
    bucket_sse_algorithm = "AES256"

    s3_bucket_tags = {
      "Environments"        = "${path_relative_to_include()}"
      "ServiceName"         = "${local.name}"
      "CreatedByTerragrunt" = "true"
    }
  }
}

generate "provider" { # provider.tfの設定
  path      = "_provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
    provider "aws" {
      region = "${local.region}"
      default_tags {
        tags = {
          Environment        = "${local.env}"
          ServiceName        = "${local.name}"
          ManagedByTerraform = true
        }
      }
    }
  EOF
}

generate "version" { # terraform.tfの設定
  path      = "_terraform.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
    terraform {
    required_version = "~> 1.9.0"
    required_providers {
      aws = {
        version = "~> 5.59.0"
        source  = "hashicorp/aws"
      }
    }
  }
  EOF
}

locals {
  env_vars    = yamldecode(file(find_in_parent_folders("env_vars.yaml")))
  common_vars = yamldecode(file(find_in_parent_folders("common_vars.yaml")))

  env    = local.env_vars.env
  region = local.common_vars.region
  name   = local.common_vars.name
}

親のhclファイルにいつも作成するファイル(backend.tf・versions.tf・provider.tf)の設定を記載しています。

environments/dev/ecr/terragrunt.hcl
# 子
include "root" {
  path = find_in_parent_folders()
}

terraform { # main.tfの設定
  source = "../../modules/ecr"
}

inputs = {
  env  = local.env
  name = local.name
}

locals {
  env_vars    = yamldecode(file(find_in_parent_folders("env_vars.yaml")))
  common_vars = yamldecode(file(find_in_parent_folders("common_vars.yaml")))

  env  = local.env_vars.env
  name = local.common_vars.name
}

子のhclファイルから値を注入する感じです。
いつもと勝手が異なるのは、locals変数ではなくyamlファイルに共通の変数を記載しています。
https://terragrunt.gruntwork.io/docs/features/locals/#including-globally-defined-locals

yamlファイルを含めた全体の構成は以下です。

.
├── environmets/
│   ├── dev/
│   │   ├── ecr/
│   │   │   └── terragrunt.hcl
│   │   ├── ecs/
│   │   ├── terragrunt.hcl
│   │   └── env_vars.yaml # これと
│   ├── common_vars.yaml # これ追加
│   ├── stg/
│   └── prd/
└── modules/
    ├── ecr/
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── ecs/

yamlには以下の感じで変数を記載します。

environments/dev/env_vars.yaml
env: dev
environments/common_vars.yaml
region: ap-northeast-1
name: fumis-infra

使ってみる!

この状態でecr/からTerragruntを実行すると、なんとtfstate用のバケットを自動で作成してくれちゃうんです...👏👏👏
tfstate用のバケットはtf管理しないことを推奨しているため、CFnなどから作るのが一般的ですが、Terragruntを使うことでその煩わしさから解放されて、コード管理も可能になっちゃいます!
(tfのコマンドを実行するとエラーになる場合もあるので注意してください)

environments/dev/ecr $ terragrunt init # 🙅‍♂️ tf init
Remote state S3 bucket terragrunt-dev-fumis-infra-terraform-tfstate-s3-bucket
does not exist or you don't have permissions to access it. Would you like Terragrunt to create it? (y/n)y
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.59.0
Terraform has been successfully initialized!
:

バケットもしっかり作ってくれていますね!

$ aws s3 ls
2024-xx-xx xx:xx:xx terragrunt-dev-fumis-infra-terraform-tfstate-s3-bucket

(細かい話をすると、initせずにplanをしてもinitから始めてくれます。これがあるだけでtfの場合のinitし忘れでエラーになるのも防げますね!)

あとはいつも通りplan/applyするだけです。

environments/dev/ecr $ terragrunt plan # 🙅‍♂️ tf plan

コマンドの互換性

Terragrunt is an orchestration tool for OpenTofu/Terraform, so except for a few of the special commands defined in these docs, Terragrunt forwards all other commands to OpenTofu/Terraform. For example, when you run terragrunt apply, Terragrunt executes tofu apply/terraform apply.

公式の言及にもある通り、いつもtfで実行している$ terraform xxx$ terragrunt xxxに変えるだけで殆どのコマンドを実行できます。
https://terragrunt.gruntwork.io/docs/reference/cli-options/#all-opentofuterraform-built-in-commands

またTerragrunt独自のコマンドもいくつかあります。
その中でも特に最高だと思ったのがrun-allコマンドです!

The command will recursively find terragrunt modules in the current directory tree and run the OpenTofu/Terraform command in dependency order (unless the command is destroy, in which case the command is run in reverse dependency order).

tfstateを分割すると対象のディレクトリ毎にapplyが必要になってしまい、リソース毎の依存関係も考慮しなければなりません。
手動で行うのはかなり辛くなってくると思いますが、Terragruntのrun-allコマンドを使えばその点も解消してくれます。つまり以下のディレクトリから実行すればその下の全ディレクトリに対して依存関係を解消しながら再起的にapplyしてくれます!

environmets/dev/ $ terragrunt run-all apply # 1個上の階層から実行

もちろん手動実行をやめてCI/CDを組むことも可能ですが、tfstateの分割が多くなると作り込みも大変になると思うので、選択肢の1つにはなりそうです。またTerragrunt自体もGitHub Actionsに対応しているため、いつものノリでusesから使用可能です。
https://github.com/gruntwork-io/terragrunt-action

改めてメリット・デメリット

🙆‍♂️

  • ファイル数、コードの重複を減らしてDRYにできる
  • tfstateファイルを保管するBucketを自動作成してくれる
  • run-allコマンドなどによってapply工数削減、依存関係を解決してくれる

🙅‍♂️

  • 情報が少ない、公式ドキュメントが英語なので調査も大変
  • Terragrunt独自の関数を覚える必要がある

https://terragrunt.gruntwork.io/docs/reference/built-in-functions/

採用するか?

tfstateファイル肥大のリスクを抱えていて、tfstateファイルを分割しない痛み(作業のconflict、plan時間の増加など)を理解している場合は、採用はかなりアリだと感じました。tfstateを分割するとapply回数やファイル数増加など発生しますが、Terragruntのメリットに挙げた部分でうまく吸収してくれると思います。
メリットは大きいですがTerragruntを実際に運用しているチームや知見を持っている方を殆ど見たことが無く、情報・利用例が少ないため公式ドキュメントを中心に自分達で模索していく必要がありそうに思います。
また新しい人が入ってきた場合にキャッチアップが少し大変になりそうなので、インフラの規模が小さかったり、tfの知見が少ない場合は採用しない方がよさそうに思いました。

まとめ

tfstateを分割するとapplyの手間が増えるため、個人的にはこれから使っていきたいと思いました。
まだ使ってみたレベルなのでもっと触ってみてメリデメの把握や、運用していく中でのベストな使い方などを見つけられたらと思います。

参考

Discussion