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を使って、オプション指定をまとめる

ここまでで、applydestroy時に同じオプションを毎回指定してきました。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を使わない場合、だいたいシェルスクリプト化したり、実行するための手順を書いたりしてそうですね。