Terraformのループ処理(for_each,for)について

13 min read読了の目安(約12500字

概要

Terraformのループ処理のfor_each,forについて説明します。

はじめに

Terraformではループ処理として、次の2つの処理を提供しています。

  • Terraformリソースのループ処理
  • データのループ処理

Terraformリソースのループ処理は、resourceブロックなどを繰り返し実行します。例えば、GCP内のVPCネットワークに複数のsubnetworkを作成するとき、google_compute_subnetworkリソースにループ処理を適用すると、1つのリソース定義で済みます。

データのループ処理は、maplistなどのCollection型をの値を1つずつ取り出し別のデータを作成します。例えば、小文字の文字列で定義した配列データをすべて大文字に変換しなおしたデータを生成するなどが可能になります。

Terraformリソースのループ処理

Terraformのリソースのループ処理としては以下の機能を提供しています。

  • for_each
  • count

またこの機能でループ処理できるTerraformリソースは以下の通りです。

  • resource
  • module
  • data

この記事ではfor_each文のみを紹介します。

for_eachによるリソースのループ処理

for_eachのループ処理は、Terraform v0.12からresourceブロックで提供しはじめました。また、その他のブロックは順次対応していきました。現在、2021年3月時点のGAとなっているv0.14では、resource,module,dataブロックで機能を提供しています。そのため、古いバージョンを使っている場合は利用できないので、注意してください。

入力可能なデータ型

for_eachに入力できるデータ型は、mapstringsの2つのCollection型のみとなっています。for_eachの機能は、これらCollection型のサイズ分ループ処理を実施します。

map型はkey = { var1 = value }という形式のデータです。このデータのkeyは重複しないように定める必要があります。例えば、以下のようなsubnetworksというデータに、各サブネットワークの名前をkeycidrregionを定義しているようなデータがmap型です。

locals {
  subnetworks = {
    tokyo-network = {
       cidr   = "192.168.10.0/24"
       region = "asia-northeast1"
    },
    osaka-network = {
       cidr   = "192.168.20.0/24"
       region = "asia-northeast2"
    },
  }
}

map型の出力は以下のようになります。

subnetworks = {
  "oska-network" = {
    "cidr" = "192.168.20.0/24"
    "region" = "asia-northeast2"
  }
  "tokyo-network" = {
    "cidr" = "192.168.10.0/24"
    "region" = "asia-northeast1"
  }
}

strings型は配列にprimitive型(string,number,bool)を格納したデータをtoset関数で変換したデータ型です。例えば、以下のようなsubnetworksという各サブネットワークの名前を格納した配列をtoset関数で変換したデータがstrings型です。

locals {
  subnetworks = toset([
    "tokyo-network", "osaka-network"
  ])
}

strings型の出力は以下のように、toset()が間に挟まります。

subnetworks = toset([
  "osaka-network",
  "tokyo-network",
])

for_eachの定義場所

for_eachはブロック全体のループ処理と、ブロック内部のブロックのループ処理の2つの機能を提供しています。これらの機能はfor_eachの定義場所によって異なります。

定義可能な場所は下記の2箇所になります。

  • ブロックの内部
  • リソースブロックのループさせるブロックの直前

ブロックの内部への定義

ブロックの内部への定義はブロック全体をループ処理します。例えば、google_compute_subnetworkのリソースブロックの内部に、先のmap型のsubnetworksを入力すると2回リソースを実行します。
また、map型のデータのキー値にはeach.keyで、内部の値はeach.value.<変数名>で参照します。

以下では、GCP上でsampleのVPCネットワークを作成し、そのVPCネットワークにtokyo-networkosaka-networkのサブネットワークをfor_eachを使い作成します。

locals {
  subnetworks = {
    tokyo-network = {
       cidr   = "192.168.10.0/24"
       region = "asia-northeast1"
    },
    osaka-network = {
       cidr   = "192.168.20.0/24"
       region = "asia-northeast2"
    },
  }
}

resource google_compute_subnetwork main {
  for_each = local.subnetworks
  
  name          = each.key
  ip_cidr_range = each.value.cidr
  region        = each.value.region
  network       = google_compute_network.main.id
}

resource google_compute_network main {
  name                    = "sample"
  auto_create_subnetworks = false
}

上記のように、map型の各値cidrregioneach.value.cidr,each.value.regionで参照しています。namemapのキー値としているため、each.keyで参照しています。

上記のコードの処理内容は、以下のようになります。

$ terraform plan

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:

  # google_compute_network.main will be created
  + resource "google_compute_network" "main" {
      + auto_create_subnetworks         = false
      + delete_default_routes_on_create = false
      + gateway_ipv4                    = (known after apply)
      + id                              = (known after apply)
      + mtu                             = (known after apply)
      + name                            = "sample"
      + project                         = (known after apply)
      + routing_mode                    = (known after apply)
      + self_link                       = (known after apply)
    }

  # google_compute_subnetwork.main["osaka-network"] will be created
  + resource "google_compute_subnetwork" "main" {
      + creation_timestamp         = (known after apply)
      + fingerprint                = (known after apply)
      + gateway_address            = (known after apply)
      + id                         = (known after apply)
      + ip_cidr_range              = "192.168.20.0/24"
      + name                       = "osaka-network"
      + network                    = (known after apply)
      + private_ipv6_google_access = (known after apply)
      + project                    = (known after apply)
      + region                     = "asia-northeast2"
      + secondary_ip_range         = (known after apply)
      + self_link                  = (known after apply)
    }

  # google_compute_subnetwork.main["tokyo-network"] will be created
  + resource "google_compute_subnetwork" "main" {
      + creation_timestamp         = (known after apply)
      + fingerprint                = (known after apply)
      + gateway_address            = (known after apply)
      + id                         = (known after apply)
      + ip_cidr_range              = "192.168.10.0/24"
      + name                       = "tokyo-network"
      + network                    = (known after apply)
      + private_ipv6_google_access = (known after apply)
      + project                    = (known after apply)
      + region                     = "asia-northeast1"
      + secondary_ip_range         = (known after apply)
      + self_link                  = (known after apply)
    }

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

------------------------------------------------------------------------

for_eachでループ処理をおこなうと、<block type>.<block name>.<block instance name>["each.key"]でリソースのオブジェクトが作成され。上記の例であれば、google_compute_subnetwork.main["osaka-network"],google_compute_subnetwork.main["tokyo-network"]のようにサブネットワークの各リソースオブジェクトが作成されます。ブロックがリソースの場合、<block type>は省略されます。別のresourceで生成したオブジェクトを参照する場合は、<block name>.<block instance name>["each.key"]を使います。

各ブロックごとで生成されるオブジェクト名は次のようになります。

  • resource: google_compute_subnetwork.main["tokyo-network"]
  • module: module.network["tokyo-network"] (networkモジュールをfor_eachで繰り返したとき)
  • data: data.google_compute_subnetwork.main["tokyo-network"] (google_compute_subnetworkのdataブロックをmainと名付けfor_each繰り返した場合)

また、strings型をfor_eachに入力すると、各文字列がキー値、データ値として入力されます。そのため、キーおよびデータへはeach.key,each.valueで参照できます。

リソースブロックのループさせるブロックの直前への定義

リソースブロック内のブロック直前にfor_eachを定義することで、そのブロックをfor_eachに入力されるCollection型のデータサイズ分繰り返し実行されます。この機能は、resourceブロックのみとなっています。outputmoduleでは定義できません。また、入力できるデータ型としては、strings,mapに加え配列も可能となっています。

ブロック機能の繰り返しの定義の方法は、ループされる前にdynamic <block name>と定義し、ブロックの中身の値をcontentブロックを内部に定義し値を代入します。for_eachdynamiccontentの間で定義します。

例えば、google_compute_firewallのリソース内にはallowブロックがあります。このブロックはファイアーフォールで許可する通信のプロトコルとポートを定義します。icmptcp:22, udp:80,82を許可するとき、以下のfirewall_allow_rulesと許可する通信のプロトコルとポートをオブジェクト型の配列で定義したとします。このfirewall_allow_rulesのデータでgoogle_compute_firewallallowブロックを繰り返し実行する方法は以下のようになります。

locals {
  firewall_allow_rules = [
    {
      protocol = "icmp",
      ports = []
    },
    {
      protocol = "tcp",
      ports = ["22"]
    },
    {
      protocol = "udp",
      ports = ["80", "82"]
    }
  ]
}

resource google_compute_firewall main {
  name    = "sample"
  network = google_compute_network.main.id
  
  dynamic allow {
    for_each = local.firewall_allow_rules
    iterator = _conf
    
    content {
      protocol = _conf.value.protocol
      ports    = _conf.value.ports
    }
  }
}

resource google_compute_network main {
  name                    = "sample"
  auto_create_subnetworks = false
}

allowブロックを繰り返し実行するため、dynamic allow {}と定義します。

for_eachdynamicの下に定義されています。

for_eachの下にiteratorが定義されています。このiteratorは、for_eachに入力されるデータへ参照するためのプレフィックスとなります。ここでは、iterator = _confとしているため、値の参照では、_conf.value.protocolとなっています。iteratorを定義しない場合、値への参照はブロック名で参照します。ここの例であれば、allow.value.protocolのように参照します。

contentブロックが、itratorの下に定義されています。ここには元のallowブロックに定義されるprotocolportsが定義されています。for_eachに入力されたデータへの参照は、先のiteratorの値を使い_conf.value.protocolのようになっています。

上記のコードの処理内容は、以下のようになります。

$ terraform plan

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:

  # google_compute_firewall.main will be created
  + resource "google_compute_firewall" "main" {
      + creation_timestamp = (known after apply)
      + destination_ranges = (known after apply)
      + direction          = (known after apply)
      + enable_logging     = (known after apply)
      + id                 = (known after apply)
      + name               = "sample"
      + network            = (known after apply)
      + priority           = 1000
      + project            = (known after apply)
      + self_link          = (known after apply)
      + source_ranges      = (known after apply)

      + allow {
          + ports    = [
              + "22",
            ]
          + protocol = "tcp"
        }
      + allow {
          + ports    = [
              + "80",
              + "82",
            ]
          + protocol = "udp"
        }
      + allow {
          + ports    = []
          + protocol = "icmp"
        }
    }

  # google_compute_network.main will be created
  + resource "google_compute_network" "main" {
      + auto_create_subnetworks         = false
      + delete_default_routes_on_create = false
      + gateway_ipv4                    = (known after apply)
      + id                              = (known after apply)
      + mtu                             = (known after apply)
      + name                            = "sample"
      + project                         = (known after apply)
      + routing_mode                    = (known after apply)
      + self_link                       = (known after apply)
    }

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

------------------------------------------------------------------------

上記のように記述することで、1つのブロック定義で複数のブロックを生成することができます。

データのループ処理

Terraformの型として、配列とmapの2つのCollection型があります。Terraformの処理の中で、管理しやすいように定義したデータからTerraformに沿った型に変換する必要があるときがあります。
例えば、先のfor_eachのブロック全体を繰り返し処理させたいとき、入力データの型をmap型にする必要があります。しかし、管理上オブジェクト型の配列で管理した方が管理しやすくなることもあります。例えば、subnetworksのmapを以下のようにオブジェクト型配列で定義したとします。

locals {
  subnetworks = [
    {
      name   = "tokyo-network",
      cidr   = "192.168.10.0/24",
      region = "asia-northeast1"
    },
    {
      name   = "osaka-network",
      cidr   = "192.168.20.0/24",
      region = "asia-northeast2"
    }
  ]
}

上記のようにすると、先程のmapのキーをnameとして明示的に定義することでkeyの意味が分かりやすくなります。しかし、このsubnetworksは配列型のため、for_eachへ代入することができません。そこで、このsubnetworksをmap型に変換する必要があります。こういったときに使う機能として、Terraformはfor文を提供しています。

for文は for <変数> in <Collection型のデータ> : <変数を使った処理> として定義します。この式を定義できる場所は、配列([])またはmap({})内となります。for文は定義された場所のデータ型を返します。つまり、配列内で定義したなら配列のデータが生成され、map内で定義したならmapのデータが生成されます。
配列型を出力させるときは、必ずキー値を定義する必要があります。キーと値の区切りには=>の記号を使います。また、このキー値は重複しないようにする必要があります。

先の例で、subnetworksの配列を配列内のnameの値をkeyとするmap型を返すのは下記のようにおこないます。

locals {
  subnetworks = [
    {
      name   = "tokyo-network",
      cidr   = "192.168.10.0/24",
      region = "asia-northeast1"
    },
    {
      name   = "osaka-network",
      cidr   = "192.168.20.0/24",
      region = "asia-northeast2"
    }
  ]
  
  subnetworks_map = { for v in local.subnetworks : v.name => v }
}

この例ではmapの内部({})でfor文を定義しているので、map型のデータが出力され、subnetworks_mapの変数に代入されます。subnetworksの配列変数からオブジェクトデータ({}のかたまり)を1つずつ取り出し、その値を変数vに入力します。そして:の後ろの部分 v.name => vで、オブジェクトのnameの値をキーとし、データをオブジェクトデータとするmapを作るよう処理しています。
subnetworks_mapのデータは、最終的に以下のようなデータ構造となります。

subnetworks_map = {
  "oska-network" = {
    "cidr" = "192.168.20.0/24"
    "name" = "oska-network"
    "region" = "asia-northeast2"
  }
  "tokyo-network" = {
    "cidr" = "192.168.10.0/24"
    "name" = "tokyo-network"
    "region" = "asia-northeast1"
  }
}

また、配列を出力する例として、以下のようなhoge,fuga,piyoという文字列を格納した配列の各文字列の文字をすべて大文字に変換した配列を生成する方法を紹介します。ここでは、大文字に変換する関数upper()を用います。

locals {
  little_strings = [
    "hoge", "fuga", "piyo"
  ]
  
  large_strings = [ for v in local.little_strings : upper(v) ]
}

配列はキー値は不要であるため、:の後ろはデータの処理のみとなっています。
変換したデータの出力結果は以下のようになります。

large_strings = [
  "HOGE",
  "FUGA",
  "PIYO",
]

さいごに

Terraformの繰り返し処理機能であるfor_eachforの使い方について紹介しました。この機能を使うことで、重複する処理を1つにまとめることができるため非常に便利です。多用するとコードが複雑になるため、考えて利用する必要がありますが、うまく設計すると非常に強力な機能なのでうまく取り入れましょう。