CLOVER🍀

That was when it all began.

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でリソース定義を行っていきます。databaseusersという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.hclcontentsの内容が反映されていますね。

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で動作しているものとします。
MySQLIPアドレスは、先ほどから変更しています

トップディレクトリにある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から離脱することも簡単そうですね。