Terraform 0.12の差分: Expresson編

[前回](https://qiita.com/keiichi-hikita/items/82f37b69c8263f52f9aa) に引き続き、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は扱うようですね。

となると、

1. .tf側でキーがあって、値が正常域外
2. .tf側でキーがあって、値が正常域
3. .tf側でキーがあって、値がnull
4. .tf側でキーがない

という4パターン、および、

A. パラメータがProvider側でRequired扱い
B. パラメータがProvider側でOptional扱いだがDefault値がある
C. パラメータがProvider側でOptional扱いでありDefault値も設定されていない

のそれぞれでどうなるのか・・・を考えてみます。
まぁ、大差はないだろうという予測のもとやってみます。  :sweat_smile: 

| | A. パラメータがProvider側でRequired扱い | B. パラメータがProvider側でOptional扱いだがDefault値がある | C. パラメータがProvider側でOptional扱いでありDefault値も設定されていない |
|:--|:--|:--|:--|
| **1. .tf側でキーがあって、値が正常域外** | ValidateFuncに弾かれる | ValidateFuncに弾かれる |ValidateFuncに弾かれる |
| **2. .tf側でキーがあって、値が正常域** | 付与された値を利用して後続処理へ | 付与された値を利用して後続処理へ | 付与された値を利用して後続処理へ |
| **3. .tf側でキーがあって、値がnull** | エラーになる<br/>(`required field is not set`)| 正常処理<br/>(Default値が利用される) | planに乗ってこない<br/>以降の処理はプロバイダの作りによる |
| **4. .tf側でキーがない** | エラーになる<br/>(`Missing required argument`) |正常処理<br/>(Default値が利用される)  | planに乗ってこない<br/>以降の処理はプロバイダの作りによる |

nullを指定したときと、キーがないときでエラーが違うんですね。
Requiredの時のエラーメッセージが違うくらいで、ほぼ同じ挙動ですね。

## list(map)の挙動の差

これは明言されているわけではない（様に見える）のですが、以下の挙動が変わってますよね。

### 0.11の場合

list of mapsを指定したいときに、以下の指定ができました。

```go

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` すると、

```bash
  + 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でも通る)

```go
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](https://www.terraform.io/docs/configuration-0-11/interpolation.html)では `変数使うときは ${ ... }ね` と書かれていたのが、基本的にその表記が不要となりましたね。

直接的には、 [References to Resource Attributes](https://www.terraform.io/docs/configuration/expressions.html#references-to-resource-attributes)を読むと差分として理解しやすいのだと思います。
（ちゃんと冒頭からその旨が書いてあるのですが）

## `*` の利用対象が拡大した

0.11までは、count > 1 で作成した場合にリソースをインデックス経由で指定する用途にしか使えなかったと思うのですが、attributeにも使えるようになっったようですね。

```go
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` リソースを２つ作ります
- 上記の`ecl_imagestorages_image_v2`はtagsという文字列のリストを持っています
- outputで、そのtagsを `index=*` で参照しています

上記のtfファイルを利用した場合、結果的にoutputはこうなります。

```go
Outputs:

image_tags = [
  [
    "tag-0-1",
    "tag-0-2",
  ],
  [
    "tag-1-1",
    "tag-1-2",
  ],
]
```

今までだと、 `"${ecl_imagestorages_image_v2.image_1.*}"` という書き方はできても、上記のようなAttributeに対する `*`付与的な書き方はできませんでした。

だめな例ですが、0.11だと例えばこういう書き方になりますよね。

```go
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を表示する段になって落ちます。

```bash
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参照のパターン

```go
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はこうなります。

```bash
Outputs:

dns_nameservers = [
  "8.8.8.8-for-in-list",
  "8.8.4.4-for-in-list",
]
```

僕はPythonが比較的慣れているので、そっちで理解するとこんな感じ。
（`l`がdns_nameservers attribute相当）

```python
>>> 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:

```go
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はこうなります。

```bash
Outputs:

allocation_pools = {
  "end" = "end ip is 192.168.1.20"
  "start" = "start ip is 192.168.1.10"
}
```

Python的にはこんなことをやっているイメージですね。

```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:

例に出ている `...` について、ちょっと意味がわからなかったのでやってみました。

```go
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はこうなります。

```bash
Outputs:

image_tags = {
  "I" = [
    "Imagetag3",
  ]
  "i" = [
    "imagetag1",
    "imagetag2",
  ]
}
```

なるほど。substrでキーが一致したものを同一リストに入れちゃいますよという意味ですね。
一旦格納したリストを展開して入れ込み直すという意味でnodeの以下みたいな動きですかね。

```bash
% 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](https://www.terraform.io/docs/configuration/expressions.html#dynamic-blocks)が使えるようになった

どうも記載されている例が自分的にはわかりにくかったのでやってみました。

例で使っているのはこちらのリソースです。

- [ecl_compute_instance_v2](https://github.com/nttcom/terraform-provider-ecl/blob/master/ecl/resource_ecl_compute_instance_v2.go#L98-L140)

結果的に思うことは、確かにコンフィグの行数は減って便利そう。
ただ注意書きにもあるように、 `使うすぎるとわかりにくくなるからね` というのは同感です。

### list利用編

例えば以下のようなリソース定義をしたい場合を考えます。

```go
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"
  }
}
```

例えばこれを、以下のように書くことができるそうです。

```go
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` 結果は以下のようになります。

```bash
  # 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にした場合も書いてみます。

最終的に以下のような形を作りたいとしましょう。

```go
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"
  }
}
```

この場合は次のようにかけます。

```go
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はこうなります。

```bash
  # 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](https://www.terraform.io/docs/configuration/expressions.html#directives)が使えるようになった

よく他言語のtemplate系でも見かける（のに親しい）

- `%{ if BOOL } %{ else } %{ endif }`
- `%{ for NAME in <COLLECTION> } %{ endfor }`

が使えるようになったようです。
(%が外側になるのが違和感ですが :sweat_smile: ）

ただしこれ、実は [String Templates](https://www.terraform.io/docs/configuration/expressions.html#string-templates)の章配下なんですよね。なので、無条件で使えるわけではないことになります。


### `%{ if BOOL } %{ else } %{ endif }`

#### いける例

例えばこういうのはいけますが・・・。

```go
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`。

```go
% terraform apply                                                            (git)-[master]
(略)

  # ecl_compute_instance_v2.instance_1 will be created
  + resource "ecl_compute_instance_v2" "instance_1" {
      (略)
      + name                = "stage-instance"
```

今度は変数値を明示。

```go
% 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 が切り替わります。

#### いけない例

ですが、こういうのは無理ですね :smile: 
まぁだめだって書いてあるんだから当然。

```bash
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 }`

例えばこんな感じになります。

```go
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
}
```

結果はこちら。

```bash
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編でした。