TerragruntでTerraformのbackend周りのコードをDRYにする

Terragruntを使えば、TerraformのコードをDRY(Don't Repeat Yourself、つまりコードの重複を減らす、無くす)にすることができ、より保守性の高いコードにすることができます。今回はbackend設定周りのコードをDRYにする方法をご紹介します。
2020.01.14

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

大阪オフィスのかずえです。みなさんTerraform使ってますか?

私はTerraformが好きですが、ちょっと使いづらいなと感じている箇所があるのも事実です。その一つが同じようなコードを何度も書く必要があるところ。

TerragruntというTerraformのラッパーツールを使うことで、TerraformのコードをDRY(Don't Repeat Yourself、つまりコードの重複を減らす、無くす)にすることができることを知りましたので、ご紹介します。

今回扱う環境

以前書いた「Terraform初心者が実戦投入するまでにやったこと」でご紹介したディレクトリ構成でTerraformを使っていると仮定して、その問題点をTerragruntを使って解決したいと思います。

ディレクトリ構成はこんな感じです。複数環境存在し、かつ各環境内でもリソースごとにディレクトリを切ってStateを分けています。

.
├── environment
│   ├── local-pjxxx
│   │   ├── .envrc
│   │   ├── README.md
│   │   ├── alb
│   │   │   ├── main.tf
│   │   │   ├── output.tf
│   │   │   └── variables.tf
│   │   ├── ec2
│   │   │   ├── main.tf
│   │   │   ├── output.tf
│   │   │   └── variables.tf
│   │   ├── security_group
│   │   │   ├── main.tf
│   │   │   ├── output.tf
│   │   │   └── variables.tf
│   │   ├── その他のリソース
│   │   ├── ・
│   │   └── ・
│   ├── production-pjxxx
│   │   ├── ・
│   │   └── ・
│   └── staging-pjxxx
│       ├── ・
│       └── ・
└── modules
    ├── alb
    │   ├── main.tf
    │   ├── output.tf
    │   └── variables.tf
    ├── ec2
    │   ├── main.tf
    │   ├── output.tf
    │   ├── userdata.txt
    │   └── variables.tf
    ├── security_group
    │   ├── main.tf
    │   ├── output.tf
    │   └── variables.tf
    ├── その他のリソース
    ├── ・
    └── ・
  • terraform apply などを行なうディレクトリは environment/local-pjxxx/alb などです。
  • 具体的なコードはmodule以下に書きます。それをenvironment下のファイルから呼び出します。
    例えば environment/local-pjxxx/albmain.tf../../../modules/alb を呼びます。環境ごとにパラメーターが変わる部分はモジュール内で変数化しておき、呼び出す際に値を指定するようにします。

問題点

色々あるのですが、今回扱うのはbackend設定周りについてです。

environment/local-pjxxx/albenvironment/local-pjxxx/ec2 などの各ディレクトリごとにbackendの設定が必要になってきます。これが問題です。ほぼ同じコードが複数個書かれることになります。

例えばenvironment/local-pjxxx/alb 以下のいずれかの.tf ファイルにこのようなコードが必要です。

terraform {
  required_version = "~> 0.12"
  backend "s3" {
    bucket  = "kazue-hogehoge-backend"
    key     = "alb.tfstate"
    region  = "ap-northeast-1"
    encrypt = true
  }
}

さらに、environment/local-pjxxx/ec2 以下にもほぼ同様のコードが必要になります。違うのはkey値だけです。

terraform {
  required_version = "~> 0.12"
  backend "s3" {
    bucket  = "kazue-hogehoge-backend"
    key     = "ec2.tfstate"
    region  = "ap-northeast-1"
    encrypt = true
  }
}

以下のように変数を使うことができれば、各ディレクトリでまったく同じコードを使うことができます。がbackendにおいては変数を扱うことができません。(さらにはこのコードブロックをmodule化したくなりますが、これもできません)

// これはエラー
terraform {
  required_version = "~> 0.12"
  backend "s3" {
    bucket  = var.backend-bucket
    key     = var.backend-key
    region  = var.backend-region
    encrypt = var.backend-encrypt
  }
}

というわけで、Stateを分けた数だけ、ほぼ同じbackendの設定が書かれることになります。

Terragruntで解決

Terragruntを使うとこのコード重複を解消することができます。具体的にはひとつ上のディレクトリ(environment/local-pjxxx)で共通設定を定義し、各Stateでそれを参照できるようにします。

方針

以下のようなディレクトリ構成にします。 terragrunt.hclというファイルが複数個増えています。

.
├── environment
│   ├── local-pjxxx
│   │   ├── terragrunt.hcl
│   │   ├── .envrc
│   │   ├── README.md
│   │   ├── alb
│   │   │   ├── main.tf
│   │   │   ├── output.tf
│   │   │   ├── variables.tf
│   │   │   └── terragrunt.hcl
│   │   ├── ec2
│   │   │   ├── main.tf
│   │   │   ├── output.tf
│   │   │   ├── variables.tf
│   │   │   └── terragrunt.hcl

terragrunt.hcl はTerragruntの設定ファイルです。

  • 親ディレクトリ(environment/local-pjxxx/)に置いたterragrunt.hcl にbackendの設定を書きます。
  • 子ディレクトリ(environment/local-pjxxx/alb,environment/local-pjxxx/ec2 など)に置いたterragrunt.hcl が、親ディレクトリのterragrunt.hclの情報を継承するするようにします。

以下、具体的な手順です。

1. インストール

上記に書かれていますが、Macの場合 brew install terragrunt でインストールできます。

が、私の環境では tfenv と競合したので、以下コマンドを使いました。(情報参照元: Comment on #580 terragrunt conflicts with tfenv)

$ brew install --ignore-dependencies terragrunt

2. Terraformコマンドを実行する各ディレクトリに terragrunt.hcl を設置

terragrunt.hcl の書式はTerraformと同じくHCLです。

environment/local-pjxxx/alb/terragrunt.hcl

include {
  path = find_in_parent_folders()
}

find_in_parent_folders() を使うことで親ディレクトリに存在するTerragrunt設定ファイルterragrunt.hcl を探し、その設定を継承します。

3. ひとつ上のディレクトリに terragrunt.hcl を設置

共通のbackend設定を記述します。

environment/local-pjxxx/terragrunt.hcl

remote_state {
  backend = "s3"
  config = {
    bucket  = "kazue-hogehoge-backend"
    key     = "${path_relative_to_include()}.tfstate"
    region  = "ap-northeast-1"
    encrypt = true    
  }
}

path_relative_to_include()は、Stateの存在するパス(environment/local-pjxxx/alb)と、先程 path で定義したディレクトリパス(environment/local-pjxxx)間の相対パスを返します。つまりこの場合albです。

4. buckendについてのTerraformコードを更新

backendの設定はterragruntに委譲されるので、空にします。

environment/local-pjxxx/alb/main.tf

terraform {
  required_version = "~> 0.12"
-  backend "s3" {
-    bucket  = "kazue-hogehoge-backend"
-    key     = "alb.tfstate"
-    region  = "ap-northeast-1"
-    encrypt = true
-  }
+  backend "s3" {}   
}

5. terragrunt実行

実行するコマンドはterraform ではなく terragruntになります。terragruntコマンドは、ここまで設定したTerragruntの設定を反映した上でterraformコマンドを実行します。(つまりterraformのラッパーコマンドです)

$ cd environment/local-pjxxx/alb/
$ terragrunt apply

このようにすることで、backendに関する設定はenvironment/local-pjxxx/terragrunt.hcl に一度書かれるだけにすることができ、各Stateディレクトリでは backend "s3" {}.tfコードと、前述のterragrunt.hclファイルを設置するだけでよくなります。

他のStateを参照する部分もDRYにする

別のStateで定義したリソースの情報を違うStateでも参照したい場合があります。例えばVPCとセキュリティグループを分けて作成していた場合、セキュリティグループリソースを作成する際にVPCのIDが必要です。その際Data Recourceのremote_stateが必要になります。コード例は以下です。

environment/local-pjxxx/security-group/variables.tf

data "terraform_remote_state" "vpc" {
  backend = "s3"

  config = {
    bucket = "kazue-hogehoge-backend"
    key    = "vpc.tfstate"
    region = "ap-northeast-1"
  }
}

ここにもbackendの具体的な設定がありますね。先程親ディレクトリで定義した設定を参照するようにしたいですね。やっていきましょう。

方針としては、親ディレクトリのTerragrunt設定ファイルterragrunt.hcl に書かれたbuckend設定情報を一度外部ファイルに外出しして、元のterragrunt.hcl も、子ディレクトリのTerraformもその情報を参照する構成にします。
(外出しせず親ディレクトリのterragrunt.hclにあるbackend設定を小ディレクトリのTerraformに渡すこともできるのですが、コード量が多くなったのでやめました)

1. backendの設定情報を外出しする

設定情報をYAMLファイルにします。JSONファイルでも構いません。

environment/local-pjxxx/backend-config.yaml

bucket: "kazue-hogehoge-backend"
region: "ap-northeast-1"
encrypt: true

2. 親ディレクトリの terragrunt.hclが、上記設定情報を参照するようにする

environment/local-pjxxx/terragrunt.hcl

+ locals {
+   backend-config = yamldecode(file("${get_terragrunt_dir()}/${find_in_parent_folders("backend-config.yaml")}"))
+ }

remote_state {
  backend = "s3"
  config = {
+     bucket  = local.backend-config.bucket
-     bucket  = "kazue-hogehoge-backend"
      key     = "${path_relative_to_include()}.tfstate"
+     region  = local.backend-config.region
-     region  = "ap-northeast-1"
+     encrypt = local.backend-config.encrypt
-     encrypt = true    
  }
}

3. 小ディレクトリのTerraformが上記設定情報を読み込む

environment/local-pjxxx/security-group/variables.tf

+ locals {
+   backend-config = yamldecode(file("../backend-config.yaml"))
+ }

4. remote_stateの定義で参照

environment/local-pjxxx/security-group/variables.tf

data "terraform_remote_state" "vpc" {
  backend = "s3"

  config = {
+    bucket = local.backend-config.bucket
-    bucket = "kazue-hogehoge-backend"
     key    = "vpc.tfstate"
+    region = local.backend-config.region
-    region = "ap-northeast-1"
  }
}

まとめ

TerragruntでTerraformのbackend周りのコードをDRYにする方法をご紹介しました。痒いところに手が届くサービスで非常に気に入りました。Terragruntは、backend周りのコードだけでなく他の箇所のTerraformのコードもDRYに、メンテしやすくすることができますので、今後ご紹介していきます。

参考情報