これは、なにをしたくて書いたもの?
Terraformのコレクションが、単一のデータ型のみが格納可能だということを、ちゃんと把握できていなかったのでメモとして。
Type Constraints - Configuration Language - Terraform by HashiCorp
平たく言うと、any
を勘違いしていました、と。
Terraformのコレクションとは?
そもそも、ここから。
コレクションには、list、map、setの3種類があります。
今回のポイントは、ここです。
All elements of a collection must always be of the same type.
コレクションの要素は、すべて同じデータ型である必要があります。ハッキリ書いてありますね。
mapに関しては、キーがstringであるコレクションですね。
map(...): a collection of values where each is identified by a string label.
any?
ここでanyというキーワードをちょっと見てみたいと思います。map(any)
とかで見かけるやつです。
Dynamic Types: The "any" Constraint
これ、ワイルドカード的なやつではありません。
The keyword any is a special construct that serves as a placeholder for a type yet to be decided. any is not itself a type: when interpreting a value against a type constraint containing any, Terraform will attempt to find a single actual type that could replace the any keyword to produce a valid result.
ポイントは、ここ。
Terraform will attempt to find a single actual type that could replace the any keyword to produce a valid result.
any
は、単一の型に収束しようとします。
つまり、map(any)
といったanyが指定されたコレクションは、格納される値によってmap(string)
などに決まるということですね。
では、複数のデータ型を入れた場合はどうなるんでしょう?たとえば、こんな感じで。
map_param = { string_param = "Hello World!!" number_param = 8 bool_param = true }
この場合、型変換が行われます。
All of the elements of a collection must have the same type, so conversion to list(any) requires that all of the given elements must be convertible to a common type. This implies some other behaviors that result from the conversion rules described in earlier sections.
複数のデータ型が入った場合は、stringに変換しようとします。
コレクションの中に、他のデータ型とコレクションを混ぜた場合、stringに変換できなくなるためエラーになります。
If the given value were instead ["a", [], "b"] then the value cannot conform to the type constraint: there is no single type that both a string and an empty tuple can convert to. Terraform would reject this value, complaining that all elements must have the same type.
この変換ルール自体は、こちらに記載があります。
numberおよびboolがstringに変換される、ということですね。
なので、stringに変換できないコレクションを、コレクションの要素として混合させるとエラーになります。
これをちゃんと認識してなくて、だいぶハマったことがあります…。
for_each
ちなみに、for_eachにもコレクションを指定することができますが、その値のデータ型はstringである必要があります。
for_each: Multiple Resource Instances Defined By a Map, or Set of Strings
ここも、たまに忘れてハマります…。
複雑な構成のコレクションをfor_eachに渡そうとして
The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a xxxxx...
みたいなエラーを言われることになります。
https://github.com/hashicorp/terraform/blob/v0.13.4/terraform/eval_for_each.go#L69-L76
go-cty/unknown.go at v1.5.1 · zclconf/go-cty · GitHub
今回確認したかった内容はだいたいこんな感じなのですが、ちょっと動作も確認してみましょう。
環境
今回の環境は、こちら。
$ terraform version Terraform v0.13.4 + provider registry.terraform.io/hashicorp/null v3.0.0
試してみる
各種のデータ型を引数に取るモジュールを用意してみます。
modules/simple_args/variables.tf
variable "string_param" { type = string } variable "number_param" { type = number } variable "bool_param" { type = bool } variable "list_param" { type = list(string) } variable "map_param" { type = map(string) } variable "object_param" { type = object({ param = string }) }
結果は、local-exec
でecho
するだけにしておきましょう。
modules/simple_args/main.tf
locals { params = <<PARAMS receive args: string_param = ${var.string_param} number_param = ${var.number_param} bool_param = ${var.bool_param} list_param = ${join(",", var.list_param)} map_param = { a = ${var.map_param.a}, b = ${var.map_param.b} } object_param = { param = ${var.object_param.param} } PARAMS } resource "null_resource" "this" { triggers = { params = local.params } provisioner "local-exec" { command = "echo '${local.params}'" } }
このモジュールを使って、NGなパターンを見ていきます。
これを呼び出すルートモジュールを作成します。
main.tf
terraform { required_version = "0.13.4" required_providers { null = { version = "3.0.0" source = "hashicorp/null" } } } ## 後で
この「後で」の部分の呼び出し方法を、いろいろ変えていきたいと思います。
コレクションを使わない場合
ルートモジュールに、こんな感じで追記。
terraform { ## 省略 } module "simple_args" { source = "./modules/simple_args" string_param = "Hello World!!" number_param = 8 bool_param = true list_param = [1, 2, 3] map_param = { a = 1, b = 2 } object_param = { param = "value123" } }
単純な呼び出しですね。実行してみます。
$ terraform init $ terraform apply -auto-approve module.object_args.module.simple_args.null_resource.this: Refreshing state... [id=6976449612170353829] module.object_args.module.simple_args.null_resource.this: Destroying... [id=6976449612170353829] module.object_args.module.simple_args.null_resource.this: Destruction complete after 0s module.simple_args.null_resource.this: Creating... module.simple_args.null_resource.this: Provisioning with 'local-exec'... module.simple_args.null_resource.this (local-exec): Executing: ["/bin/sh" "-c" "echo 'receive args:\n string_param = Hello World!!\n number_param = 8\n bool_param = true\n list_param = 1,2,3\n map_param = { a = 1, b = 2 }\n object_param = { param = value123 }\n'"] module.simple_args.null_resource.this (local-exec): receive args: module.simple_args.null_resource.this (local-exec): string_param = Hello World!! module.simple_args.null_resource.this (local-exec): number_param = 8 module.simple_args.null_resource.this (local-exec): bool_param = true module.simple_args.null_resource.this (local-exec): list_param = 1,2,3 module.simple_args.null_resource.this (local-exec): map_param = { a = 1, b = 2 } module.simple_args.null_resource.this (local-exec): object_param = { param = value123 } module.simple_args.null_resource.this: Creation complete after 0s [id=3000518257607236606]
まあ、これは動きますね、と。
mapで渡そうとしてみる
では、呼び出し先のモジュールとの間に、モジュールをもうひとつ置いてみます。
こんな感じで、引数にmap(any)
を取るモジュールを用意。
modules/map_args/variables.tf
variable "container" { type = map(any) }
先ほど作ったモジュールに対して、mapを介して引数を渡します。
modules/map_args/main.tf
module "simple_args" { source = "../simple_args" string_param = var.container.string_param number_param = var.container.number_param bool_param = var.container.bool_param list_param = var.container.list_param map_param = var.container.map_param object_param = var.container.object_param }
ルートモジュール側は、このモジュールを呼び出します。引数には、mapを渡します。
module "map_args" { source = "./modules/map_args" container = { string_param = "Hello World!!" number_param = 8 bool_param = true list_param = [ 1, 2, 3 ] map_param = { a = 1, b = 2 } object_param = { param = "value123" } } }
initして
$ terraform init
実行しようとするものの、うまくいきません。
$ terraform apply -auto-approve Error: Invalid value for module argument on main.tf line 30, in module "map_args": 30: container = { 31: string_param = "Hello World!!" 32: number_param = 8 33: bool_param = true 34: list_param = [ 1, 2, 3 ] 35: map_param = { a = 1, b = 2 } 36: object_param = { param = "value123" } 37: } The given value is not suitable for child module variable "container" defined at modules/map_args/variables.tf:1,1-21: all map elements must have the same type.
いろんなデータ型を混合してあり、かつコレクションやobjectまでいるのでstringに変換できなくなっているからですね。
all map elements must have the same type.
つまり、こういったmapの使い方はできません、と。map(any)
ではなく、最初からmap(string)
とかの具体的な型指定に
した方が、混乱しなさそう…。
うまくいかないことはわかりましたが、もう少し小細工してみましょう。
引数を、任意項目にしてみます。
modules/simple_args/variables.tf
variable "string_param" { type = string default = null } variable "number_param" { type = number default = null } variable "bool_param" { type = bool default = null } variable "list_param" { type = list(string) default = null } variable "map_param" { type = map(string) default = null } variable "object_param" { type = object({ param = string }) default = null }
mapを使って呼び出すモジュールを、lookup
を使って指定されていない項目にはデフォルト値を与えてみます。
modules/map_args/main.tf
module "simple_args" { source = "../simple_args" string_param = lookup(var.container, "string_param", "default") number_param = lookup(var.container, "number_param", 10) bool_param = lookup(var.container, "bool_param", true) list_param = lookup(var.container, "list_param", [0]) map_param = lookup(var.container, "map_param", { a = 99, b = 99 }) object_param = lookup(var.container, "object_param", { param = "v1" }) }
stringの項目だけmapで指定してみます。
module "map_args" { source = "./modules/map_args" container = { string_param = "Hello World!!" } }
実行してみます。
$ terraform apply -auto-approve module.simple_args.null_resource.this: Refreshing state... [id=3000518257607236606] Error: Invalid function argument on modules/map_args/main.tf line 25, in module "simple_args": 25: list_param = lookup(var.container, "list_param", [0]) Invalid value for "default" parameter: the default value must have the same type as the map elements. Error: Invalid function argument on modules/map_args/main.tf line 26, in module "simple_args": 26: map_param = lookup(var.container, "map_param", { a = 99, b = 99 }) Invalid value for "default" parameter: the default value must have the same type as the map elements. Error: Invalid function argument on modules/map_args/main.tf line 27, in module "simple_args": 27: object_param = lookup(var.container, "object_param", { param = "v1" }) Invalid value for "default" parameter: the default value must have the same type as the map elements.
今度は、lookupの部分で怒られます。
まあ、コレクションやobjectに関しては、データ型が合わないのでどうやっても通りませんね、と。
objectを使う
では、こういう場合はどうするのか?というと、Terraformとしてはobjectを使うのが正解なんでしょう。
https://github.com/hashicorp/terraform/issues/21384#issuecomment-495265031
objectを引数に取るモジュールを作ってみます。
modules/object_args/variables.tf
variable "container" { type = object({ string_param = string number_param = number bool_param = bool list_param = list(string) map_param = map(any) object_param = object({ param = string }) }) }
objectの中身は、最初のモジュールにそのまま渡すだけです。
modules/object_args/main.tf
module "simple_args" { source = "../simple_args" string_param = var.container.string_param number_param = var.container.number_param bool_param = var.container.bool_param list_param = var.container.list_param map_param = var.container.map_param object_param = var.container.object_param }
ルートモジュールで、これを呼び出します。
module "object_args" { source = "./modules/object_args" container = { string_param = "Hello World!!" number_param = 8 bool_param = true list_param = [ 1, 2, 3 ] map_param = { a = 1, b = 2 } object_param = { param = "value123" } } }
$ terraform init $ terraform apply -auto-approve module.simple_args.null_resource.this: Refreshing state... [id=3000518257607236606] module.simple_args.null_resource.this: Destroying... [id=3000518257607236606] module.object_args.module.simple_args.null_resource.this: Creating... module.simple_args.null_resource.this: Destruction complete after 0s module.object_args.module.simple_args.null_resource.this: Provisioning with 'local-exec'... module.object_args.module.simple_args.null_resource.this (local-exec): Executing: ["/bin/sh" "-c" "echo 'receive args:\n string_param = Hello World!!\n number_param = 8\n bool_param = true\n list_param = 1,2,3\n map_param = { a = 1, b = 2 }\n object_param = { param = value123 }\n'"] module.object_args.module.simple_args.null_resource.this (local-exec): receive args: module.object_args.module.simple_args.null_resource.this (local-exec): string_param = Hello World!! module.object_args.module.simple_args.null_resource.this (local-exec): number_param = 8 module.object_args.module.simple_args.null_resource.this (local-exec): bool_param = true module.object_args.module.simple_args.null_resource.this (local-exec): list_param = 1,2,3 module.object_args.module.simple_args.null_resource.this (local-exec): map_param = { a = 1, b = 2 } module.object_args.module.simple_args.null_resource.this (local-exec): object_param = { param = value123 } module.object_args.module.simple_args.null_resource.this: Creation complete after 0s [id=7084308946517713323]
objectであれば、複数のデータ型が入っていても、問題なく扱えます。mapと違って属性名も決まっているので補完も
効きやすいです。
では、mapよりも便利なのかというと、objectの場合は全属性が必須となります。
Values that match the object type must contain all of the specified keys, and the value for each key must match its specified type.
オプション的な引数の場合、この制約は割と面倒になったりします…。
なので、使いどころは制約、特性を理解しつつ考えていきましょう、と。