Terraformの繰り返し処理(メタ引数編)

# Terraformの繰り返し処理の種類

Terraformには`for`,`for_each`,`count`の3つの繰り返し処理があります。

# 要約
|分類|　　|説明|
|:---|:---|:---|
|式|for|繰り返し処理を行える。感覚的にはPythonなどと同じイメージ。今回は説明しない。|
|メタ引数|for_each|mapを利用した繰り返し処理。|
||count|単純な指定値による繰り返し処理。もっともイメージがつきやすい。|

今回は`for`には触れません。

# count
countはもっとも理解しやすい繰り返し処理ではなかろーか。
例えば以下のようにします。(コードは先日書いた記事のものから。Providerは省略)

```terraform
# リージョン指定
variable "location" {
  type    = string
  default = "japaneast"
}

# サブネットの名前リスト
variable "subnet_name" {
  description = ""
  default = [
    "subnet-1",
    "subnet-2",
    "subnet-3",
    "subnet-4"
  ]
}
# サブネットのIPリスト
variable "subnet_address" {
  default = [
    "10.0.0.0/26",
    "10.0.0.64/26",
    "10.0.0.128/26",
    "10.0.0.192/26",
  ]
}
# リソースグループを作成する
resource "azurerm_resource_group" "rg" {
  name     = "rg-test"
  location = var.location
}
# vNetを作成する
resource "azurerm_virtual_network" "vnet" {
  name                = "vnet-test-01"
  location            = var.location
  resource_group_name = azurerm_resource_group.rg.name
  address_space       = ["10.0.0.0/16"]
}
# サブネットを作成する
resource "azurerm_subnet" "subnet" {
  count                = length(var.subnet_name)
  name                 = element(var.subnet_name, count.index)
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = [element(var.subnet_address, count.index)]
  resource_group_name  = azurerm_resource_group.rg.name
}
```
countを利用しているのは最後のsubnet作成のときです。このコードを実行すると、4つサブネットが生成されます。
`count`は単純に __数字の回数分だけ__ そのブロックの処理を繰り返します。
`count.index`を利用するとその繰り返した回数を取得できます。
要素がlistで管理されている場合は、countで指定するとリストの長さの分だけ繰り返す(countとlengthを合わせて利用する)ので簡単に作成できます。

以下plan結果。(subnetのみ抜粋)
```terminal
  # azurerm_subnet.subnet[0] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefixes                               = [
          + "10.0.0.0/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "subnet-1"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-test"
      + virtual_network_name                           = "vnet-test-01"
    }

  # azurerm_subnet.subnet[1] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefixes                               = [
          + "10.0.0.64/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "subnet-2"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-test"
      + virtual_network_name                           = "vnet-test-01"
    }
…(以下省略)
```

しかしながら、`count`だと困ることがあります。
例えば`count`で作成したサブネットの名前を別リソースで呼び出したいとき、それぞれ以下のように呼び出すのですが…

```terraform
# 一つ目のサブネット
azurerm_subnet.subnet[0].name
# 二つ目のサブネット
azurerm_subnet.subnet[1].name
# 三つ目のサブネット
azurerm_subnet.subnet[2].name
# 四つ目のサブネット
azurerm_subnet.subnet[3].name
```
これだと、ソースコード上は`azurerm_subnet.subnet[0]`と`azurerm_subnet.subnet[3]`の違いが一目でわからない。
__「このサブネットってなんのリソースで利用するサブネットだっけ…？」__ などなど、コードの可読性に大きく影響します。

また、もし`subnet-2`が不要になり、`length(var.subnet_name)`が3となった場合、順番的に`azurerm_subnet.subnet[3]`が生成されなくなります。
コードの修正はそれほど時間がかからないかもしれないですが、すでにapply済みであった場合、`azurerm_subnet.subnet[3]`がなくなることでその上に構築されたリソースも含めて再作成・再設定が行われることになり、大きな変更となってしまいます。

そのため、__昨今では基本的には`for_each`を利用すべき__、といわれています。

# for_each

`for_each`は`map`を定義する前提で、その`map`の要素分繰り返すことができます。
イメージつきにくいかと思いますが、上の例と同じことをやると、以下のようになります。

```terraform
variable "location" {
  type    = string
  default = "japaneast"
}
# 名前とIPのセットのmap
variable "subnet" {
  type = map(string)
  default = {
      subnet-1 = "10.0.0.0/26"
      subnet-2 = "10.0.0.64/26"
      subnet-3 = "10.0.0.128/26"
      subnet-4 = "10.0.0.192/26"
      }
}
# リソースグループを作成する
resource "azurerm_resource_group" "rg" {
  name     = "rg-test"
  location = var.location
}
# vNetを作成する
resource "azurerm_virtual_network" "vnet" {
  name                = "vnet-test-01"
  location            = var.location
  resource_group_name = azurerm_resource_group.rg.name
  address_space       = ["10.0.0.0/16"]
}
resource "azurerm_subnet" "subnet" {
  # var.subnet_nameの数分だけ繰り返す
  for_each             = var.subnet
  name                 = each.key
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = [each.value]
  resource_group_name  = azurerm_resource_group.rg.name
}
```

`subnet-1 = "10.0.0.0/26"`
のうち、右側の「subnet-1」がkey,左側の「"10.0.0.0/26"」がvalue。
これらを`each.key`や`each.value`で参照できます。

出力
```terminal
(前略)
  # azurerm_subnet.subnet["subnet-1"] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefixes                               = [
          + "10.0.0.0/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "subnet-1"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-test"
      + virtual_network_name                           = "vnet-test-01"
    }

  # azurerm_subnet.subnet["subnet-2"] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefixes                               = [
          + "10.0.0.64/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "subnet-2"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-test"
      + virtual_network_name                           = "vnet-test-01"
    }
…(以下略)
```


countの場合は`azurerm_subnet.subnet[0].name`で指定が必要でしたが、`for_each`の場合は`azurerm_subnet.subnet["subnet-1"].name`で指定できます。
つまり、サブネット名のKeyで区別がつきやすいです。これが`for_each`の利点になります。また、特定の要素を削除したとしても他への影響が少ないです。

`for`と合わせると、また違った繰り返し処理が実装できますが…。それはまた今度ということで。
(追記)以下で紹介しています。

https://qiita.com/hikaru_motomiya/items/fdd784adb5134c31120c

# 参考(公式ドキュメント)

count
https://developer.hashicorp.com/terraform/language/meta-arguments/count

for_each
https://developer.hashicorp.com/terraform/language/meta-arguments/for_each