Terragruntでより幸せなTerraform生活を目指す
これは何?
TerraformをリファクタリングするにあたってTerragruntを利用できないか検証する機会があったので、簡単なサンプルを作ってゆるくまとめました。
Terragrunt is 何?
Terragruntは Gruntwork.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にすることができる
- providerやbackend関連の定義は
- tfstateの異なるmodule間の実行依存を解決する
-
./envs/dev/iam
,./envs/dev/elb
などには本来依存関係があり(例:IAMリソースを作成してからELBリソースを作成する必要がある)、素のTerraformでは実行順序を工夫する必要があります - Terragruntが提供する機能を使うことで依存関係を解決した上でplan/applyなどを実行できるようになります
-
使ってみよう
習うより慣れろ!というわけで実際のコードを使ってみていきましょう。
Terragruntのinstall
公式の出している手順に沿ってポチポチするとTerragruntをinstallできます。
tfenvよろしく、Terragruntにもversion管理ツールとしてtgenvというものもあるので、そちらを使うのもアリかもしれません。
ソースコードの概要
今回は以下のGitHub Repositoryを使って動作確認をしてみます。
あまり良い例(かつ、手軽に用意できるもの)がシュッと思いつかなかったので少し無理やりですが、以下のような構成のリソースを用意しました。
./envs/dev
配下に iam_role
と iam_policy
が存在し、 iam_role
が iam_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がやってくれる)。
子の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
を利用します。
// ./envs/dev/iam_policy/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
dependencies = {
paths = ["../iam_role"]
}
実行順序の制御に加えてmodule間の値の受け渡しも行いたい場合には、以下のように依存している側のmodule内のterragrunt.hclで 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を直接サポートしているわけではないため、このようにするのが塩梅かなと思っています。
// ./envs/dev/terragrunt.hcl
terraform {
before_hook "workspace" {
commands = ["plan", "state", "apply", "destroy"]
execute = ["terraform", "workspace", "select", get_env("WORKSPACE")]
}
}
Terragrunt組み込み関数
TerragruntにはTerraformにはない組み込み関数があるため、覗いてみると良いものがあるかもしれません。
参考
Discussion