CLOVER🍀

That was when it all began.

Terraformで、リソースを作成する・しないをコントロールしたい

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

Terraformでリソースを作るにあたり、条件によって作る・作らないをコントロールしたいなと。

解法としては繰り返しを使うみたいなのですが、自分でちゃんと追ってみましょうか、と。

環境とお題

今回の環境は、こちら。

$ terraform version
Terraform v0.12.25
+ provider.mysql v1.9.0

ProviderはMySQLを使い、データベースの作成をコントロールしてみましょう。MySQL自体は、8.0.20を使います。

雛形

まずは、定型的な雛形を用意。
main.tf

terraform {
  required_version = ">= 0.12.25"
}

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

  version = "1.9.0"
}

こちらをベースにカスタマイズしていきましょう。

データベースを複数作る/作らないをコントロールする

Terraformでリソースを複数作成するには、countまたはfor_eachを使用します。

どちらも、指定した数だけリソースを作成することができます。

この時、countを0にしたり、for_eachに空のコレクションを与えるようにすると、そのリソースを作成しないようにすることが
できます。

countで実現する

こんな感じで指定すると、この例だとデータベースが5つ作成されます。

locals {
  create_count = 5
}

resource "mysql_database" "this" {
  count = local.create_count

  name = format("my_database%d", count.index + 1)
}

output "database_names" {
  value = mysql_database.this[*].name
}

Outputとしては、作成したデータベース名を全部返すようにしましょう。

確認。

$ terraform apply -auto-approve
mysql_database.this[3]: Creating...
mysql_database.this[0]: Creating...
mysql_database.this[2]: Creating...
mysql_database.this[4]: Creating...
mysql_database.this[1]: Creating...
mysql_database.this[0]: Creation complete after 0s [id=my_database1]
mysql_database.this[2]: Creation complete after 0s [id=my_database3]
mysql_database.this[3]: Creation complete after 0s [id=my_database4]
mysql_database.this[1]: Creation complete after 0s [id=my_database2]
mysql_database.this[4]: Creation complete after 0s [id=my_database5]

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

Outputs:

database_names = [
  "my_database1",
  "my_database2",
  "my_database3",
  "my_database4",
  "my_database5",
]

この時、countに指定する値を0にすると

locals {
  create_count = 0
}

データベースは作成されなくなります。

$ terraform apply -auto-approve

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

Outputs:

database_names = []

Outputは、空のlistになりましたね。

for_eachで実現する

続いて、for_eachで実現してみましょう。for_eachを使う場合は、list(をsetに変換する)かmapを使いますが、今回はlistで。

locals {
  database_names = ["database1", "database2"]
}

resource "mysql_database" "this" {
  for_each = toset(local.database_names)

  name = each.value
}

この例では、データベースが2つできます。

$ terraform apply -auto-approve
mysql_database.this["database1"]: Creating...
mysql_database.this["database2"]: Creating...
mysql_database.this["database1"]: Creation complete after 0s [id=database1]
mysql_database.this["database2"]: Creation complete after 0s [id=database2]

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

空のlistを渡した場合は

locals {
  database_names = []
}

データベースは作成されません。for_eachでも、countと同じことができますね。

$ terraform apply -auto-approve

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

ところで、for_eachの方にはOutputの定義がありません。

output "database_names" {
  value = mysql_database.this[*].name
}

これは、先ほどの定義のままだと、こんな感じでエラーになるからです…。

Error: Unsupported attribute

  on main.tf line 33, in output "database_names":
  33:   value = mysql_database.this[*].name

This object does not have an attribute named "name".

Splat Expressionsは、listだけみたいですねぇ…。

Splat Expressions

for_each and splat · Issue #22476 · hashicorp/terraform · GitHub

もしも指定するのなら、固定の要素にするか

output "database_names" {
  value = mysql_database.this["database1"].name
}

listに変換することになります。

output "database_names" {
  value = values(mysql_database.this)[*].name
}

単一のリソースを作る/作らないをコントロールする

ここまでは「複数個のリソースができる」ことを前提に書いてきましたが、「作るとしてもひとつ、もしくは作らない」という
パターンを考えてみたいと思います。

countで実現する

こういうのは、countでやった方がわかりやすいでしょうか?あと、作る・作らないのコントロールは、boolですると良さそうですよね。

locals {
  create = true
}

resource "mysql_database" "this" {
  count = local.create ? 1 : 0
  name = "my_database"
}

こんな感じで、作成するなら1、しないなら0にしておけばOKです。

  count = local.create ? 1 : 0

ただ、Outputにはちょっと困ります。

こんな感じの指定だとlistになるので、呼び出し元がlistとして扱うことになってしまいます。

output "database_names" {
  value = mysql_database.this[*].name
}

単一のリソースを作る前提の場合、これは嬉しくないですね。

かといって、こんな書き方をすると

output "database_name" {
  value = mysql_database.this[0].name
}

リソースを作成する時はいいのですが

$ terraform apply -auto-approve
mysql_database.this[0]: Creating...
mysql_database.this[0]: Creation complete after 0s [id=my_database]

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

Outputs:

database_name = my_database

作成しない場合は

locals {
  create = false
}

エラーになってしまいます。

$ terraform apply -auto-approve

Error: Invalid index

  on main.tf line 42, in output "database_name":
  42:   value = mysql_database.this[0].name
    |----------------
    | mysql_database.this is empty tuple

The given key does not identify an element in this collection value.

さあ、どうしたものでしょう。

いろいろ見ていると、concatで使ってなんとかするみたいですね。

concat - Functions - Configuration Language - Terraform by HashiCorp

output "database_name" {
  value = concat(mysql_database.this.*.name, [null])[0]
}

### もしくは
output "database_name" {
  value = concat(mysql_database.this.*.name, [""])[0]
}

コミュニティモジュールとか見ていると、後者([""]指定)の方を使うみたいですね。

これで、最初のlistが空の時は、後続のダミーのlistの要素が使われるという感じに。

細工が必要ですねぇ。

for_eachで実現する

for_eachで実現する場合は、こんな感じでしょうか。

locals {
  create = true
}

resource "mysql_database" "this" {
  for_each = local.create ? toset(["my_database"]) : toset([])

  name = each.value
}

output "database_name" {
  value = concat(values(mysql_database.this)[*].name, [null])[0]

  # もしくは
  #value = concat(values(mysql_database.this)[*].name, [""])[0]
}

単一のリソースを作るかどうか制御するのが悩みどころだったのですが、こんな感じにすればできるみたいなので、
覚えておきましょう。