CLOVER🍀

That was when it all began.

Terragruntを使って、Terraform実行時の引数やオプション指定をまとめてみる

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

Terragruntを学ぶシリーズ。

次は、Terragruntを使ってTerraform実行時の引数やオプション指定をまとめてみます。

Terragruntで、Terraform実行時の引数やオプション指定をまとめる

Terraformを実行する時に、引数やオプションを指定することがあります。

たとえば共通の変数の値を設定したファイルを場合は、Terraformモジュールの実行ごとに-var-fileオプションを付けることに
なります。また、apply時にいつもロックのタイムアウトの設定を指定したい場合は、毎回-lock-timeoutを指定することに
なります。
※ terraform.tfvarsファイルを各モジュールディレクトリに作成すれば-var-fileの指定は要らないという話もありますが、その場合は
  似た内容のファイルがバラまかれることになりますね…

Quick start / Keep your Terraform CLI arguments DRY

Keep your CLI flags DRY

Terragruntを使うと、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で動作しているものとします。

お題

MySQL Providerを使い、以下のリソースを定義する2つのTerraformルートモジュールを作成します。

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

この時に指定するProviderの定義やデータベース名を変数化し、これを指定するオプションをまとめてみましょう。
※ Terragruntを使うとProviderの定義をまとめることができますが、今回はオプションの話に絞ります

最初はTerragruntを使わずにTerraformで毎回オプションを指定し、その後にTerragruntで指定するオプションをまとめてみましょう。

Terragruntを使わない場合

まずは、Terragruntを使わずにベースを作っていきます。

データベース、ユーザーおよび権限用に2つのディレクトリを作成。

$ mkdir database users

今回は、こういう感じの構成にしました。

$ tree
.
├── database
│   ├── main.tf
│   └── variables.tf
├── database.tfvars
├── provider.tfvars
└── users
    ├── main.tf
    └── variables.tf

2 directories, 6 files

MySQL Providerを使うので接続先の定義が必要なのですが、これはtfvarsファイルにまとめておきます。

provider.tfvars

mysql_provider_endpoint = "172.17.0.2:3306"
mysql_provider_username = "root"
mysql_provider_password = "password"

また、作成するデータベース、権限設定に関するデータベース名もtfvarsファイルにまとめておきましょう。

database.tfvars

database_name = "my_database"

データベース側のモジュール定義に移動します。

$ cd database

リソース定義は、こんな感じで用意。Provider定義やデータベース名は変数化しています。

main.tf

terraform {
  required_version = "0.14.7"

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

provider "mysql" {
  endpoint = var.mysql_provider_endpoint
  username = var.mysql_provider_username
  password = var.mysql_provider_password
}

resource "mysql_database" "app" {
  name                  = var.database_name
  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

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

変数定義。

variables.tf

variable "mysql_provider_endpoint" {
  type = string
}

variable "mysql_provider_username" {
  type = string
}

variable "mysql_provider_password" {
  type = string
}

variable "database_name" {
  type = string
}

initして

$ terraform init

上位のディレクトリにあるtfvarsファイルを指定してapply。

$ terraform apply -var-file=../provider.tfvars -var-file=../database.tfvars

リソースができました。

mysql_database.app: Creating...
mysql_database.app: Creation complete after 0s [id=my_database]

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

Outputs:

database_name = "my_database"

ユーザーおよび権限側。

$ cd ../users

こちらのリソース定義は、こんな感じです。データベース定義と同じく、Provider定義とデータベース名に関する部分が変数に
なっています。

main.tf

terraform {
  required_version = "0.14.7"

  required_providers {

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

provider "mysql" {
  endpoint = var.mysql_provider_endpoint
  username = var.mysql_provider_username
  password = var.mysql_provider_password
}

resource "mysql_user" "admin_user" {
  user               = "admin"
  plaintext_password = "password"
  host               = "%"
}

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

resource "mysql_user" "application_user" {
  user               = "appuser"
  plaintext_password = "password"
  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"]
}

output "admin_user_name" {
  value = mysql_user.admin_user.user
}

output "admin_user_privileges" {
  value = mysql_grant.admin_user.privileges
}

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

output "app_user_privileges" {
  value = mysql_grant.application_user.privileges
}

変数定義は、データベース定義の時と同じですね。

variables.tf

variable "mysql_provider_endpoint" {
  type = string
}

variable "mysql_provider_username" {
  type = string
}

variable "mysql_provider_password" {
  type = string
}

variable "database_name" {
  type = string
}

initして

$ terraform init

やっぱりtfvarsファイルを指定してapply。

$ terraform apply -var-file=../provider.tfvars -var-file=../database.tfvars

リソースが作成されました。

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

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

Outputs:

admin_user_name = "admin"
admin_user_privileges = toset([
  "ALL",
])
app_user_name = "appuser"
app_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])

ここまでで、1度リソースを破棄して上位ディレクトリに戻りましょう。destroy時も、tvfarsの指定が必要ですね。

$ terraform destroy -var-file=../provider.tfvars -var-file=../database.tfvars
$ cd ../database
$ terraform destroy -var-file=../provider.tfvars -var-file=../database.tfvars
$ cd ..

Terragruntを使って、オプション指定をまとめる

ここまでで、applyやdestroy時に同じオプションを毎回指定してきました。Provider定義のように、共通に使うものもあるので
こういうのは確実に同じオプションを複数のTerraformモジュールで指定することになりますね。

で、Terragruntを使ってこういうオプション指定をまとめるには、terragrunt.hclファイルを作成して、以下のように記述します。

terragrunt.hcl

terraform {
  extra_arguments "common_vars" {
    commands = get_terraform_commands_that_need_vars()

    arguments = [
      "-var-file=../provider.tfvars",
      "-var-file=../database.tfvars"
    ]
  }
}

terraformブロック内に、extra_argumentsというブロックを定義します。

Blocks / terraform

extra_argumentsは、TerraformのCLIに指定する引数を定義するためのブロックです。

引数やオプション以外にも、環境変数の設定、必須となるtfvarsファイルやオプションの(存在すれば使う)のtfvarsファイルの
設定などができるようです。

get_terraform_commands_that_need_varsというのは、Terragruntの組み込み関数です。Variablesを使う可能性があるコマンドを
返します。

Built-in functions / get_terraform_commands_that_need_vars

定義はこちら。

https://github.com/gruntwork-io/terragrunt/blob/v0.28.7/config/config_helpers.go#L113

https://github.com/gruntwork-io/terragrunt/blob/v0.28.7/config/config_helpers.go#L38-L47

つまり、こちらの定義は

    commands = get_terraform_commands_that_need_vars()

以下と同義になります。

    commands = ["apply", "console", "destroy", "import", "plan", "push", "refresh"]

今回は-var-fileのみの指定にしていますが、他にオプションを指定したり、コマンドごとに細かく分けたりといった例が
ドキュメントに書かれています。

Keep your CLI flags DRY

次に、データベースのモジュール定義へ移動。

$ cd database

terragrunt.hclファイルを作成します。内容は、上位ディレクトリにあるterragrunt.hclを参照するだけです。

terragrunt.hcl

include {
  path = find_in_parent_folders()
}

これで、terragruntコマンドを使う場合はオプション指定なしでplanなどが実行できるようになります。

$ terragrunt plan

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # mysql_database.app will be created
  + resource "mysql_database" "app" {
      + default_character_set = "utf8mb4"
      + default_collation     = "utf8mb4_ja_0900_as_cs_ks"
      + id                    = (known after apply)
      + name                  = "my_database"
    }

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

Changes to Outputs:
  + database_name = "my_database"

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

tfvarsで定義した内容が補完されていますね。つまり、以下と同義の状態になっています。

$ terraform plan -var-file=../provider.tfvars -var-file=../database.tfvars

同様にapplyもオプション指定なしで実行できます。

$ terragrunt apply

リソースが作成できました。

mysql_database.app: Creating...
mysql_database.app: Creation complete after 0s [id=my_database]

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

Outputs:

database_name = "my_database"

ユーザーや権限を定義したモジュールへ移動。

$ cd ../users

こちらも、terragrunt.hclファイルを作成。

terragrunt.hcl

include {
  path = find_in_parent_folders()
}

データベース定義の時と同じように、オプション指定なしでapplyできるようになります。

$ terragrunt apply

リソースができました。

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

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

Outputs:

admin_user_name = "admin"
admin_user_privileges = toset([
  "ALL",
])
app_user_name = "appuser"
app_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])

destroyもオプションが要らなくなります。

$ terragrunt destroy
$ cd ../database
$ terragrunt destroy
$ cd ..

これで、Terragruntを使ってオプション指定をまとめることが確認できました。

複数のextra_argumentsを定義する

先ほどはextra_argumentsをひとつ定義して使いましたが、複数定義することもできます。

あまり意味はありませんが、Providerに関するtfvarsファイルを指定したとextra_arguments、データベース名に関する
extra_argumentsに分割してみます。
少し変化を入れるため、データベース名に関しては-varで指定するようにしてみました。といっても、tfvarsで指定していた
内容と同じですが…。

terragrunt.hcl

terraform {
  extra_arguments "provider_vars" {
    commands = get_terraform_commands_that_need_vars()

    arguments = [
      "-var-file=../provider.tfvars"
    ]
  }

  extra_arguments "database_vars" {
    commands = get_terraform_commands_that_need_vars()

    arguments = [
      "-var", "database_name=my_database"
    ]
  }
}

実際のモジュール適用側では、この変更を意識することなく使うことができます。

データベース側。

$ cd database
$ terragrunt apply

確認。

mysql_database.app: Creating...
mysql_database.app: Creation complete after 0s [id=my_database]

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

Outputs:

database_name = "my_database"

ユーザーおよび権限側。

$ cd ../users
$ terragrunt apply

確認。

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

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

Outputs:

admin_user_name = "admin"
admin_user_privileges = toset([
  "ALL",
])
app_user_name = "appuser"
app_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])

さらにオプションを指定すると?

ところで、引数やオプション定義を指定した状態で、同じオプションを指定するとどうなるのでしょう?

terragrunt.hclがこの状態で

terragrunt.hcl

terraform {
  extra_arguments "provider_vars" {
    commands = get_terraform_commands_that_need_vars()

    arguments = [
      "-var-file=../provider.tfvars"
    ]
  }

  extra_arguments "database_vars" {
    commands = get_terraform_commands_that_need_vars()

    arguments = [
      "-var", "database_name=my_database"
    ]
  }
}

データベースのモジュール側で、-varを指定してplanを実行してみます。

$ cd database
$ terragrunt plan -var database_name=foo

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # mysql_database.app will be created
  + resource "mysql_database" "app" {
      + default_character_set = "utf8mb4"
      + default_collation     = "utf8mb4_ja_0900_as_cs_ks"
      + id                    = (known after apply)
      + name                  = "foo"
    }

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

Changes to Outputs:
  + database_name = "foo"

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

terragruntコマンド実行時に指定した内容で、planが実行されました…が、これは以下のように解釈されていると考えるべき
でしょうね。

$ terraform plan -var-file=../provider.tfvars -var database_name=my_database -var database_name=foo

少なくとも、まとめたオプションと同じものをコマンド実行時に改めて指定しない方が良い気がします。

モジュール固有の内容を指定したい場合?

今回は各モジュール実行時に指定するオプションを全部同じにまとめましたが、一部はモジュール単位で固有に指定、といった
ことはできるのでしょうか。

たとえば、トップディレクトリにあるterragrunt.hclファイルは以下のProviderに関するtfvarsファイルを指定するだけにしてみます。

terragrunt.hcl

terraform {
  extra_arguments "provider_vars" {
    commands = get_terraform_commands_that_need_vars()

    arguments = [
      "-var-file=../provider.tfvars"
    ]
  }
}

データベースの定義へ移動。

$ cd database

この時点では、データベース側のモジュールを実行するにはデータベース名の指定が足りない状態です。

ここで、terragrunt.hclファイルを上位ディレクトリの内容を参照するだけでなく、固有のextra_argumentsを持つように変えてみます。

terragrunt.hcl

include {
  path = find_in_parent_folders()
}

terraform {
  extra_arguments "database_vars" {
    commands = get_terraform_commands_that_need_vars()

    arguments = [
      "-var",
      "database_name=my_database"
    ]
  }
}

planで確認してみます。

$ terragrunt plan

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # mysql_database.app will be created
  + resource "mysql_database" "app" {
      + default_character_set = "utf8mb4"
      + default_collation     = "utf8mb4_ja_0900_as_cs_ks"
      + id                    = (known after apply)
      + name                  = "my_database"
    }

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

Changes to Outputs:
  + database_name = "my_database"

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

認識しているようですね、OKです。

こういう感じで、モジュールを跨いで指定するオプションだけでなく、Terraform実行時に指定するオプションを忘れないように
定義する場としても使えそうですね。

まとめ

Terragruntを使って、Terraformの引数やオプションをまとめてみました。

指定するオプションが増えてきたりするとまあ面倒ですし、忘れたりするのでこうやってまとめたりするのもよいのかなと思います。

Terragruntを使わない場合、だいたいシェルスクリプト化したり、実行するための手順を書いたりしてそうですね。

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

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

前に、Terragruntを使ってTerraform Backendの定義をまとめてみました。

TerragruntをUbuntu Linux 20.04 LTSにインストールして、Terraform Backendの定義をまとめてみる - CLOVER🍀

今度は、Providerの定義をまとめてみたいと思います。

TerragruntでProviderの定義をまとめる

Providerの定義をまとめるとは、どういうことでしょう。

Quick startやTerragruntの機能に関するドキュメントを見てみます。

Quick start / Keep your provider configuration DRY

Keep your Terraform code DRY / DRY common Terraform code with Terragrunt generate blocks

Terraform Backendの時とは異なり、Providerの定義にはVariableを使うことができます。とはいえ、それでも同じような定義が
各Terraformのルートモジュールに散らばることになります。

再利用可能なTerraformモジュールを使う場合であっても、Providerの定義は通常モジュール定義には含まれないため、
Providerはルートモジュール側で定義することになります。

Terragruntのgenerateブロックを使用すると、このようなProviderの定義をまとめてコードをDRYに保つことができる、
ということのようです。

では、試していきましょう。

環境

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

$ terraform version
Terraform v0.14.6


$ terragrunt -v
terragrunt version v0.28.6

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

Provider: MySQL - Terraform by HashiCorp

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

また、最後でTerraform BackendとしてConsulも利用します。

お題

MySQL Providerを使い、以下のリソースを定義する2つのTerraformルートモジュールを作成します。

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

最初はTerraformのみで実現し、その後にTerragruntでProvider定義をまとめてみましょう。

Terraformのみで構成してみる

では、Terraformでリソース定義を行っていきます。databaseとusersという2つのディレクトリを作成し、以下のような
ディレクトリ構成にしました。

$ tree database users
database
└── main.tf
users
└── main.tf

0 directories, 2 files

データベース側。

$ cd database

定義は、こんな感じです。

main.tf

terraform {
  required_version = "0.14.6"

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

provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}

resource "mysql_database" "app" {
  name                  = "my_database"
  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

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

実行できることだけ確認します。initしてapply。

$ terraform init
$ terraform apply

リソースができました。

mysql_database.app: Creating...
mysql_database.app: Creation complete after 0s [id=my_database]

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

Outputs:

database_name = "my_database"

ユーザーや権限側も。

$ cd ../users

リソース定義。

main.tf

terraform {
  required_version = "0.14.6"

  required_providers {

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

provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}

locals {
  database_name = "my_database"
}

resource "mysql_user" "admin_user" {
  user               = "admin"
  plaintext_password = "password"
  host               = "%"
}

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

resource "mysql_user" "application_user" {
  user               = "appuser"
  plaintext_password = "password"
  host               = "%"
}

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

output "admin_user_name" {
  value = mysql_user.admin_user.user
}

output "admin_user_privileges" {
  value = mysql_grant.admin_user.privileges
}

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

output "app_user_privileges" {
  value = mysql_grant.application_user.privileges
}

こちらも確認。

$ terraform init
$ terraform apply

リソースができました。

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

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

Outputs:

admin_user_name = "admin"
admin_user_privileges = toset([
  "ALL",
])
app_user_name = "appuser"
app_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])

確認できたので、1度リソースを破棄します。

$ terraform destroy
$ cd ../database
$ terraform destroy
$ cd ..

Terragruntを使って、Provider定義をまとめる

先ほどのTerraformの定義ファイルを見返すと、2つのルートモジュールでProviderの定義が重複していることがわかります。

provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}

これをまとめてしまおうというのが、今回のお題です。

トップレベルのディレクトリ(databaseおよびusersの親ディレクトリ)で、以下のようなファイルを作成します。

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
}

generateブロックに関するリファレンスは、こちらです。

Configuration Blocks and Attributes / generate

generateの隣に書いているproviderというのは、このブロックの名前です。terragrunt.hclには複数のgenerateブロックを
含めることができるため、区別できるように指定します。

pathは、生成するファイル名。

if_existsは、ファイルが存在した場合の動作を指定します。overwrite_terragruntは、Terragruntにより生成されたファイルの場合は
上書きする、という意味になります。

contentsは、ファイルに出力する内容です。

ところで、required_providersも重複しているのでは?という話もあるのですが、issueになっています。
読んでみると、Providerと同じ方法でまとめられる雰囲気もあるようですが…。

Possibility to generate required_providers ? · Issue #1374 · gruntwork-io/terragrunt · GitHub

データベース定義側に移動。

$ cd database

terragrunt.hclというファイルを作成します。中身は、親ディレクトリを参照する以下の定義のみでOKです。

terragrunt.hcl

include {
  path = find_in_parent_folders()
}

main.tfからは、Providerの定義を削除してしまいます。

main.tf

terraform {
  required_version = "0.14.6"

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

# provider "mysql" {
#   endpoint = "172.17.0.2:3306"
#   username = "root"
#   password = "password"
# }

resource "mysql_database" "app" {
  name                  = "my_database"
  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

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

コマンドをterraformからterragruntに切り替え、とりあえずplanを実行してみましょう。

$ terragrunt plan

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # mysql_database.app will be created
  + resource "mysql_database" "app" {
      + default_character_set = "utf8mb4"
      + default_collation     = "utf8mb4_ja_0900_as_cs_ks"
      + id                    = (known after apply)
      + name                  = "my_database"
    }

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

Changes to Outputs:
  + database_name = "my_database"

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

すると、カレントディレクトリにprovider.tfというファイルが生成されます。

provider.tf

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

親ディレクトリに作成した、terragrunt.hclのcontentsの内容が反映されていますね。

applyします。

$ terragrunt apply

動きました。

mysql_database.app: Creating...
mysql_database.app: Creation complete after 1s [id=my_database]

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

Outputs:

database_name = "my_database"

ユーザーおよび権限側も。

$ cd ../users

terragrunt.hclを作成する必要がありますが、内容はデータベース側と同じでいいのでコピーします。

$ cp ../database/terragrunt.hcl ./.

main.tfから、Provider定義を削除。

main.tf

terraform {
  required_version = "0.14.6"

  required_providers {

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

# provider "mysql" {
#   endpoint = "172.17.0.2:3306"
#   username = "root"
#   password = "password"
# }

locals {
  database_name = "my_database"
}

resource "mysql_user" "admin_user" {
  user               = "admin"
  plaintext_password = "password"
  host               = "%"
}

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

resource "mysql_user" "application_user" {
  user               = "appuser"
  plaintext_password = "password"
  host               = "%"
}

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

output "admin_user_name" {
  value = mysql_user.admin_user.user
}

output "admin_user_privileges" {
  value = mysql_grant.admin_user.privileges
}

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

output "app_user_privileges" {
  value = mysql_grant.application_user.privileges
}

今度は、いきなりapplyします。

$ terragrunt apply

OKです。

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

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

Outputs:

admin_user_name = "admin"
admin_user_privileges = toset([
  "ALL",
])
app_user_name = "appuser"
app_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])

Terragruntによって生成されたprovider.tfは、こんな感じです。

provider.tf

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

Terragruntによって、Provider定義をまとめ、terragruntコマンドの実行とともにTerraformのProvider定義ファイルを生成できることを
確認しました。

次は、destroyして上位ディレクトリへ。
※terraform.tfstateを削除しているのは、この後のためです

$ terragrunt destroy
$ rm terraform.tfstate*
$ cd ../database
$ terragrunt destroy
$ rm terraform.tfstate*
$ cd ..

ここまでで、こういうディレクトリ、ファイル構成になりました。

$ tree
.
├── database
│   ├── main.tf
│   ├── provider.tf
│   └── terragrunt.hcl
├── terragrunt.hcl
└── users
    ├── main.tf
    ├── provider.tf
    └── terragrunt.hcl

Terraform BackendをConsulにしてみる

最後は、Terraform BackendとしてConsulを使い、Terraform BackendとProviderの定義の両方をまとめてみましょう。

Consulはバージョン1.9.3が172.17.0.2で動作しているものとし、MySQLはバージョン8.0.23が172.17.0.3で動作しているものとします。
※MySQLのIPアドレスは、先ほどから変更しています

トップディレクトリにあるterragrunt.hclに、remote_stateを加えます。

terragrunt.hcl

remote_state {
  backend = "consul"

  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }

  config = {
    address = "172.17.0.2:8500"
    scheme  = "http"
    path    = "terraform/state/mysql/${path_relative_to_include()}"
    lock    = true
  }
}

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

これで、Terraform Backendの定義とProviderの定義の両方がまとめられていることを確認します。

データベース側へ移動。

$ cd database

Terraform Backendが変わったので、今回はinitが必要になります(.terraformディレクトリも消しておけば良かったですね)。

$ terragrunt init

生成されたbackend.tf。

backend.tf

# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
  backend "consul" {
    address = "172.17.0.2:8500"
    lock    = true
    path    = "terraform/state/mysql/database"
    scheme  = "http"
  }
}

provider.tfは、IPアドレスの変更が反映されています(172.17.0.2から172.17.0.3になりました)。

provider.tf

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

applyします。

$ terragrunt apply

リソースができました。

mysql_database.app: Creating...
mysql_database.app: Creation complete after 0s [id=my_database]

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

Outputs:

database_name = "my_database"

StateがConsulに保存されたことも確認しておきます。

f:id:Kazuhira:20210218002710p:plain

次に、ユーザーや権限側の方も変更を適用しましょう。ディレクトリを移動して、initまでしておきます。

$ cd ../users
$ terragrunt init

生成されたbackend.tf。

backend.tf

# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
  backend "consul" {
    address = "172.17.0.2:8500"
    lock    = true
    path    = "terraform/state/mysql/users"
    scheme  = "http"
  }
}

provider.tfにも、変更が反映されます。

provider.tf

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

applyします。

$ terragrunt apply

こちらもOKです。

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

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

Outputs:

admin_user_name = "admin"
admin_user_privileges = toset([
  "ALL",
])
app_user_name = "appuser"
app_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])

Consol側にも、Stateが保存されたことを確認しておきます。

f:id:Kazuhira:20210218002857p:plain

各ディレクトリのmain.tfはなにも触っていませんが、これでConsul BackendでStateが管理できるようになりました。

最後に、トップレベルのディレクトリからツリーを見ておきます。

$ cd ..

こうなりました、と。

$ tree
.
├── database
│   ├── backend.tf
│   ├── main.tf
│   ├── provider.tf
│   └── terragrunt.hcl
├── terragrunt.hcl
└── users
    ├── backend.tf
    ├── main.tf
    ├── provider.tf
    └── terragrunt.hcl

まとめ

Terragruntを使って、Providerの定義をまとめてみました。

Terraform Backendをまとめた時と似たようなファイル生成の仕組みを使っているのですが、terragrunt.hclの書き方が違うので
扱いも異なるんでしょうね。

最終的にはTerraformの定義ファイルとして生成されるので、ファイルさえできてしまえばあとはTerraformのみでも使えたり、
Terragruntから離脱することも簡単そうですね。