Terraform 0.12.6からつかえるようになったresourceのfor_eachを試してみた

## はじめに

今朝方こんなTweetがあったのを見た方も多いかと思います。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr">Terraform 0.12.6 is released and includes FOR EACH FOR RESOURCES. 👾 <a href="https://t.co/ZVWOzZKDQU">https://t.co/ZVWOzZKDQU</a><br><br>Send your 👏 to <a href="https://twitter.com/pamasaur?ref_src=twsrc%5Etfw">@pamasaur</a> for implementing this amazing feature! <a href="https://t.co/wZbOxCx7zZ">pic.twitter.com/wZbOxCx7zZ</a></p>&mdash; Mitchell Hashimoto (@mitchellh) <a href="https://twitter.com/mitchellh/status/1156661893789966336?ref_src=twsrc%5Etfw">July 31, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

`リソースでfor_eachがつかえる` のか、なるほど。

これがどういうことなのかを実際に試してみました。

## 結論

例えばこんな書き方ができます。

```go
variable "keyname_servername" {
  type = map(string)
  default = {
    server_1 = "keypair_1"
    server_2 = "keypair_2"
    server_3 = "keypair_3"
  }
}

resource "ecl_compute_keypair_v2" "keypair_1" {
  for_each = var.keyname_servername
  name     = each.value
}

resource "ecl_compute_instance_v2" "instance_1" {
  for_each = var.keyname_servername

  name        = each.key
  flavor_name = "1CPU-4GB"
  image_name  = "Ubuntu-18.04.1_64_virtual-server_02"
  key_pair    = ecl_compute_keypair_v2.keypair_1["${each.key}"].name
}
```

- 繰り返し処理内で作っているKeypairを、それぞれ繰り返し内で処理しているインスタンスに埋め込みます。
- その際対応関係もしっかり指定します
    - 1つ目のkeypairを1つ目のインスタンスに埋め込む
    - 2つ目のkeypairを2つ目のインスタンスに埋め込む
    - 3つ目のkeypairを3つ目のインスタンスに埋め込む

では、上記のHCLがどういう意味になるのかを1つずつ書いてみます。

## for_eachを書くと何が起きるのか

### まずは繰り返し処理が起きるということなのです

リソースの定義内にfor_eachを書くと繰り返し処理が行われます。
これは、直接mapやlistをかいてもいいのですが、変数から参照することも可能です。

ということで、まずは変数を定義しdefault値を付与。それがここ。

```go
variable "keyname_servername" {
  type = map(string)
  default = {
    server_1 = "keypair_1"
    server_2 = "keypair_2"
    server_3 = "keypair_3"
  }
}
```

key-valueなmapを生成しています。

で、それをリソース定義内から参照しています。
それがここ。（ここで関係がない部分は省略)

```go
resource "ecl_compute_keypair_v2" "keypair_1" {
  for_each = var.keyname_servername <-- ここと
  (略)
}

resource "ecl_compute_instance_v2" "instance_1" {
  for_each = var.keyname_servername <-- ここ

  (略)
}
```

### で、その繰り返し処理で作られたリソースをHCL内で参照するには？

例えばここだけを見てみます。

```go
variable "servername_keynamr" {
  type = map(string)
  default = {
    server_1 = "keypair_1"
    server_2 = "keypair_2"
    server_3 = "keypair_3"
  }
}

resource "ecl_compute_keypair_v2" "keypair_1" {
  for_each = var.keyname_servername <-- ここと
  (略)
}
```

この時作られるリソースは、以下のような識別子になります。

- `ecl_compute_keypair_v2.keypair_1["server_1"]`
- `ecl_compute_keypair_v2.keypair_1["server_2"]`
- `ecl_compute_keypair_v2.keypair_1["server_3"]`

つまりfor_eachのkeyが、 `[]` 付きでリソース識別子の後ろにつくわけですね。
この書き方を使えば、for_eachで作られたリソースの属性を参照することが出来ます。


## `each.key`, `each.value` ってなに？

一番最初の例に、 `each.key` , `each.value` ってのが出てましたよね。
これは何なのかというと、 `for_each` 指定している対象のkeyとvalueです。

今回はfor_eachのソースはこれ。

```go
variable "keyname_servername" {
  type = map(string)
  default = {
    server_1 = "keypair_1"
    server_2 = "keypair_2"
    server_3 = "keypair_3"
  }
}
```

ということで、 `each.key` は、 `server_1`, `server_2` , `server_3` になりますし、`each.value` は、 `keypair_1`, `keypair_2` , `keypair_3` になります。


## 最初に戻って・・・

改めて見てみます。

```go
variable "keyname_servername" {
  type = map(string)
  default = {
    server_1 = "keypair_1"
    server_2 = "keypair_2"
    server_3 = "keypair_3"
  }
}

resource "ecl_compute_keypair_v2" "keypair_1" {
  for_each = var.keyname_servername
  name     = each.value
}

resource "ecl_compute_instance_v2" "instance_1" {
  for_each = var.keyname_servername

  name        = each.key
  flavor_name = "1CPU-4GB"
  image_name  = "Ubuntu-18.04.1_64_virtual-server_02"
  key_pair    = ecl_compute_keypair_v2.keypair_1["${each.key}"].name
}
```

特にここ。

```go
  key_pair    = ecl_compute_keypair_v2.keypair_1["${each.key}"].name
```

これはつまり、

- for_eachで作られるキーペア3点はそれぞれ以下の識別子になる
    - `ecl_compute_keypair_v2.keypair_1["server_1"]`
    - `ecl_compute_keypair_v2.keypair_1["server_2"]`
    - `ecl_compute_keypair_v2.keypair_1["server_3"]`
- これをinterpolation syntaxで書くとこうなる
    - `ecl_compute_keypair_v2.keypair_1["${each.key}"]`
- で、それをインスタンス内から参照しつつ、そのname属性を埋め込むとこうなる
    - `key_pair    = ecl_compute_keypair_v2.keypair_1["${each.key}"].name`

・・・ということをやっていることになります。

実際には今回の例はあまりfor_eachしなくてもいい気がしてしまいますが、こんな感じで書けるようですね。


## 実際にapplyしてみた

こんな感じになりました。

```bash
% terraform apply                                                                              

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # ecl_compute_instance_v2.instance_1["server_1"] will be created
  + resource "ecl_compute_instance_v2" "instance_1" {
      + access_ip_v4        = (known after apply)
      + all_metadata        = (known after apply)
      + availability_zone   = (known after apply)
      + flavor_id           = (known after apply)
      + flavor_name         = "1CPU-4GB"
      + id                  = (known after apply)
      + image_id            = (known after apply)
      + image_name          = "Ubuntu-18.04.1_64_virtual-server_02"
      + key_pair            = "keypair_1"
      + name                = "server_1"
      + power_state         = "active"
      + region              = (known after apply)
      + stop_before_destroy = false

      + network {
          + access_network = (known after apply)
          + fixed_ip_v4    = (known after apply)
          + mac            = (known after apply)
          + name           = (known after apply)
          + port           = (known after apply)
          + uuid           = (known after apply)
        }
    }

  # ecl_compute_instance_v2.instance_1["server_2"] will be created
  + resource "ecl_compute_instance_v2" "instance_1" {
      + access_ip_v4        = (known after apply)
      + all_metadata        = (known after apply)
      + availability_zone   = (known after apply)
      + flavor_id           = (known after apply)
      + flavor_name         = "1CPU-4GB"
      + id                  = (known after apply)
      + image_id            = (known after apply)
      + image_name          = "Ubuntu-18.04.1_64_virtual-server_02"
      + key_pair            = "keypair_2"
      + name                = "server_2"
      + power_state         = "active"
      + region              = (known after apply)
      + stop_before_destroy = false

      + network {
          + access_network = (known after apply)
          + fixed_ip_v4    = (known after apply)
          + mac            = (known after apply)
          + name           = (known after apply)
          + port           = (known after apply)
          + uuid           = (known after apply)
        }
    }

  # ecl_compute_instance_v2.instance_1["server_3"] will be created
  + resource "ecl_compute_instance_v2" "instance_1" {
      + access_ip_v4        = (known after apply)
      + all_metadata        = (known after apply)
      + availability_zone   = (known after apply)
      + flavor_id           = (known after apply)
      + flavor_name         = "1CPU-4GB"
      + id                  = (known after apply)
      + image_id            = (known after apply)
      + image_name          = "Ubuntu-18.04.1_64_virtual-server_02"
      + key_pair            = "keypair_3"
      + name                = "server_3"
      + power_state         = "active"
      + region              = (known after apply)
      + stop_before_destroy = false

      + network {
          + access_network = (known after apply)
          + fixed_ip_v4    = (known after apply)
          + mac            = (known after apply)
          + name           = (known after apply)
          + port           = (known after apply)
          + uuid           = (known after apply)
        }
    }

  # ecl_compute_keypair_v2.keypair_1["server_1"] will be created
  + resource "ecl_compute_keypair_v2" "keypair_1" {
      + fingerprint = (known after apply)
      + id          = (known after apply)
      + name        = "keypair_1"
      + private_key = (known after apply)
      + public_key  = (known after apply)
      + region      = (known after apply)
    }

  # ecl_compute_keypair_v2.keypair_1["server_2"] will be created
  + resource "ecl_compute_keypair_v2" "keypair_1" {
      + fingerprint = (known after apply)
      + id          = (known after apply)
      + name        = "keypair_2"
      + private_key = (known after apply)
      + public_key  = (known after apply)
      + region      = (known after apply)
    }

  # ecl_compute_keypair_v2.keypair_1["server_3"] will be created
  + resource "ecl_compute_keypair_v2" "keypair_1" {
      + fingerprint = (known after apply)
      + id          = (known after apply)
      + name        = "keypair_3"
      + private_key = (known after apply)
      + public_key  = (known after apply)
      + region      = (known after apply)
    }

Plan: 6 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

ecl_compute_keypair_v2.keypair_1["server_2"]: Creating...
ecl_compute_keypair_v2.keypair_1["server_3"]: Creating...
ecl_compute_keypair_v2.keypair_1["server_1"]: Creating...
ecl_compute_keypair_v2.keypair_1["server_2"]: Creation complete after 2s [id=keypair_2]
ecl_compute_keypair_v2.keypair_1["server_3"]: Creation complete after 2s [id=keypair_3]
ecl_compute_keypair_v2.keypair_1["server_1"]: Creation complete after 2s [id=keypair_1]
ecl_compute_instance_v2.instance_1["server_3"]: Creating...
ecl_compute_instance_v2.instance_1["server_1"]: Creating...
ecl_compute_instance_v2.instance_1["server_2"]: Creating...
ecl_compute_instance_v2.instance_1["server_1"]: Still creating... [10s elapsed]
ecl_compute_instance_v2.instance_1["server_3"]: Still creating... [10s elapsed]
ecl_compute_instance_v2.instance_1["server_2"]: Still creating... [10s elapsed]
ecl_compute_instance_v2.instance_1["server_3"]: Creation complete after 13s [id=a3bf0f56-ad5f-4c32-aefa-9b2137041bba]
ecl_compute_instance_v2.instance_1["server_2"]: Creation complete after 13s [id=6f6b1979-f651-4dd4-9395-bd5b4385c9c7]
ecl_compute_instance_v2.instance_1["server_1"]: Creation complete after 13s [id=bfef921c-0be8-4c29-b691-5f887be0cbe6]

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

こんな感じでうまくいきました。

## もう少し実用的な例

ただ、あんまりこれは実用的じゃなかったかな・・・と思ったので、もう少し実用的な例。
というのは、上記のインスタンスとキーペアの例だと、本来 `(known after apply)` として出てくれたほうがそれっぽいはずのキーペア名が事前にわかっちゃってますからね。


```go
variable "keyname_servername" {
  type = map(string)
  default = {
    net_1 = "192.168.1.0/24"
    net_2 = "192.168.2.0/24"
    net_3 = "192.168.3.0/24"
  }
}

resource "ecl_network_network_v2" "network_1" {
  for_each = var.keyname_servername
  name     = each.key
}

resource "ecl_network_subnet_v2" "subnet_1" {
  for_each   = var.keyname_servername
  cidr       = each.value
  network_id = ecl_network_network_v2.network_1["${each.key}"].id
  no_gateway = true
}
```

これだとapply時にこんな感じ。

```bash
% terraform apply                                                            (git)-[master]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # ecl_network_network_v2.network_1["net_1"] will be created
  + resource "ecl_network_network_v2" "network_1" {
      + admin_state_up = (known after apply)
      + id             = (known after apply)
      + name           = "net_1"
      + plane          = "data"
      + region         = (known after apply)
      + shared         = (known after apply)
      + status         = (known after apply)
      + subnets        = (known after apply)
      + tenant_id      = (known after apply)
    }

  # ecl_network_network_v2.network_1["net_2"] will be created
  + resource "ecl_network_network_v2" "network_1" {
      + admin_state_up = (known after apply)
      + id             = (known after apply)
      + name           = "net_2"
      + plane          = "data"
      + region         = (known after apply)
      + shared         = (known after apply)
      + status         = (known after apply)
      + subnets        = (known after apply)
      + tenant_id      = (known after apply)
    }

  # ecl_network_network_v2.network_1["net_3"] will be created
  + resource "ecl_network_network_v2" "network_1" {
      + admin_state_up = (known after apply)
      + id             = (known after apply)
      + name           = "net_3"
      + plane          = "data"
      + region         = (known after apply)
      + shared         = (known after apply)
      + status         = (known after apply)
      + subnets        = (known after apply)
      + tenant_id      = (known after apply)
    }

  # ecl_network_subnet_v2.subnet_1["net_1"] will be created
  + resource "ecl_network_subnet_v2" "subnet_1" {
      + cidr              = "192.168.1.0/24"
      + dns_nameservers   = (known after apply)
      + enable_dhcp       = (known after apply)
      + gateway_ip        = (known after apply)
      + id                = (known after apply)
      + ip_version        = 4
      + ipv6_address_mode = (known after apply)
      + ipv6_ra_mode      = (known after apply)
      + network_id        = (known after apply)
      + no_gateway        = true
      + region            = (known after apply)
      + status            = (known after apply)
      + tenant_id         = (known after apply)

      + allocation_pools {
          + end   = (known after apply)
          + start = (known after apply)
        }
    }

  # ecl_network_subnet_v2.subnet_1["net_2"] will be created
  + resource "ecl_network_subnet_v2" "subnet_1" {
      + cidr              = "192.168.2.0/24"
      + dns_nameservers   = (known after apply)
      + enable_dhcp       = (known after apply)
      + gateway_ip        = (known after apply)
      + id                = (known after apply)
      + ip_version        = 4
      + ipv6_address_mode = (known after apply)
      + ipv6_ra_mode      = (known after apply)
      + network_id        = (known after apply)
      + no_gateway        = true
      + region            = (known after apply)
      + status            = (known after apply)
      + tenant_id         = (known after apply)

      + allocation_pools {
          + end   = (known after apply)
          + start = (known after apply)
        }
    }

  # ecl_network_subnet_v2.subnet_1["net_3"] will be created
  + resource "ecl_network_subnet_v2" "subnet_1" {
      + cidr              = "192.168.3.0/24"
      + dns_nameservers   = (known after apply)
      + enable_dhcp       = (known after apply)
      + gateway_ip        = (known after apply)
      + id                = (known after apply)
      + ip_version        = 4
      + ipv6_address_mode = (known after apply)
      + ipv6_ra_mode      = (known after apply)
      + network_id        = (known after apply)
      + no_gateway        = true
      + region            = (known after apply)
      + status            = (known after apply)
      + tenant_id         = (known after apply)

      + allocation_pools {
          + end   = (known after apply)
          + start = (known after apply)
        }
    }

Plan: 6 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

ecl_network_network_v2.network_1["net_2"]: Creating...
ecl_network_network_v2.network_1["net_1"]: Creating...
ecl_network_network_v2.network_1["net_3"]: Creating...
ecl_network_network_v2.network_1["net_1"]: Creation complete after 6s [id=3a6533b5-da6e-4bd8-8c56-73bfd90feee4]
ecl_network_network_v2.network_1["net_3"]: Creation complete after 6s [id=7172d43f-b1a9-4661-991e-e9c048021bbf]
ecl_network_network_v2.network_1["net_2"]: Creation complete after 6s [id=2b380a27-6b95-43f5-a580-f72e4df2d566]
ecl_network_subnet_v2.subnet_1["net_1"]: Creating...
ecl_network_subnet_v2.subnet_1["net_3"]: Creating...
ecl_network_subnet_v2.subnet_1["net_2"]: Creating...
ecl_network_subnet_v2.subnet_1["net_1"]: Creation complete after 5s [id=ae222a23-7384-473b-9fcf-2d94f3b08b8b]
ecl_network_subnet_v2.subnet_1["net_3"]: Creation complete after 5s [id=aeb6d40c-3755-4a45-baf5-eeab076675ed]
ecl_network_subnet_v2.subnet_1["net_2"]: Creation complete after 5s [id=8c157345-8a0e-4d20-9280-313297090269]

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

作成後のリソースを見てみると、ちゃんとsubnetからidを参照して、リソース感が結合できてます。

```bash
% ecl --os-cloud=jp4 network logical-network list
+--------------------------------------+-----------+--------------------------------------+--------+
| ID                                   | Name      | Subnets                              | Status |
+--------------------------------------+-----------+--------------------------------------+--------+
| 2b380a27-6b95-43f5-a580-f72e4df2d566 | net_2     | 8c157345-8a0e-4d20-9280-313297090269 | ACTIVE |
| 3a6533b5-da6e-4bd8-8c56-73bfd90feee4 | net_1     | ae222a23-7384-473b-9fcf-2d94f3b08b8b | ACTIVE |
| 7172d43f-b1a9-4661-991e-e9c048021bbf | net_3     | aeb6d40c-3755-4a45-baf5-eeab076675ed | ACTIVE |
+--------------------------------------+-----------+--------------------------------------+--------+
```


以上、まずは取り急ぎ検証してみた結果でした。