CLOVER🍀

That was when it all began.

Terraformのcount、for_eachに作成前のリソースの情報を指定できないという話

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

Terraformでcountを使っていた時にこんなエラーに当たったので、「これはなんだろう?」と思い。

The "count" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the count depends on.

ちょっと調べてみることにしました。

結論を書くと、countやfor_eachで指定する値の中に、作成前のリソースの情報(属性)が含まれているとダメみたいです。

ドキュメントにも書いてありました。

Resources / Using Expressions in count

The count meta-argument accepts numeric expressions. However, unlike most resource arguments, the count value must be known before Terraform performs any remote resource actions. This means count can't refer to any resource attributes that aren't known until after a configuration is applied (such as a unique ID generated by the remote API when an object is created).

Resources / Using Expressions in for_each

The for_each meta-argument accepts map or set expressions. However, unlike most resource arguments, the for_each value must be known before Terraform performs any remote resource actions. This means for_each can't refer to any resource attributes that aren't known until after a configuration is applied (such as a unique ID generated by the remote API when an object is created).

これは、その他のリソースに関する引数とは異なる、countやfor_eachに関する性質です。

環境

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

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

Providerは、MySQLを使用します。MySQLは8.0.20を使い、172.17.0.2のサーバーで動作しているものとします。

サンプル

では、サンプルを書いていきます。

お題はこうしましょう。

  • MySQLデータベースのユーザーおよび権限を作成するモジュールを作る
  • 権限などで利用するデータベースは、モジュールのInput Variablesで指定する
  • データベースの情報が指定されなかった場合は、モジュール側でデフォルトのデータベースを作成する

このお題だと、データベースが条件によって作成されたり、作成されなかったりします。

では、最初にモジュールの呼び出し側となるリソース定義ファイルを載せます。
main.tf

terraform {
  required_version = "0.12.28"
}

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

  version = "1.9.0"
}

module "user" {
  source = "./modules/user"
}

この段階では、単純にモジュールを呼び出しているだけです。

次に、モジュール側の定義ファイル。
modules/user/main.tf

variable "database_id" {
  type    = string
  default = null
}

variable "database_name" {
  type    = string
  default = null
}

resource "mysql_database" "default" {
  count = var.database_id == null ? 1 : 0

  name = "default_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

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

resource "mysql_grant" "grant" {
  user       = mysql_user.user.user
  host       = "%"
  database   = var.database_name != null ? var.database_name : mysql_database.default[0].id
  privileges = ["ALL"]
}

Input Variableに、mysql_databaseリソースのidとnameを指定しています。

variable "database_id" {
  type    = string
  default = null
}

variable "database_name" {
  type    = string
  default = null
}

特に、idはリソース作成後でないと得られない値です。

MySQL: mysql_database - Terraform by HashiCorp

ここで、idが指定されていなければデフォルトのデータベースを作成し、指定された場合はデータベースを作成しない、という
条件でmysql_databaseリソースの定義を行います。

resource "mysql_database" "default" {
  count = var.database_id == null ? 1 : 0

  name = "default_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

ちなみに、mysql_databaseリソースの情報はmysql_grantで使用します。

resource "mysql_grant" "grant" {
  user       = mysql_user.user.user
  host       = "%"
  database   = var.database_name != null ? var.database_name : mysql_database.default[0].id
  privileges = ["ALL"]
}

これを実行します。

$ terraform apply -auto-approve

問題なく、うまくいきます。

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

1度、リソースは破棄しておきます。

$ terraform destroy -force

モジュールの外側でmysql_databaseを定義した場合

次に、モジュールの外側でデータベースを事前に作成するように変更してみましょう。 main.tf

terraform {
  required_version = "0.12.28"
}

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

  version = "1.9.0"
}

resource "mysql_database" "predefined" {
  name = "my_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

module "user" {
  source = "./modules/user"

  database_id   = mysql_database.predefined.id
  database_name = mysql_database.predefined.name
}

mysql_databaseリソース定義を追加して

resource "mysql_database" "predefined" {
  name = "my_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

モジュールのid、nameには、このリソースの属性を指定します。

module "user" {
  source = "./modules/user"

  database_id   = mysql_database.predefined.id
  database_name = mysql_database.predefined.name
}

これがどういう結果になるか、ですが、Terraformが実行できなくなります。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


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

Error: Invalid count argument

  on modules/user/main.tf line 13, in resource "mysql_database" "default":
  13:   count = var.database_id == null ? 1 : 0

The "count" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the count depends on.

エラーメッセージを読むと、「countは、applyを実行するまで決定できない値には依存できない」と言っています。
インスタンスをいくつ作っていいか予測できないから、だと。

これを回避するには、エラーメッセージにあるように事前にモジュールが依存している(countを使うリソースが依存している)
リソースを作成しておきます。

$ terraform apply -target=mysql_database.predefined -auto-approve

まあ、完全には終わってないよ、って言われますけど。

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

Warning: Resource targeting is in effect

You are creating a plan with the -target option, which means that the result
of this plan may not represent all of the changes requested by the current
configuration.
        
The -target option is not for routine use, and is provided only for
exceptional situations such as recovering from errors or mistakes, or when
Terraform specifically suggests to use it as part of an error message.


Warning: Applied changes may be incomplete

The plan was created with the -target option in effect, so some changes
requested in the configuration may have been ignored and the output values may
not be fully updated. Run the following command to verify that no other
changes are pending:
    terraform plan
    
Note that the -target option is not suitable for routine use, and is provided
only for exceptional situations such as recovering from errors or mistakes, or
when Terraform specifically suggests to use it as part of an error message.


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

このあとは、「terraform apply」すれば残りのリソースを作成してくれます。

$ terraform apply -auto-approve
mysql_database.predefined: Refreshing state... [id=my_database]
module.user.mysql_user.user: Creating...
module.user.mysql_user.user: Creation complete after 0s [id=user@%]
module.user.mysql_grant.grant: Creating...
module.user.mysql_grant.grant: Creation complete after 0s [id=user@%:`my_database`]

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

ちなみに、この事象はcountではなく、for_eachを使っても発生します。

resource "mysql_database" "default" {
  for_each = var.database_id == null ? toset([var.database_id]) : toset([])
  #count = var.database_id == null ? 1 : 0

  name = "default_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

こんな感じのエラーになります。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


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

Error: Invalid for_each argument

  on modules/user/main.tf line 12, in resource "mysql_database" "default":
  12:   for_each = var.database_id == null ? toset([var.database_id]) : toset([])

The "for_each" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the for_each depends on.

というわけで、countやfor_eachを使う値に、terraform applyなどが同じタイミングで適用、生成される値が使用されている場合は、
こういう結果になるので事前に作成しておきましょう、と。

ちょっと惜しい感じもするのですが、今回のような単一の値ではなく、リソースの作成結果がリストのような複数の値を返し、
それを元に別のリソースを作成する…といったようなケースを考えると、確かに作成するリソースの数が定まらなくなるので
仕方ないですね、と。

参考

https://github.com/hashicorp/terraform/blob/v0.12.28/terraform/eval_count.go#L39

https://github.com/hashicorp/terraform/blob/v0.12.28/terraform/eval_for_each.go#L23

Conditional expression assigned to local cannot be evaluated during plan · Issue #21450 · hashicorp/terraform · GitHub