😽

Terraformをいい感じで管理するTerragruntのメリットを紹介する

2023/12/22に公開

記事の内容

Terraformをいい感じに管理できるOSSツールTerragruntのメリットを紹介します。

対象読者

  • Terraformユーザー
  • Terraformの管理をいい感じにしたい人

記事の長さ

5分で読めます

Terragruntとは

https://terragrunt.gruntwork.io/

Terragruntとは、Gruntwork.io (https://gruntwork.io/)が出しているOSSプロジェクトです。

Terragruntを使うことにより、Terraformを便利に使いこなすことができるようになります。

Terragruntを使い始めたきっかけ

Terraformでさまざまなリソースを管理しているとstateファイルが肥大化します。
すると、少しの変更でもplanに時間がかかったり、意図しない差分が発生しリリースに時間がかかるようになりました。

そういった課題を解決するために、Terragruntに注目しました。

Terragruntを利用すると、Backendを細かく分けStateファイルを小さく保つと同時に、ソースコード自体をDRYに記述することができます。
また、各module間のdependencyも管理することができるので、stateファイルが分かれていたとしても他のmoduleのoutputの値を利用できるようになります。

Terragruntを使うメリット

Terragruntを導入するメリットを紹介します。

英語にはなるのですが、詳しくは公式ドキュメントに記載があります。

https://terragrunt.gruntwork.io/docs/#getting-started

本記事では、簡潔に紹介いたします。

Terraform Stateを管理するバケットもIaCで管理できる

Terraformでプロジェクトを作成するときに発生する問題の一つが、TerraformのStateファイルを管理するバケット自体をTerraformで管理できないということです。

というのも、Terraformの環境構築をする際に、Terraform Stateを配置するバケット(S3やGCS)を指定しないといけないのですが、Terraformの環境構築が完了していないとバケット自体が作れないので、バケットは別途先に作っておく必要があります。(ニワタマ問題)

Terragruntをつかうとその問題を解決できます。

Terragruntを使って、Bucketを自動作成する

Terragruntでは、terragrunt.hclというファイルを設定ファイルとして利用します。

Terragruntで管理したいディレクトリのルートに以下の設定ファイルを配置し、terragrunt planコマンドを実行します。

terragrunt.hcl

remote_state {
  backend = "gcs"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    project = "terragrunt-experiment"
    bucket = "terragrunt-state-auto-generate"
    prefix = "${path_relative_to_include()}/terraform.tfstate"
    location = "asia-northeast1"
  }
}
$ terragrunt plan
Remote state GCS bucket terragrunt-state-auto-generate 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...

Successfully configured the backend "gcs"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

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.

すると、terragrunt-state-auto-generateというバケットがないが、そのバケットをTerraform Stateを配置するために利用しているため、作成してもいいですか?という文言が表示されて、Yを入力すると自動でそのバケットが作成されます。

このTerragruntのプロジェクトではそのバケットがStateファイルを格納するバケットとして登録されます。

Terragruntの挙動

$ tree
.
├── backend.tf
└── terragrunt.hcl

先ほどの、terragrunt planを実行したディレクトリを見ると、backend.tfファイルが自動生成されています。これは、terragruntが自動で生成したファイルでTerraformのbackend設定が書き込まれています。

backend.tf

# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
  backend "gcs" {
    bucket = "terragrunt-state-auto-generate"
    prefix = "./terraform.tfstate"
  }
}

つまり、Terragruntが行ったことは、terragruntの設定ファイルに基づき、GCSバケットを作成し、backend.tfを自動生成したことになります。
TerragruntはTerraformの薄いラッパーなので、人間にとってわかりやすくTerraformを管理することを可能にしてくれます。

Stateファイルを小さく保つ

Stateファイルを配置するバケットを自動生成してくれるメリットを説明しました。
それだけだと、Terragruntを導入するほどのメリットにはなりません。

Terragruntを導入する最大のメリットはTerraformのStateファイルを小さく保てるということです。

例えば、以下のような構成を作成します。(network/main.tfを追加)

.
├── backend.tf
├── network
│   └── main.tf
└── terragrunt.hcl

この構成で、network/main.tfにVPC Networkを記述するResourceを追加します。

network/main.tf

resource "google_compute_network" "vpc_network" {
  name                    = "vpc-network"
  auto_create_subnetworks = false
}

このnetworkディレクトリに以下のterragruntの設定ファイルを追加します。

network/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

これは、親ディレクトリのTerragruntの設定ファイルを利用するための記述です。この状態で、terragrunt planを実行します。

$ cd network
$ terragrunt plan
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # google_compute_network.vpc_network will be created
  + resource "google_compute_network" "vpc_network" {
      + auto_create_subnetworks                   = false
      + delete_default_routes_on_create           = false
      + gateway_ipv4                              = (known after apply)
      + id                                        = (known after apply)
      + internal_ipv6_range                       = (known after apply)
      + mtu                                       = (known after apply)
      + name                                      = "vpc-network"
      + network_firewall_policy_enforcement_order = "AFTER_CLASSIC_FIREWALL"
      + numeric_id                                = (known after apply)
      + project                                   = "terragrunt-experiment"
      + routing_mode                              = (known after apply)
      + self_link                                 = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

上記のようにTerraform Planの結果が表示されます。また、Terragruntを使って、planを実行したことにより、backend.tfファイルがnetworkディレクトリの中に自動生成されます。

network/backend.tf

# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
  backend "gcs" {
    bucket = "terragrunt-state-auto-generate"
    prefix = "network/terraform.tfstate"
  }
}

先ほどルートディレクトリで設定した、terragrunt.hclの設定を踏襲し、同じバケットにnetworkというprefixがついたstateファイルを作成するbackend.tfファイルです。

この機能により、ディレクトリ毎にStateファイルが作成されます。ディレクトリ毎にStateファイルが作成されることにより、モノリスで巨大なStateファイルが生み出す不都合を解決することができます。

Providerを一括管理できる

様々なディレクトリにResourcesが分かれてしまうと、Providerの管理が煩雑になりがちです。
全てのディレクトリでprovider.tfファイルを作成して、コピペを繰り返していくのはあまりにも冗長なので、TerragruntはProviderを一括で管理する機能を提供しています。

terragrunt.hcl

remote_state {
  backend = "gcs"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    project = "terragrunt-experiment"
    bucket = "terragrunt-state-auto-generate"
    prefix = "${path_relative_to_include()}/terraform.tfstate"
    location = "asia-northeast1"
  }
}

generate "provider" {
  path = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
provider "google" {
  project = "terragrunt-experiment"
  region  = "asia-northeast1"
}
provider "google" {
  alias   = "us-central1"
  project = "terragrunt-experiment"
  region  = "us-central1"
}
EOF
}

ルートのterragrunt.hclに、generate "provider" ブロックを追加します。

この状態で、terragrunt planを実行すると、各ディレクトリに、provider.tfファイルが自動生成されます。

$ tree
.
├── backend.tf
├── network
│   ├── backend.tf
│   ├── main.tf
│   ├── provider.tf
│   └── terragrunt.hcl
└── terragrunt.hcl

CLIにvariablesを自動で取り込む

Terraformでvariablesの値をファイルで管理して、CLI実行時に取り込む場合、

$ terraform apply \
    -var-file=../../common.tfvars \
    -var-file=../region.tfvars

のように、引数を指定する必要があります。しかし、都度オプションを指定するのはめんどくさいですし、指定忘れのリスクもあります。もちろん、チーム開発する際には余計なコミュニケーションコストも発生します。

Terragruntを使うと、ここもソースコードで管理できるようになります。

common.tfvars

service_name = "terragrunt"

network/variable.tf

variable "service_name" {
  type = string
}

network/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}
terraform {
  extra_arguments "common_vars" {
    commands = get_terraform_commands_that_need_vars()

    arguments = [
      "-var-file=../common.tfvars"
    ]
  }
}

上記のようにファイルを作成し、networkディレクトリ配下でterragrunt planを実行します。

$ cd network
$ terragrunt plan
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # google_compute_network.vpc_network will be created
  + resource "google_compute_network" "vpc_network" {
      + auto_create_subnetworks                   = false
      + delete_default_routes_on_create           = false
      + gateway_ipv4                              = (known after apply)
      + id                                        = (known after apply)
      + internal_ipv6_range                       = (known after apply)
      + mtu                                       = (known after apply)
      + name                                      = "terragrunt"
      + network_firewall_policy_enforcement_order = "AFTER_CLASSIC_FIREWALL"
      + numeric_id                                = (known after apply)
      + project                                   = "terragrunt-experiment"
      + routing_mode                              = (known after apply)
      + self_link                                 = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

すると、common.tfvarsで指定したvariableの値を取り込んで、Planが実行されます。

簡易的なmodule管理

├── prod
│   ├── app
│   │   ├── main.tf
│   │   └── outputs.tf
│   ├── mysql
│   │   ├── main.tf
│   │   └── outputs.tf
│   └── vpc
│       ├── main.tf
│       └── outputs.tf
├── qa
│   ├── app
│   │   ├── main.tf
│   │   └── outputs.tf
│   ├── mysql
│   │   ├── main.tf
│   │   └── outputs.tf
│   └── vpc
│       ├── main.tf
│       └── outputs.tf
└── stage
    ├── app
    │   ├── main.tf
    │   └── outputs.tf
    ├── mysql
    │   ├── main.tf
    │   └── outputs.tf
    └── vpc
        ├── main.tf
        └── outputs.tf

Terragruntを使って、小さなStateファイルを管理できるメリットは説明しましたが、それのデメリットとして、上記のように環境✖️機能ごとにディレクトリができてしまうというものがあります。

Terragruntではmoduleを利用して、この問題にアプローチします。

各環境からimportするTerraform Resourceを定義するmoduleディレクトリを作成します。
また、そのmoduleディレクトリの中に、networkに関するmoduleを作成するnetworkディレクトリと必要なterraformファイル一式を用意します。

$ tree
└── modules
    └── network
        ├── main.tf
        ├── outputs.tf
        ├── provider.tf
        └── variables.tf

それぞれのファイルを以下のように編集します。

main.tf

resource "google_compute_network" "this" {
  project                = var.project
  name                    = var.name
  auto_create_subnetworks = false
}

outputs.tf

output "network_id" {
  value = google_compute_network.this.id
}

provider.tf

provider "google" {
  region = var.region
  project = var.project
}

variables.tf

variable "region" {
  type = string
  default = "asia-northeast1"
}

variable "name" {
  type = string
}

variable "project" {
  type = string
}

そして、このmoduleを引き込む形で、terragruntの設定ファイルを作成します。

network/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}
terraform {
  source = "../../modules/network"
}

inputs = merge(
  {
    name = "my-service"
    region = "asia-northeast1"
    project = "terragrunt-experiment"
  }
)

そして、terragrunt planコマンドを実行します。

$ terragrunt plan

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # google_compute_network.this will be created
  + resource "google_compute_network" "this" {
      + auto_create_subnetworks                   = false
      + delete_default_routes_on_create           = false
      + gateway_ipv4                              = (known after apply)
      + id                                        = (known after apply)
      + internal_ipv6_range                       = (known after apply)
      + mtu                                       = (known after apply)
      + name                                      = "my-service"
      + network_firewall_policy_enforcement_order = "AFTER_CLASSIC_FIREWALL"
      + numeric_id                                = (known after apply)
      + project                                   = "terragrunt-experiment"
      + routing_mode                              = (known after apply)
      + self_link                                 = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + network_id = (known after apply)

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

すると、与えた引数の通りにmoduleのTerraformResourceを実行します。

こうすることによって、inputの値だけを変更したterragrunt.hclファイルを環境分用意すれば、DRYなディレクトリ構成になります。

このやり方で、再構築したTerragruntプロジェクトのディレクトリ構成は以下のようになります。

├── prod
│   ├── app
│   │   ├── terragrunt.hcl
│   ├── mysql
│   │   ├── terragrunt.hcl
│   └── vpc
│       ├── terragrunt.hcl
├── qa
│   ├── app
│   │   ├── terragrunt.hcl
│   ├── mysql
│   │   ├── terragrunt.hcl
│   └── vpc
│       ├── terragrunt.hcl
└── stage
    ├── app
    │   ├── terragrunt.hcl
    ├── mysql
    │   ├── terragrunt.hcl
    └── vpc
        ├── terragrunt.hcl

共通の値を利用する

DRYなファイル構成を実現することができました。
さらに便利にTerragruntを利用するための機能を紹介します。

Terragruntでは、GCP Project IDやServiceの名前・メインで利用しているリージョンなど、共通で使える値をlocalsに指定して、全てのterragrunt.hclファイルで再利用することができます。

.
├── backend.tf
├── common.hcl
├── network
│   ├── backend.tf
│   ├── provider.tf
│   └── terragrunt.hcl
├── provider.tf
├── region.hcl
└── terragrunt.hcl

共通で使用した値をcommon.hclregion.hclというファイルに書き込みます。

common.hcl

locals {
  service_name = "my-service"
  project_id   = "terragrunt-experiment"
}

region.hcl

locals {
  main_region = "asia-northeast1"
}

そして、この二つのhclファイルをterragrunt.hclファイルから呼び出します。

network/terragrunt.hcl

locals {
  common = read_terragrunt_config(find_in_parent_folders("common.hcl"))
  region = read_terragrunt_config(find_in_parent_folders("region.hcl"))
}

include "root" {
  path = find_in_parent_folders()
}
terraform {
  source = "../../modules/network"
}

inputs = merge(
  {
    name = local.common.locals.service_name
    region = local.region.locals.main_region
    project = local.common.locals.project_id
  }
)

こうすることによって、様々なディレクトリから共通の設定値を呼び出すことができるようになり、ソースをさらに最適化できます。

また、find_in_parent_foldersという関数名の如く、terragrunt.hclファイルの親ディレクトリを自動でサーチして、ファイル名が一致するファイルを見つけて取り込んでくれるため、以下のような構成も取れるようになります。

├── common.hcl
├── prod
│   ├── env.hcl
│   ├── app
│   │   ├── terragrunt.hcl
│   ├── mysql
│   │   ├── terragrunt.hcl
│   └── vpc
│       ├── terragrunt.hcl
├── qa
│   ├── env.hcl
│   ├── app
│   │   ├── terragrunt.hcl
│   ├── mysql
│   │   ├── terragrunt.hcl
│   └── vpc
│       ├── terragrunt.hcl
└── stage
    ├── env.hcl
    ├── app
    │   ├── terragrunt.hcl
    ├── mysql
    │   ├── terragrunt.hcl
    └── vpc
        ├── terragrunt.hcl

これで環境毎に異なる設定値を指定することができます。

まとめ

Terraformをさらに使いやすくするTerragruntの解説をしました。

Terraformはそれ単体でも非常に有用なツールで、IaCのデファクトスタンダードになっています。しかし、Terraformのみで使用すると、巨大なStateファイルに悩まされます。

そういったTerraform特有の悩みを解決してくれるのが、Terragruntなので、Terraformを大型プロジェクトで利用する予定がある人は、導入を検討してみてください。

ソース

今回作成したサンプルソースは以下に配置しておきます。

https://github.com/rara-tan/zenn-terragrunt-start

note

勉強法やキャリア構築法など、エンジニアに役立つ記事をnoteで配信しています。

https://note.com/ring_belle/membership

Discussion