なぜ今Terragruntを使うのか

Terragrunt: how to keep your Terraform code DRY and maintainableを見つつ個人的にまとめたものです。

Terragruntは何を解決したいのか

new problems have since cropped up: namely, how to keep your Terraform code DRY and maintainable.

「DRYで保守しやすいTerraformのコードを維持する方法が、問題として出てきた(意訳)」と書かれていることから、現在のTerragruntは、DRYで保守性の高いコードを書けるようにしようという流れの模様。

backendの設定をDRYに保つ

backendでは、変数や式をサポートしていないため、下記のように書くことができません。
そのため、値が少し違うだけでもコピペしないといけません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
terraform {
  backend "s3" {
    # Using variables does NOT work here!
    bucket         = var.terraform_state_bucket
    key            = var.terraform_state_key
    region         = var.terraform_state_region
    encrypt        = var.terraform_state_encrypt
    dynamodb_table = var.terraform_state_dynamodb_table
  }
}

terragruntでは設定ファイルを継承できるので、上記の問題を解決することができます。

下記にディレクトリ構成とファイルの内容を示します。

1
2
3
4
5
6
7
8
stage
├── terragrunt.hcl <- Terragruntを使用するために必要です。
├── frontend-app
│   ├── main.tf
│   └── terragrunt.hcl <- backendの継承で使用します。
└── mysql
    ├── main.tf
    └── terragrunt.hcl <- backendの継承で使用します。

下記のように設定することで、terragruntが自動的に親のbackendを子にも設定します。
mysqlのみ載せてありますが、frontend-appも同じになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# stage/mysql/main.tf

terraform {
  # 空にしておくことで、Terragruntによってbackendの内容が自動的に埋め込まれます。必須です。
  backend "s3" {}
}

# stage/mysql/terragrunt.hcl
include {
  path = find_in_parent_folders() // 親ディレクトリからterragrunt.hclを探してそのパスを返す関数です。今回の場合にはstage/terragrunt.hclへのパス(../terragrunt.hcl)を返します。
}

# stage/terragrunt.hcl
remote_state {
  backend = "s3"
  config = {
    bucket = "my-terraform-state"
    
    key = "${path_relative_to_include()}/terraform.tfstate" // path_relative_to_include関数は、子モジュールへの相対パスを返します。mysqlの場合には、./mysqlが返ります。
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "my-lock-table"
  }
}

子モジュール(stage/mysql/terragrunt.hcl)が親モジュール(stage/terragrunt.hcl)の設定を参照して自動的にbackend(terraform.backend.s3)を埋めるという挙動になっています。

これにより、backendの設定をDRYに保つことができます。

Terraform CLIの引数をDRYに保つ

下記のようにterragrunt.hclに書くことで、コマンド実行時に自動的に引数を設定してくれるようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# terragrunt.hcl
terraform {
  extra_arguments "common_vars" {
    commands = ["plan", "apply"]

    arguments = [
      "-var-file=../../common.tfvars",
      "-var-file=../region.tfvars"
    ]
  }
}
1
2
3
terraform apply \
    -var-file=../../common.tfvars \
    -var-file=../region.tfvars

↕ つまり、上記のterragrunt.hclの設定を行うことで、上のコマンドと下のコマンドは等価になります。

1
terragrunt apply

また、下記のようなterragruntが用意している関数を書いて、-var-fileをすべて網羅したコマンドを列挙するということも省くことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# terragrunt.hcl
terraform {
  extra_arguments "common_vars" {
    commands = get_terraform_commands_that_need_vars() // get_terraform_commands_that_need_vars()を使用することで、-var-fileや-varを引数として使用するすべてのコマンドに対して、引数入力の省略を行うことができます。

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

環境全体でバージョン管理されたTerraformモジュールを用いることで不変なインフラを促す

terragrunt.hclには下記のようにsourceで使用するモジュールとしてgitリポジトリを使用できます。

1
2
3
4
terraform {
  source = 
    "github.com:foo/infrastructure-modules.git//app?ref=v0.0.1"
}

infrastructure-modulesリポジトリのapp/main.tfは下記のような感じです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# infrastructure-modules/app/main.tf

provider "aws" {
  region = "us-east-1"
  # ... other provider settings ...
}
terraform {
  backend "s3" {}
}
module "app" {
  source = "../../../app"
  instance_type  = var.instance_type
  instance_count = var.instance_count
  # ... other app settings ...
}
# infrastructure-modules/app/outputs.tf
output "url" {
  value = module.app.url
}
# infrastructure-modules/app/variables.tf

variable "instance_type" {}
variable "instance_count" {}

(Terraformのモジュールと等価ではない)このモジュール(infrastructure-modules/app/main.tf)には、providerやbackendを含めることができるため、呼び出し側は、下記程度のコードで済むようになり、よりDRYに書くことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
terraform {
  source = 
    "github.com:foo/infrastructure-modules.git//app?ref=v0.0.2"
}

// inputsに書いたものは、TF_VARのprefixがついた環境変数として渡されます。
inputs = {
  instance_count = 3 // TF_VAR_instance_countと同じ
  instance_type  = "t2.micro" // TF_VAR_instance_countと同じ
}

終わりに

(providerとbackendはterraformの文脈だとmoduleに切り出せないので)providerとbackendを共通化する方法どうするんだろうと思っていたけど、terraformのsourceを用いればできることがわかったのでよかった。

シンボリックリンク使ってなんとかしてたので、来週リファクタしようかと思う。

providerとbackendのようにmodule切り出しできないものは、シンボリックリンク使わないとどうにもならないので、自分の場合とりあえず当分はterragruntを使うことになりそうだなーって思いました。

以上

Built with Hugo
テーマ StackJimmy によって設計されています。