前回 に引き続き、Terraform 0.12における差分を学習していった結果をまとめていきたいと思います。
今回は Expression 編です。
ちなみに、新旧比較。タイトルは変わっていますがこういう対応関係らしいです。
0.12 : https://www.terraform.io/docs/configuration/expressions.html
0.11 : https://www.terraform.io/docs/configuration-0-11/interpolation.html
nullが値に使えるようになった
null与える=明示的な無視・・・とTerraformは扱うようですね。
となると、
- .tf側でキーがあって、値が正常域外
- .tf側でキーがあって、値が正常域
- .tf側でキーがあって、値がnull
- .tf側でキーがない
という4パターン、および、
A. パラメータがProvider側でRequired扱い
B. パラメータがProvider側でOptional扱いだがDefault値がある
C. パラメータがProvider側でOptional扱いでありDefault値も設定されていない
のそれぞれでどうなるのか・・・を考えてみます。
まぁ、大差はないだろうという予測のもとやってみます。
A. パラメータがProvider側でRequired扱い | B. パラメータがProvider側でOptional扱いだがDefault値がある | C. パラメータがProvider側でOptional扱いでありDefault値も設定されていない | |
---|---|---|---|
1. .tf側でキーがあって、値が正常域外 | ValidateFuncに弾かれる | ValidateFuncに弾かれる | ValidateFuncに弾かれる |
2. .tf側でキーがあって、値が正常域 | 付与された値を利用して後続処理へ | 付与された値を利用して後続処理へ | 付与された値を利用して後続処理へ |
3. .tf側でキーがあって、値がnull | エラーになる ( required field is not set ) |
正常処理 (Default値が利用される) |
planに乗ってこない 以降の処理はプロバイダの作りによる |
4. .tf側でキーがない | エラーになる ( Missing required argument ) |
正常処理 (Default値が利用される) |
planに乗ってこない 以降の処理はプロバイダの作りによる |
nullを指定したときと、キーがないときでエラーが違うんですね。
Requiredの時のエラーメッセージが違うくらいで、ほぼ同じ挙動ですね。
list(map)の挙動の差
これは明言されているわけではない(様に見える)のですが、以下の挙動が変わってますよね。
0.11の場合
list of mapsを指定したいときに、以下の指定ができました。
resource "ecl_network_network_v2" "net_1" {
name = "net_1"
}
resource "ecl_network_subnet_v2" "sub_1" {
network_id = "${ecl_network_network_v2.net_1.id}"
cidr = "192.168.1.0/24"
allocation_pools = {
start = "192.168.1.10"
end = "192.168.1.20"
}
allocation_pools = {
start = "192.168.1.30"
end = "192.168.1.40"
}
}
この状態で terraform plan
すると、
+ ecl_network_subnet_v2.sub_1
id: <computed>
allocation_pools.#: "2"
allocation_pools.0.end: "192.168.1.20"
allocation_pools.0.start: "192.168.1.10"
allocation_pools.1.end: "192.168.1.40"
allocation_pools.1.start: "192.168.1.30"
cidr: "192.168.1.0/24"
...
のようになったのですが、0.12だとエラーになりますよね。
allocation_pools = { ...
の =
がだめらしいです。
0.11でも0.12でも通るのは(というか0.12で通るのは)以下の書き方。
0.12の場合(0.11でも通る)
resource "ecl_network_network_v2" "net_1" {
name = "net_1"
}
resource "ecl_network_subnet_v2" "sub_1" {
network_id = "${ecl_network_network_v2.net_1.id}"
cidr = "192.168.1.0/24"
allocation_pools {
start = "192.168.1.10"
end = "192.168.1.20"
}
allocation_pools {
start = "192.168.1.30"
end = "192.168.1.40"
}
}
${ ... }
記法が状況により不要になった
0.12の差分として最も有名な部分かもしれませんが、
0.11の Interpolation Syntaxでは 変数使うときは ${ ... }ね
と書かれていたのが、基本的にその表記が不要となりましたね。
直接的には、 References to Resource Attributesを読むと差分として理解しやすいのだと思います。
(ちゃんと冒頭からその旨が書いてあるのですが)
*
の利用対象が拡大した
0.11までは、count > 1 で作成した場合にリソースをインデックス経由で指定する用途にしか使えなかったと思うのですが、attributeにも使えるようになっったようですね。
resource "ecl_imagestorages_image_v2" "image_1" {
count = 2
name = "image-${count.index}"
disk_format = "qcow2"
container_format = "bare"
tags = [
"tag-${count.index}-1",
"tag-${count.index}-2"
]
local_file_path = "./image.txt"
}
output "image_tags" {
value = [
ecl_imagestorages_image_v2.image_1[0].tags[*],
ecl_imagestorages_image_v2.image_1[1].tags[*]
]
}
- count = 2で、
ecl_imagestorages_image_v2
リソースを2つ作ります - 上記の
ecl_imagestorages_image_v2
はtagsという文字列のリストを持っています - outputで、そのtagsを
index=*
で参照しています
上記のtfファイルを利用した場合、結果的にoutputはこうなります。
Outputs:
image_tags = [
[
"tag-0-1",
"tag-0-2",
],
[
"tag-1-1",
"tag-1-2",
],
]
今までだと、 "${ecl_imagestorages_image_v2.image_1.*}"
という書き方はできても、上記のようなAttributeに対する *
付与的な書き方はできませんでした。
だめな例ですが、0.11だと例えばこういう書き方になりますよね。
resource "ecl_imagestorages_image_v2" "image_1" {
count = 2
name = "image-${count.index}"
disk_format = "qcow2"
container_format = "bare"
tags = [
"tag-${count.index}-1",
"tag-${count.index}-2",
]
local_file_path = "./image.txt"
}
output "image_tags" {
value = [
"${ecl_imagestorages_image_v2.image_1.0.tags.*}",
]
}
こちら、 terraform apply
時にはエラーにならないのですが、apply完了後outputを表示する段になって落ちます。
Error: Error applying plan:
1 error occurred:
* output.image_tags: Resource 'ecl_imagestorages_image_v2.image_1.0' does not have attribute 'tags.*' for variable 'ecl_imagestorages_image_v2.image_1.0.tags.*'
for
が使えるようになった
これも有名ですよね。
pythonの内包表記のような書き方ができるようになりました。
list参照のパターン
resource "ecl_network_network_v2" "net_1" {
name = "net_1"
}
resource "ecl_network_subnet_v2" "sub_1" {
cidr = "192.168.1.0/24"
network_id = ecl_network_network_v2.net_1.id
dns_nameservers = [
"8.8.8.8",
"8.8.4.4",
]
}
output "dns_nameservers" {
value = [for i in ecl_network_subnet_v2.sub_1.dns_nameservers : "${i}-for-in-list"]
}
この場合のoutputはこうなります。
Outputs:
dns_nameservers = [
"8.8.8.8-for-in-list",
"8.8.4.4-for-in-list",
]
僕はPythonが比較的慣れているので、そっちで理解するとこんな感じ。
(l
がdns_nameservers attribute相当)
>>> l = ["8.8.8.8", "4.4.4.4"]
>>> [ "%s-for-in-list" % i for i in l]
['8.8.8.8-for-in-list', '8.8.4.4-for-in-list']
map参照のパターン
mapでも同じような事ができます。
ただ、mapではなくobjectが生成されるという扱いのようですが。
(そもそも []
もlistではなくtupleを生成しているんですね)
The type of brackets around the for expression decide what type of result it produces. The above example uses [ and ], which produces a tuple. If { and } are used instead, the result is an object, and two result expressions must be provided separated by the => symbol:
resource "ecl_network_network_v2" "net_1" {
name = "net_1"
}
resource "ecl_network_subnet_v2" "sub_1" {
cidr = "192.168.1.0/24"
network_id = ecl_network_network_v2.net_1.id
allocation_pools {
start = "192.168.1.10"
end = "192.168.1.20"
}
}
output "allocation_pools" {
value = { for k, v in ecl_network_subnet_v2.sub_1.allocation_pools[0] : k => "${k} ip is ${v}" }
}
outputはこうなります。
Outputs:
allocation_pools = {
"end" = "end ip is 192.168.1.20"
"start" = "start ip is 192.168.1.10"
}
Python的にはこんなことをやっているイメージですね。
>>> d = {"start": "192.168.1.0", "end": "192.168.1.20"}
>>> {k: "%s ip is %s" % (k, v) for k, v in d.items()}
{'start': 'start ip is 192.168.1.0', 'end': 'end ip is 192.168.1.20'}
ちなみに・・・
ちなみにこんな表記もありますね。
Finally, if the result type is an object (using { and } delimiters) then the value result expression can be followed by the ... symbol to group together results that have a common key:
例に出ている ...
について、ちょっと意味がわからなかったのでやってみました。
resource "ecl_imagestorages_image_v2" "img_1" {
name = "net_1"
container_format = "bare"
disk_format = "qcow2"
local_file_path = "./image.txt"
tags = [
"imagetag1",
"imagetag2",
"Imagetag3", // <-- ここだけ "I"mageと冒頭大文字
]
}
output "image_tags" {
value = { for s in ecl_imagestorages_image_v2.img_1.tags : substr(s, 0, 1) => s... if s != "" }
}
outputはこうなります。
Outputs:
image_tags = {
"I" = [
"Imagetag3",
]
"i" = [
"imagetag1",
"imagetag2",
]
}
なるほど。substrでキーが一致したものを同一リストに入れちゃいますよという意味ですね。
一旦格納したリストを展開して入れ込み直すという意味でnodeの以下みたいな動きですかね。
% node
> let a = [1, 2, 3]
undefined
> let b = [4, 5, 6]
undefined
> let c = [...a, ...b]
undefined
> c
[ 1, 2, 3, 4, 5, 6 ]
dynamic blocksが使えるようになった
どうも記載されている例が自分的にはわかりにくかったのでやってみました。
例で使っているのはこちらのリソースです。
結果的に思うことは、確かにコンフィグの行数は減って便利そう。
ただ注意書きにもあるように、 使うすぎるとわかりにくくなるからね
というのは同感です。
list利用編
例えば以下のようなリソース定義をしたい場合を考えます。
resource "ecl_compute_instance_v2" "instance_1" {
name = "instance_1"
flavor_id = "some_flavor_id"
image_id = "some_image_id"
network = {
uuid = "uuid-1"
}
network = {
uuid = "uuid-2"
}
network = {
uuid = "uuid-3"
}
network = {
uuid = "uuid-4"
}
}
例えばこれを、以下のように書くことができるそうです。
locals {
instance_networks = [
"uuid-1",
"uuid-2",
"uuid-3",
"uuid-4",
]
}
resource "ecl_compute_instance_v2" "instance_1" {
name = "instance_1"
flavor_id = "some_flavor_id"
image_id = "some_image_id"
dynamic "network" {
for_each = local.instance_networks
iterator = instance_net
content {
uuid = instance_net.value
}
}
}
- dynamic "xxxx" の
"xxxx"
の部分で、動的に作りたい(リソースにとっての)Argument
を指定します。ここではdynamic "network"
なので、networkというArgumentを動的に作りたい!
と宣言してます - dynamic block内の
for_each
で、この中でループさせたい変数を記載します。ここではlocal.instance_networks
をループさせてます - dynamic block内の
iterator
で、ループされた結果のローカル変数名(pythonとかでいえばfor i in range(...) の i に相当
を記載します。省略も可能で、省略した場合はdynamicブロックの名前が同様の意味合いに使われるようです。(つまり dynamicなnetwork
ブロック内で、network
というローカル変数を使うと、繰り返しの要素を意味する変数になる ) - 上記のローカル変数の
.value
で繰り返しの値を参照できます。mapの場合は.key
も使えるのですが、それについては別途次に説明します
上記の terraform plan
結果は以下のようになります。
# ecl_compute_instance_v2.instance_1 will be created
+ resource "ecl_compute_instance_v2" "instance_1" {
+ name = "instance_1"
(略)
+ network {
+ access_network = false
+ fixed_ip_v4 = (known after apply)
+ mac = (known after apply)
+ name = (known after apply)
+ port = (known after apply)
+ uuid = "uuid-1"
}
+ network {
+ access_network = false
+ fixed_ip_v4 = (known after apply)
+ mac = (known after apply)
+ name = (known after apply)
+ port = (known after apply)
+ uuid = "uuid-2"
}
+ network {
+ access_network = false
+ fixed_ip_v4 = (known after apply)
+ mac = (known after apply)
+ name = (known after apply)
+ port = (known after apply)
+ uuid = "uuid-3"
}
+ network {
+ access_network = false
+ fixed_ip_v4 = (known after apply)
+ mac = (known after apply)
+ name = (known after apply)
+ port = (known after apply)
+ uuid = "uuid-4"
}
}
意図した繰り返しになってくれているようです。
とはいえ、なんか書き方が直感的じゃないなぁ・・・。
map利用編
繰り返しの対象をmapにした場合も書いてみます。
最終的に以下のような形を作りたいとしましょう。
resource "ecl_compute_instance_v2" "instance_1" {
name = "instance_1"
flavor_id = "some_flavor_id"
image_id = "some_image_id"
network = {
uuid = "uuid-1"
fixed_ip_v4 = "192.168.1.10"
}
network = {
uuid = "uuid-2"
fixed_ip_v4 = "192.168.1.20"
}
network = {
uuid = "uuid-3"
fixed_ip_v4 = "192.168.1.30"
}
network = {
uuid = "uuid-4"
fixed_ip_v4 = "192.168.1.40"
}
}
この場合は次のようにかけます。
locals {
instance_networks = {
"uuid-1" = "192.168.1.10",
"uuid-2" = "192.168.2.10",
"uuid-3" = "192.168.3.10",
"uuid-4" = "192.168.4.10",
}
}
resource "ecl_compute_instance_v2" "instance_1" {
name = "instance_1"
flavor_id = "some_flavor_id"
image_id = "some_image_id"
dynamic "network" {
for_each = local.instance_networks
iterator = instance_net
content {
uuid = instance_net.key
fixed_ip_v4 = instance_net.value
}
}
}
planはこうなります。
# ecl_compute_instance_v2.instance_1 will be created
+ resource "ecl_compute_instance_v2" "instance_1" {
+ name = "instance_1"
(略)
+ network {
+ access_network = false
+ fixed_ip_v4 = "192.168.1.10"
+ mac = (known after apply)
+ name = (known after apply)
+ port = (known after apply)
+ uuid = "uuid-1"
}
+ network {
+ access_network = false
+ fixed_ip_v4 = "192.168.2.10"
+ mac = (known after apply)
+ name = (known after apply)
+ port = (known after apply)
+ uuid = "uuid-2"
}
+ network {
+ access_network = false
+ fixed_ip_v4 = "192.168.3.10"
+ mac = (known after apply)
+ name = (known after apply)
+ port = (known after apply)
+ uuid = "uuid-3"
}
+ network {
+ access_network = false
+ fixed_ip_v4 = "192.168.4.10"
+ mac = (known after apply)
+ name = (known after apply)
+ port = (known after apply)
+ uuid = "uuid-4"
}
}
-
ローカル変数.key
,ローカル変数.value
でそれぞれkey, valueを参照します。 - for_eachの結果は、勝手にpythonでいう items() っぽくなってくれるようですね。
なかなか便利。
Directivesが使えるようになった
よく他言語のtemplate系でも見かける(のに親しい)
%{ if BOOL } %{ else } %{ endif }
%{ for NAME in <COLLECTION> } %{ endfor }
が使えるようになったようです。
(%が外側になるのが違和感ですが )
ただしこれ、実は String Templatesの章配下なんですよね。なので、無条件で使えるわけではないことになります。
%{ if BOOL } %{ else } %{ endif }
いける例
例えばこういうのはいけますが・・・。
variable "is_prod" {
type = bool
default = false
}
resource "ecl_compute_instance_v2" "instance_1" {
name = "%{ if var.is_prod == true }prod%{ else }stage%{ endif }-instance"
flavor_id = "some_flavor_id"
image_id = "some_image_id"
}
この状態で terraform apply
。
% terraform apply (git)-[master]
(略)
# ecl_compute_instance_v2.instance_1 will be created
+ resource "ecl_compute_instance_v2" "instance_1" {
(略)
+ name = "stage-instance"
今度は変数値を明示。
% terraform apply -var 'is_prod=true' (git)-[master]
(略)
# ecl_compute_instance_v2.instance_1 will be created
+ resource "ecl_compute_instance_v2" "instance_1" {
(略)
+ name = "prod-instance"
こんな感じで if - else が切り替わります。
いけない例
ですが、こういうのは無理ですね
まぁだめだって書いてあるんだから当然。
variable "is_prod" {
type = bool
default = false
}
%{ if var.is_prod }
resource "ecl_compute_instance_v2" "instance_1" {
name = "prod-instance"
flavor_id = "some_flavor_id"
image_id = "some_image_id"
}
%{ else }
resource "ecl_compute_instance_v2" "instance_1" {
name = "stage-instance"
flavor_id = "some_flavor_id"
image_id = "some_image_id"
}
%{ endif }
自分は ifが使える!
と聞いたときこういうのを想像しましたがだめでした。(もちろんforも同じように考えた)
まぁ、Terraformの作りからして、たしかにこういう書き方は必要がないですもんね。
あと気づいたのは、 %{ else-if }
は無いんですよね。
あまり使わないでしょうしね。
%{ for NAME in <COLLECTION> } %{ endfor }
例えばこんな感じになります。
locals {
instance_networks = {
"[uuid-1]" = "192.168.1.10",
"[uuid-2]" = "192.168.2.10",
"[uuid-3]" = "192.168.3.10",
"[uuid-4]" = "192.168.4.10",
}
}
resource "ecl_compute_instance_v2" "instance_1" {
name = "instance_1"
flavor_id = "1CPU-4GB"
image_id = "c11a6d55-70e9-4d04-a086-4451f07da0d7"
dynamic "network" {
for_each = local.instance_networks
iterator = instance_net
content {
uuid = instance_net.key
fixed_ip_v4 = instance_net.value
}
}
}
output "server_networks" {
value = <<EOF
%{for i in ecl_compute_instance_v2.instance_1.network~}
Instance IP for network uuid ${i.uuid} is ${i.fixed_ip_v4}
%{endfor}
EOF
}
結果はこちら。
Outputs:
server_networks = Instance IP for network uuid [uuid-4] is 192.168.4.10
Instance IP for network uuid [uuid-2] is 192.168.2.10
Instance IP for network uuid [uuid-3] is 192.168.3.10
Instance IP for network uuid [uuid-1] is 192.168.1.10
%{ for
の先頭もしくは末尾に ~
を付与することで、改行とかスペースの扱いが変わるとも書いてありますね。
今回は末尾に追加しましたが。
以上Expression編でした。