CLOVER🍀

That was when it all began.

Terragruntを使って、環境ごとのTerraformの構成ファイルを削減する

これは、なにをしたくて書いたもの?

Terragruntを学ぶシリーズ。

今回は、Terragruntを使って環境ごとに作るTerraformの構成ファイルを減らしてみます。

Terragruntで、環境ごとの構成ファイルを減らす

Terraformを使って、複数の環境を構築、管理するのにはいくつかの方法があります。

Terraform自身が提供するのは、Workspaceですね。

State: Workspaces - Terraform by HashiCorp

WorkspaceはStateを切り替えることができるもので、こちらとVariablesなどで環境差異を表現することになります。

他には、割り切って環境ごとにTerraform構成ファイルを独立して管理する方法もあります。この方法だと、ファイルのコピーが増えることに
なりますね。

ちょうど、今回使うTerragruntのドキュメントにも例があります。

Quick start / Promote immutable, versioned Terraform modules across environments

こういう感じのものです。環境ごとにファイルが分離されていますし、Stateもそれぞれ独立して管理されます。

├── 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の場合では、環境ごとにファイルを独立させる考えを取りつつも、リソース定義はモジュール化して参照する方法を提示しています。
さらに、モジュールに対するVariableの設定をTerraformの構成ファイルなしで実現できます。

そのあたりが書かれているのが、こちらのドキュメントです。

Quick start / Promote immutable, versioned Terraform modules across environments

Keep your Terraform code DRY

Terragruntを使うと、リソース定義をモジュールにまとめた上で、こういった形で.tfファイルなしで表現できます。

├── 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

今回は、こちらの機能を見ていこうと思います。

環境

今回の環境は、こちらです。

$ terraform version
Terraform v0.14.7


$ terragrunt -v
terragrunt version v0.28.7

Terraform Providerは、MySQL用のものを使用します。

Provider: MySQL - Terraform by HashiCorp

使用するMySQLは8.0.23とし、172.17.0.2および172.17.0.3で動作しているものとします。

また、今回はGitリポジトリが必要になります。Gitリポジトリのソフトウェアはあまり関係ありませんが、GitLab 13.9を使います。
GitLabは192.168.0.3で動作しているものとします。

お題

MySQL Providerを使った、以下の2つのモジュールを定義します。

  • データベース
  • ユーザーおよび権限

これらを環境別のディレクトリから参照し、それぞれ使用するモジュールは同じ、構築する情報(=Variable)は異なる状態を実現してみます。

環境はdevelopmentとproductionの2つとし、リソース構築先のMySQLはdevelopment用を172.17.0.2、production用を172.17.0.3とします。

また、今回はTerragruntを使わないパターンは作りません。長くなるので…。

では、始めていきましょう。

Terraformモジュールを作成する

今回は、Terraformモジュールがないと始まりません。

GitLab上にリポジトリを作成して(名前はmysql-terraform-moduleにしました)、こちらのモジュールを登録します。

git cloneして

$ git clone http://192.168.0.3/kazuhira/mysql-terraform-module.git
$ cd mysql-terraform-module

モジュール用のディレクトリを作成。

$ mkdir -p modules/{database,users}

以下のような構成にします。

$ tree
.
└── modules
    ├── database
    │   ├── main.tf
    │   ├── outputs.tf
    │   ├── variables.tf
    │   └── versions.tf
    └── users
        ├── main.tf
        ├── outputs.tf
        ├── variables.tf
        └── versions.tf

3 directories, 8 files

ファイルは、それぞれこんな感じです。

データベース用。

modules/database/main.tf

resource "mysql_database" "this" {
  name                  = var.database_name
  default_character_set = var.default_character_set
  default_collation     = var.default_collation
}

必須なのは、データベース名だけにしました。

modules/database/variables.tf

variable "database_name" {
  type = string
}

variable "default_character_set" {
  type    = string
  default = "utf8mb4"
}

variable "default_collation" {
  type    = string
  default = "utf8mb4_ja_0900_as_cs_ks"
}

modules/database/outputs.tf

output "database_name" {
  value = mysql_database.this.name
}

modules/database/versions.tf

terraform {
  required_version = ">= 0.14.7"

  required_providers {
    mysql = {
      source  = "terraform-providers/mysql"
      version = ">= 1.9.0"
    }
  }
}

ユーザーおよび権限用。

管理用のユーザーと、アプリケーションユーザーの2種類を作成します。

modules/users/main.tf

resource "mysql_user" "administrator" {
  user               = var.administrator_username
  plaintext_password = var.administrator_password
  host               = var.administrator_allow_host
}

resource "mysql_grant" "administrator" {
  user       = mysql_user.administrator.user
  host       = mysql_user.administrator.host
  database   = var.database_name
  privileges = ["ALL"]
}

resource "mysql_user" "application_user" {
  user               = var.application_user_username
  plaintext_password = var.application_user_password
  host               = var.application_user_allow_host
}

resource "mysql_grant" "application_user" {
  user       = mysql_user.application_user.user
  host       = mysql_user.application_user.host
  database   = var.database_name
  privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

必須のものは、データベース名と、各ユーザーのパスワードにしました。

modules/users/variables.tf

variable "database_name" {
  type = string
}

variable "administrator_username" {
  type    = string
  default = "admin"
}

variable "administrator_password" {
  type = string
}

variable "administrator_allow_host" {
  type    = string
  default = "%"
}

variable "application_user_username" {
  type    = string
  default = "appuser"
}

variable "application_user_password" {
  type = string
}

variable "application_user_allow_host" {
  type    = string
  default = "%"
}

modules/users/outputs.tf

output "administrator_username" {
  value = mysql_user.administrator.user
}

output "administrator_password" {
  value     = mysql_user.administrator.plaintext_password
  sensitive = true
}

output "administrator_allow_host" {
  value = mysql_user.administrator.host
}

output "application_user_username" {
  value = mysql_user.application_user.user
}

output "application_user_password" {
  value     = mysql_user.application_user.plaintext_password
  sensitive = true
}

output "application_user_allow_host" {
  value = mysql_user.application_user.host
}

modules/users/versions.tf

terraform {
  required_version = ">= 0.14.7"

  required_providers {
    mysql = {
      source  = "terraform-providers/mysql"
      version = ">= 1.9.0"
    }
  }
}

作成したモジュールを、Gitリポジトリに登録します。

$ git add modules
$ git commit -m 'add, modules'
$ git push origin master

タグもつけておきましょう。

$ git tag v0.0.1
$ git push origin v0.0.1

ここまでは、単にTerraformモジュールを作成しただけですね。

環境別にモジュールを利用し、かつTerraform構成ファイルは作成しない

ここから、Terragruntが出てきます。

まずは環境用のディレクトリと、各モジュールを使うためのディレクトリを作成しましょう。

$ mkdir -p environments/{development,production}
$ mkdir -p environments/development/{database,users}
$ mkdir -p environments/production/{database,users}

最終的に、できあがったディレクトリおよびファイルは、こんな感じです。いずれのディレクトリにもファイルはterragrunt.hclしか
存在しません。

$ tree environments
environments
├── development
│   ├── database
│   │   └── terragrunt.hcl
│   ├── terragrunt.hcl
│   └── users
│       └── terragrunt.hcl
└── production
    ├── database
    │   └── terragrunt.hcl
    ├── terragrunt.hcl
    └── users
        └── terragrunt.hcl

6 directories, 6 files

developmentと名付けた方の環境から見ていきましょう。

$ cd environments/development

このディレクトリにあるterragrunt.hclファイルの中身は、こんな感じです。

terragrunt.hcl

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}
EOF
}

Providerの定義をまとめる機能を使っています。データベース用、ユーザーおよび権限用のモジュールがありますが、接続先のMySQLサーバーは
どちらも同じなので。

Terragruntを使って、TerraformのProvider定義をまとめてみる - CLOVER🍀

データベース側に移動します。

$ cd database

こちらに配置しているterragrunt.hclの中身です。

terragrunt.hcl

include {
  path = find_in_parent_folders()
}

terraform {
  source = "git::http://192.168.0.3/kazuhira/mysql-terraform-module.git//modules/database?ref=v0.0.1"
}

inputs = {
  database_name = "development_database"
}

Providerの定義を参照するために、上位ディレクトリにあるterragrunt.hclファイルを参照するようにしています。

include {
  path = find_in_parent_folders()
}

terraformブロックのsource属性は、このディレクトリで使うモジュールを指定します。

terraform {
  source = "git::http://192.168.0.3/kazuhira/mysql-terraform-module.git//modules/database?ref=v0.0.1"
}

Configuration Blocks and Attributes / terraform

記述方法自体は、通常のTerraformと同じですね。//以下で、Gitリポジトリ内のモジュールを指定しています。タグはrefで指定します。

Module Sources - Terraform by HashiCorp

モジュールに与えるVariableは、inputsで指定します。

inputs = {
  database_name = "development_database"
}

Configuration Blocks and Attributes / inputs

ここまでで、このディレクトリについては準備ができたので、applyしてみましょう。

$ terragrunt apply

リソースができました。

mysql_database.this: Creating...
mysql_database.this: Creation complete after 0s [id=development_database]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

database_name = "development_database"

ちなみに、この時のディレクトリ内のファイルやディレクトリは、このようになっています。

$ ll
合計 20
drwxrwxr-x 3 xxxxx xxxxx 4096  2月 24 00:05 ./
drwxrwxr-x 4 xxxxx xxxxx 4096  2月 23 22:05 ../
-rw-r--r-- 1 xxxxx xxxxx 1194  2月 24 00:05 .terraform.lock.hcl
drwxrwxr-x 3 xxxxx xxxxx 4096  2月 24 00:05 .terragrunt-cache/
-rw-rw-r-- 1 xxxxx xxxxx  218  2月 23 23:15 terragrunt.hcl

続いて、ユーザーおよび権限側へ。

$ cd ../users

terragrunt.hclは、こちら。使い方自体は、データベース側と同じです。

terragrunt.hcl

include {
  path = find_in_parent_folders()
}

terraform {
  source = "git::http://192.168.0.3/kazuhira/mysql-terraform-module.git//modules/users?ref=v0.0.1"
}

inputs = {
  database_name = "development_database"

  administrator_password    = "admin_password"
  application_user_password = "appuser_password"
}

applyしておきましょう。

$ terragrunt apply

リソースができました。

mysql_user.administrator: Creating...
mysql_user.application_user: Creating...
mysql_user.application_user: Creation complete after 0s [id=appuser@%]
mysql_grant.application_user: Creating...
mysql_user.administrator: Creation complete after 0s [id=admin@%]
mysql_grant.administrator: Creating...
mysql_grant.application_user: Creation complete after 0s [id=appuser@%:`development_database`]
mysql_grant.administrator: Creation complete after 0s [id=admin@%:`development_database`]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

administrator_allow_host = "%"
administrator_password = <sensitive>
administrator_username = "admin"
application_user_allow_host = "%"
application_user_password = <sensitive>
application_user_username = "appuser"

production側の内容も見てみましょう。くどくなるので、applyの実行の様子は載せません。

$ cd ../../production

Providerの定義をしている、terragrunt.hcl。developmentの時とは、接続先が異なります。

terragrunt.hcl

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "mysql" {
  endpoint = "172.17.0.3:3306"
  username = "root"
  password = "password"
}
EOF
}

データベース側。

database/terragrunt.hcl

include {
  path = find_in_parent_folders()
}

terraform {
  source = "git::http://192.168.0.3/kazuhira/mysql-terraform-module.git//modules/database?ref=v0.0.1"
}

inputs = {
  database_name = "production_database"
}

ユーザーおよび権限側。

users/terragrunt.hcl

include {
  path = find_in_parent_folders()
}

terraform {
  source = "git::http://192.168.0.3/kazuhira/mysql-terraform-module.git//modules/users?ref=v0.0.1"
}

inputs = {
  database_name = "production_database"

  administrator_password    = "rZRuLj0xx2Z1M"
  application_user_password = "gSudLAJt4icZL"
}

これで、同じモジュールを使って、リソースを構築するための設定が異なるという状態を、Terraform構成ファイルなしで実現できました。

Stateやprovider.tfは?

developmentのデータベース側の定義に戻ってみます。

$ cd ../../development/database

先ほどTerragruntを使ってapplyした時にディレクトリを、provider.tfがありませんでした。今回はTerraform Backendを特に設定して
いないので、Stateがファイルで管理されているはずなのですが、それもありません。

$ ll
合計 20
drwxrwxr-x 3 xxxxx xxxxx 4096  2月 24 00:05 ./
drwxrwxr-x 4 xxxxx xxxxx 4096  2月 23 22:05 ../
-rw-r--r-- 1 xxxxx xxxxx 1194  2月 24 00:05 .terraform.lock.hcl
drwxrwxr-x 3 xxxxx xxxxx 4096  2月 24 00:05 .terragrunt-cache/
-rw-rw-r-- 1 xxxxx xxxxx  218  2月 23 23:15 terragrunt.hcl

provider.tfについては、親ディレクトリにあるterragrunt.tfの内容からして、こちらのディレクトリに作成されているのでは?と思うのですが。

実は、こんなディレクトリにできていたりします。

.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/provider.tf

# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}

こういう状態です。

$ tree .terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4
└── modules
    ├── database
    │   ├── main.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── terraform.tfstate
    │   ├── terragrunt.hcl
    │   ├── variables.tf
    │   └── versions.tf
    └── users
        ├── main.tf
        ├── outputs.tf
        ├── variables.tf
        └── versions.tf

3 directories, 11 files

Stateまで、こちらに入っているんですね。

findすると、こんな感じになっています。
※ .gitディレクトリ除く

.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/.terragrunt-source-version
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/users/outputs.tf
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/users/versions.tf
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/users/main.tf
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/users/variables.tf
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/outputs.tf
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/provider.tf
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/terraform.tfstate
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/versions.tf
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/main.tf
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/terragrunt.hcl
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/.terraform.lock.hcl
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/.terragrunt-module-manifest
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/variables.tf
.terragrunt-cache/ro7akS-zy30K33w0IWWX0m8k7Jk/yKALuQR6CZfAIYeyffGdzMPkAo4/modules/database/.terraform/providers/registry.terraform.io/terraform-providers/mysql/1.9.0/linux_amd64/terraform-provider-mysql_v1.9.0_x4

実は、この構成だとterragrunt applyなどを実行する時に.terragrunt-cache配下の一時ディレクトリに移動しているようなのです。

Keep your Terraform code DRY / Important gotcha: working with relative file paths

このため、-var-fileやterragrunt.hcl内で書くパスは、絶対パスにしておかないとうまくいかないようです。

モジュールが更新された場合

こちらの内容です。

Keep your Terraform code DRY / Important gotcha: Terragrunt caching

sourceの指定をリモートにした場合、Terragruntはモジュールのダウンロードを1度しか行いません。

terraform init -upgrade、terragrunt init -upgradeなどしても、意味がありません。

モジュールを再度ダウンロードするには、--terragrunt-source-updateオプションを使用します。

$ terragrunt apply --terragrunt-source-update

ローカルのモジュールを使う場合

--terragrunt-sourceオプションを使用することで、一時的にsourceで指定しているモジュールの参照先を変更できるようです。

Keep your Terraform code DRY / Working locally

これで、ローカルのモジュールを利用して作業ができます、と。

まとめ

Terragruntを使った、環境ごとのリソース定義方法(?)を見てみました。

どうなんでしょうね、確かにすごくすっきりした構成になりますし、モジュールをリポジトリ管理して参照するのも良いと思うのですが。

terragrunt.hclを使って変更できる範囲が、ちょっと少ないような…。

環境差異がVariablesで吸収できる範囲なら、有効なのでしょうか?

Workspaceを使わない、この考え方自体は押さえておいた方がよいかなと思います。