🌤️

Terragruntでより幸せなTerraform生活を目指す

2021/12/19に公開

これは何?

TerraformをリファクタリングするにあたってTerragruntを利用できないか検証する機会があったので、簡単なサンプルを作ってゆるくまとめました。

Terragrunt is 何?

TerragruntGruntwork.io が出しているOSSであり、Terraformをより便利に扱えるようにするための薄いwrapperです。Terragruntを利用することで以下のような構成のTerraformをいい感じにリファクタリングできます。

.
├── envs
│   ├── dev // 配下のディレクトリ単位でtfstateが別れている
│   │   ├── iam
│   │   ├── elb
│   │   └── network
│   └── prd // devと同じような感じで個別のリソース単位でディレクトリが存在する
└── modules // 共通処理
  • Terraformの設定定義をDRYにする
    • providerやbackend関連の定義は ./envs/dev/iam , ./envs/dev/elb などでそれぞれ定義する必要がありますが、それらの定義を1箇所に集約することができる
    • Terraform実行時に付与する引数もDRYにすることができる
  • tfstateの異なるmodule間の実行依存を解決する
    • ./envs/dev/iam , ./envs/dev/elb などには本来依存関係があり(例:IAMリソースを作成してからELBリソースを作成する必要がある)、素のTerraformでは実行順序を工夫する必要があります
    • Terragruntが提供する機能を使うことで依存関係を解決した上でplan/applyなどを実行できるようになります

使ってみよう

習うより慣れろ!というわけで実際のコードを使ってみていきましょう。

Terragruntのinstall

公式の出している手順に沿ってポチポチするとTerragruntをinstallできます。
https://terragrunt.gruntwork.io/docs/getting-started/install/

tfenvよろしく、Terragruntにもversion管理ツールとしてtgenvというものもあるので、そちらを使うのもアリかもしれません。

https://formulae.brew.sh/formula/tgenv

ソースコードの概要

今回は以下のGitHub Repositoryを使って動作確認をしてみます。
https://github.com/yu-croco/terragrunt_sample

あまり良い例(かつ、手軽に用意できるもの)がシュッと思いつかなかったので少し無理やりですが、以下のような構成のリソースを用意しました。
./envs/dev 配下に iam_roleiam_policy が存在し、 iam_roleiam_policy に依存しています(iam_role のリソースを作成しないと iam_policy はエラーになる)。

./envs/dev 配下には各階層に terragrunt.hcl というファイルが配置してありますが、これは後ほど説明します。

$ tree -L 4
.
├── Makefile
├── README.md
└── envs
    ├── dev
    │   ├── iam_policy // `iam_role` のoutput値に依存した処理を持つ
    │   │   ├── main.tf
    │   │   ├── terragrunt.hcl // 子の定義
    │   │   └── var.tf
    │   ├── iam_role
    │   │   ├── main.tf
    │   │   ├── output.tf
    │   │   └── terragrunt.hcl // 子の定義
    │   └── terragrunt.hcl // 親の定義
    ├── provider.tf
    └── version.tf

設定値の共通化

本来であれば ./envs/dev/iam_role./envs/dev/iam_policy にそれぞれ個別でbackend関連の設定を行う必要がありますが、Terragruntを利用することで定義の共通化や実行の簡略化を行ってくれます。
Terragruntでは terragrunt.hcl というファイルを用いて設定の共通化を行います。管理する階層の一番上のterragrunt.hclを親(今回の例では ./envs/dev/terragrunt.hcl)とし、その配下の各ディレクトリに配置されたterragrunt.hclを子(今回の例では ./envs/dev/xxxx/terragrunt.hcl)としています。

// ./envs/dev/terragrunt.hcl
remote_state {
  backend = "s3"
  config = {
    bucket = "terragrunt-sample-dev-tfstate"
    // 呼び出される階層ごとに `iam_role.tfstate` や `iam_policy.tfstate` といった値になる
    key    = "${path_relative_to_include()}.tfstate"
    region = "ap-northeast-1"
    encrypt = true
  }
}

generate "provider" {
  path      = "_provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = file("../provider.tf")
}

generate "version" {
  path      = "_version.tf"
  if_exists = "overwrite_terragrunt"
  contents  = file("../version.tf")
}

generate を利用することで contents 内で指定した定義からファイルを生成してくれます。今回の場合には子である ./envs/dev/xxx 内で _provider.tf_version.tf というファイル名で作成されます。
これにより、1つの定義ファイル(コードでは ./envs/provider.tf , ./envs/version.tf )をメンテナンスするだけで完結します(配布はTerragruntがやってくれる)。
https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#generate

子のterragrunt.hclでは以下のように定義しています。
include を使うことで親のterragrunt.hclを取り込むことができます。これにより、backend周りの定義を親に記載するだけで済みます。

// envs/dev/iam_role
include "root" {
  path = find_in_parent_folders()
}

依存管理

今回は ./envs/dev/iam_policy./envs/dev/iam_role に対して依存しています。
単純な実行順序の依存の制御の場合には以下のように依存している側のmodule内のterragrunt.hclで dependencies を利用します。
https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#dependencies

// ./envs/dev/iam_policy/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

dependencies = {
  paths = ["../iam_role"]
}

実行順序の制御に加えてmodule間の値の受け渡しも行いたい場合には、以下のように依存している側のmodule内のterragrunt.hclで dependency を利用します。
https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#dependency

// ./envs/dev/iam_policy/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

dependency "iam_role" {
  config_path = "../iam_role"
  // plan時などには`../iam_role`から値が分かってこないのでmockを利用しておくことで型としての埋め合わせができます
  mock_outputs = {
    iam_role_name = "iam-role-name"
  }
}

inputs = {
  iam_role_name = dependency.iam_role.outputs.iam_role_name
}

./envs/dev/iam_role 側では iam_policy に値を渡すためにoutput定義を行います。

// ./envs/dev/iam_role/output.tf
output "iam_role_name" {
  value = aws_iam_role.sample_role.name
}

module間の依存関係は terragrunt graph-dependencies コマンドを実行することで確認できます。

$ terragrunt graph-dependencies
digraph {
	"iam_policy" ;
	"iam_policy" -> "iam_role";
	"iam_role" ;
}

dependencies/dependencyを定義した状態で terragrunt run-all xxxx を実行することで、Terragruntが依存関係を解決した上で全てのリソースに対してコマンドを実行してくれます。
terragrunt run-all plan の場合には ./envs/dev 配下の全てのリソースに対するplanが行われます。

今回の場合には以下のように iam_role -> iam_policy の順番で実施されます。

terragrunt run-all planの結果
INFO[0000] The stack at /terragrunt_sample/envs/dev will be processed in the following order for command plan:
Group 1
- Module /terragrunt_sample/envs/dev/iam_role

Group 2
- Module /terragrunt_sample/envs/dev/iam_policy


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:

  # aws_iam_role.sample_role will be created
  + resource "aws_iam_role" "sample_role" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.amazonaws.com"
                        }
                      + Sid       = ""
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "terragrunt-sample-role"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + tags_all              = (known after apply)
      + unique_id             = (known after apply)

      + inline_policy {
          + name   = (known after apply)
          + policy = (known after apply)
        }
    }

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

Changes to Outputs:
  + iam_role_name = "terragrunt-sample-role"

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

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.

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:

  # aws_iam_policy.sample_policy will be created
  + resource "aws_iam_policy" "sample_policy" {
      + arn       = (known after apply)
      + id        = (known after apply)
      + name      = "terragrunt-sample-policy"
      + path      = "/"
      + policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = "Get*"
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = ""
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + policy_id = (known after apply)
      + tags_all  = (known after apply)
    }

  # aws_iam_role_policy_attachment.sample_policy will be created
  + resource "aws_iam_role_policy_attachment" "sample_policy" {
      + id         = (known after apply)
      + policy_arn = (known after apply)
      + role       = "iam-role-name"
    }

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

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

いい感じに依存関係を解決してくれてますね。

その他TIPS

動的に値を設定する

以下のように環境変数などでpathを切り替えられるようにすることで、より柔軟に値を設定することができるようになります(用法用量を守って正しく使う必要はありますが)。

// ./envs/dev/terragrunt.hcl
locals {
  backend_config = yamldecode(file("../backend/${get_env("BACKEND_CONFIG_TYPE")}/backend_config.yaml"))
}

remote_state {
  backend = "s3"

  config = {
    bucket               = local.backend_config.bucket
    key                  = local.backend_config.key
    region               = local.backend_config.region
    profile              = local.backend_config.profile
    workspace_key_prefix = local.backend_config.workspace_key_prefix
  }
}

実行時に引数を渡す

extra_arguments を利用することでTerragruntコマンド実行の際に自動で指定した引数を付与することができます。

// ./envs/dev/terragrunt.hcl
terraform {
  extra_arguments "common_vars" {
    commands = get_terraform_commands_that_need_vars()

    arguments = [
      "-var-file=./backend/${get_env("BACKEND_CONFIG_TYPE")}/provider.tfvars",
    ]
  }
}

workspacesを併用する

Terraform Workspacesを利用したい場合には、Terragruntの Hooks を利用するのが良さそうです。
Hooksにはbefore/afterの2種類があり、それぞれterraformコマンドを実施する直前/直後に指定したコマンドを実行してくれます。
TerragruntではTerraform Workspacesを直接サポートしているわけではないため、このようにするのが塩梅かなと思っています。

https://github.com/gruntwork-io/terragrunt/issues/1581

// ./envs/dev/terragrunt.hcl
terraform {
  before_hook "workspace" {
    commands = ["plan", "state", "apply", "destroy"]
    execute  = ["terraform", "workspace", "select", get_env("WORKSPACE")]
  }
}

Terragrunt組み込み関数

TerragruntにはTerraformにはない組み込み関数があるため、覗いてみると良いものがあるかもしれません。
https://terragrunt.gruntwork.io/docs/reference/built-in-functions/

参考

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

https://dev.classmethod.jp/articles/terragrunt-makes-your-terraform-backend-code-dry/

https://kazuhira-r.hatenablog.com/entry/2021/02/20/234927

https://zoo200.net/manage-terragrunt-with-tgenv/

Discussion