🧑‍💻

Terragruntで実行環境の管理を改善しよう

2024/05/16に公開

はじめに

こんにちはへたれです。
株式会社アイデミーでエンジニアとして、材料開発のためのデータ活用プラットフォームLab Bankを開発、運用しています。

課題と解決

Lab Bankではマルチテナント構成のTerraform戦略で紹介したようにテナントごとに細かくtfstateを分割して管理しています。
おかげで今のところは当初の狙い通りに影響範囲やリリースのし易さは確保できています。
ただ運用をしていく上で以下のような課題ができてきました。

  • dev, stg, prdやテナントA, テナントBなど重複する記述が多すぎる
    • プロバイダやバックエンドの記述が個別
    • 環境やテナントごとにmoduleのバージョン分けができない
  • 手作業が多く、環境の誤認識が起きがち
    • devと間違えてprdにapplyする危険性
    • tfvarsの更新忘れ

全部手順書化 → 手順が複雑化しすぎるとやはりミスが起きやすくなる、良くはないけれども慣れると手順書を見ずに作業して失敗したりする...
手順を自動化するツールを作成 → 無闇に自作ツールを作ってメンテナンスコストを増やしたくない

何かいいツールはないか?とネットの海を彷徨っていたところに発見したのが、Terragruntでした。

Terragruntとは?

Terragrunt is a thin wrapper that provides extra tools for keeping your configurations DRY, working with multiple Terraform modules, and managing remote state.

とあるように、Terraformの痒いところに手が届くラッパーツールというところです。
ドキュメントを読んだ印象としては、terraformのリソース記述ではなく、モジュールや実行環境(プロバイダやバックエンド)の管理を簡便にしてくれるツールという印象でした。
設定ファイルもterragrunt.hcl、つまりはHCLで書けるため文法の学習コストもほぼ皆無ですし、複雑なロジックも入れ込む余地がありません。

細かい機能などは公式ドキュメントか、他の方のブログで解説されているものもあるのでそちらをご覧ください。

個人的に魅力と感じた機能はこの辺でした。

Terraform Cloud等と異なり、OSSのCLIツールなのでサッと導入できるのもいいところですね。

GruntWorkなるプラットフォームもあるみたいですが、本記事では触れません。

使ってみた

実際に動かして何がどの程度できるのか?を確認してみました。
動かしてみたサンプルコードはGitHubリポジトリで公開しています。

実行環境の共通化

prdの実行環境共通部分を /env/prd/terragrunt.hcl に記述し、各テナントは /env/prd/tenant_a/terragrunt.hcl および /env/prd/tenant_b/terragrunt.hcl から共通設定を読み込んでいます。

/env/prd/tenant_a/terragrunt.hcl
include "prd" {
  path = find_in_parent_folders()
}

実行環境共通部分はいくつかのパートに別れています。
以下のパートはtfstateを置くバックエンドについての設定です。

/env/prd/terragrunt.hcl
remote_state {
  backend = "s3"
  config = {
    bucket = "terragrunt-sample-tfstate"
    key    = "${path_relative_to_include()}/terraform.tfstate"
    region = "ap-northeast-1"
    dynamodb_table = "terragrunt-sample-lock-table"
  }
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
}

次のパートはプロバイダ周りです。
本当は文字列で生成するのではなく、ちゃんとバリデーションされる形で渡せるようになって欲しいのですが...
該当機能が見当たらなかったのと、公式ドキュメントでもこのような形で紹介されていました。

/env/prd/terragrunt.hcl
generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite"
  contents = <<EOF
terraform {
  required_version = "1.7.4"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.33.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
  default_tags {
    tags = {
      Env       = "dev"
      App       = "sample-code"
      CreatedBy = "居石峻寛"
      ManagedBy = "terraform"
    }
  }
}
EOF
}

これらを記述したのち、terragrunt plan を実行すると自動でファイルが作成され、以下のような構成になりました。
(initを自動で行ってくれるのもよかったです)

.
├── backend.tf
├── provider.tf
├── tenant_a
│   ├── backend.tf
│   ├── provider.tf
│   └── terragrunt.hcl
├── tenant_b
│   ├── backend.tf
│   ├── provider.tf
│   └── terragrunt.hcl
└── terragrunt.hcl

ここでbackend.tf、provider.tfは上述のterrgrunt.hclで指定した内容で、以下のような形で出力されています。
(tenant_a, tenant_b以下も同じ内容です)

backend.tf
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
  backend "s3" {
    bucket         = "terragrunt-sample-tfstate"
    dynamodb_table = "terragrunt-sample-lock-table"
    key            = "tenant_b/terraform.tfstate"
    region         = "ap-northeast-1"
  }
}
provider.tf
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
  required_version = "1.7.4"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.33.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
  default_tags {
    tags = {
      Env       = "dev"
      App       = "sample-code"
      CreatedBy = "居石峻寛"
      ManagedBy = "terraform"
    }
  }
}

また/env/prd, /env/prd/tenant_a, /env/prd/tenant_bそれぞれで terragrunt apply を実行した後にS3を覗きにいくと、以下のようなパスでtfstateが配置されていました。

.
├── terraform.tfstate
├── tenant_a
│   └── terraform.tfstate
└── tenant_b
    └── terraform.tfstate

こちらの記述により、terragrunt.hclが配置されている相対パスを使って自動的にS3のパスが計算されたためです。

/env/prd/terragrunt.hcl
key    = "${path_relative_to_include()}/terraform.tfstate"

Hooks機能

またTerragruntにはHooks機能があります。
これはTerragruntの各種コマンドを実行する前後に合わせて任意のコマンドを実行できるという代物です。
Hooksの設定もterragrunt.hclにて行います。
get_terragrunt_dir()でなくget_parent_terragrunt_dir()を指定しているのは、tenant_a, tenant_b配下でも同様のスクリプトを利用したいため、include先のディレクトリを指定しています。

/env/prd/terragrunt.hcl
terraform {
  # planとapplyの直前にPRD環境であることをチェックする
  before_hook "before_hook" {
    commands     = ["apply", "plan"]
    execute      = ["${get_parent_terragrunt_dir()}/confirm_env.sh"]
  }
}

実行するコマンドは以下のシェルスクリプトにて指定しています。
plan, applyの前にPRD環境で実行しようとしていることを確認し、環境認識のミスを防ぐ目的で作りました。

/env/prd/confirm_env.sh
#!/bin/sh
read -p "PRD環境ですが実行しますか? (prd/N): " input
case $input in "prd"*) ;; *) echo "aborted" && exit 1 ;; esac

実際の動作はこのようになっています。

% terragrunt apply
INFO[0001] Executing hook: before_hook                  
PRD環境ですが実行しますか? (prd/N): N
aborted
ERRO[0002] Error running hook before_hook with message: exit status 1 
ERRO[0002] Errors encountered running before_hooks. Not running 'terraform'. 
ERRO[0002] 1 error occurred:
        * exit status 1

% terragrunt apply
INFO[0001] Executing hook: before_hook                  
PRD環境ですが実行しますか? (prd/N): prd

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

おわりに

いかがでしたでしょうか?
過度に複雑化をせずに実行環境をDRYに記述できるのはとても便利だなと感じました。
またそれ以上にHooksを上手く使いこなせるとTerraformの運用フローからミスを削減していけるなという可能性を感じています。
2024/05/16時点ではほぼ毎週のようにマイナーアップデートされていますし、今後の発展も期待できるのではないでしょうか?

もしツールをチームで導入して運用することができた暁には、運用方法について再度記事にさせていただければと思っています。

またアイデミーでは一緒に働く仲間を募集中です!
今回の記事を読んで興味が湧いた方、まずはカジュアル面談からでもお待ちしております!

Aidemy Tech Blog

Discussion