いまさらTerraform 0.12のdynamicブロックを使ってみた

# これは何ですか

- GCPのCloud IAM設定をTerraformで書くときに `for_each` と `dynamic` ブロックでいい感じに書けないかと試行錯誤したので、記録を残しておきます。
- Cloud IAM Conditions がベータ版だったので一部条件はまだ使えなさそうですが、そのうち使えるようになると思って残しておきます。

# サンプルコード

https://github.com/Hikosaburou/terraform_gcp_project_iam_policy

# ざっくり解説

サンプルコードの解説を簡単に書く。

## tfファイルの概要

サービスアカウントに付与する役割と条件は、以下のように入れ子構造のmap型変数で表現する。

```variables.tf
# Cloud IAM Settings
variable "role_sa_ssh" {
  description = "Roles for sa-ssh account"

  default = {
    instanceadmin = {
      role = "roles/compute.instanceAdmin.v1"
    },
    compute_ssh = {
      role = "roles/compute.osAdminLogin"
    },
    iap_tunnelresourceaccessor = {
      role = "roles/iap.tunnelResourceAccessor"
      conditions = [
        {
          title       = "restrict_tcp_tunneling"
          description = "Allow access for destination port 22 in the request"
          expression  = "destination.port == 22"
        }
      ]
    },
    sauser = {
      role = "roles/iam.serviceAccountUser"
    }
  }
}
```

1階層目はlistでも良い。今回は追加削除する際のplanの視認性を考えてmapにした。
- `role` には役割のフルネームを指定する。
- `conditions` には、`google_project_iam_member` リソースの `condition` ブロックの要素を書く。複数設定することを想定してlistにする。`description` のようなOptionalな値を設定しない場合は `null` とする。
 
IAMメンバー(ここではサービスアカウント `sa_ssh`) と役割の紐付けには `google_project_iam_member` リソースを使う。 `google_project_iam_member` リソースは次のように書く。上で定義した `role_sa_ssh` 変数を使って、Cloud IAMメンバーと役割の紐付けを行う。

```gcp_serviceaccount.tf
# サービスアカウント設定は役割との紐付けまで管理する
# 鍵は別管理とする
resource "google_service_account" "sa_ssh" {
  account_id   = "sa-ssh"
  display_name = "ssh"
}

resource "google_project_iam_member" "sa_ssh" {
  provider = google-beta
  for_each = var.role_sa_ssh

  project = var.gcp_project_id
  role    = each.value.role
  member  = "serviceAccount:${google_service_account.sa_ssh.email}"

  dynamic condition {
    for_each = contains(keys(each.value), "conditions") ? each.value["conditions"] : []
    content {
      title       = condition.value.title
      description = condition.value.description
      expression  = condition.value.expression
    }
  }
}
```
`google_project_iam_member` リソースのドキュメントは以下。
https://www.terraform.io/docs/providers/google/r/google_project_iam.html

以降、簡単に `google_project_iam_member` リソースの解説をする。

## `for_each` を使って複数のリソースを生成する

`google_project_iam_member` リソース内では `for_each` を使う。

```
resource "google_project_iam_member" "sa_ssh" {
  provider = google-beta
  for_each = var.role_sa_ssh
```

Terraform 0.12.6 以降では、リソース設定の中で `for_each` に map/list 型の変数を指定することで、変数の各要素に対応した複数のリソースを生成できる。
https://www.terraform.io/docs/configuration/resources.html#for_each-multiple-resource-instances-defined-by-a-map-or-set-of-strings

for_eachで指定した変数の各要素は、リソース内で `each.key`(map型のみ), `each_value` として参照できる。たとえば、役割の指定では次のように変数内の要素を指定している。

```
  role    = each.value.role
```

`role_sa_ssh` 変数の各要素は `role` キーを持つ。`each_value.role` のようにキーを指定することで、`role` キーに対応する値が取り出せる。

なお、繰り返しリソースを生成する機能としてはもともと `count` が存在した。ただ、リソース管理の観点からいえば `for_each` の方がメリットが多いので、0.12.6以降のバージョンで新たにTerraformを書くときは `for_each` を使う方が良い。
https://dev.classmethod.jp/etc/terraform_count_delete/


## `dynamic` ブロックで resource 内のブロックを動的に定義する

Cloud IAM Conditisons は、`google_project_iam_member` リソースの `condition` ブロックで設定する(ベータ版機能)。条件を設定しない場合もあるので、`condition` ブロックは0個以上の複数個が記述可能である。

map/list型の変数に応じて動的にブロックを記述する方法として、0.12 では `dynamic` ブロックが導入された。
https://www.terraform.io/docs/configuration/expressions.html#dynamic-blocks

`google_project_iam_member` リソース内では、次のように `condition` ブロックを記述している。

```
  dynamic condition {
    for_each = contains(keys(each.value), "conditions") ? each.value["conditions"] : []
    content {
      title       = condition.value.title
      description = condition.value.description
      expression  = condition.value.expression
    }
  }
```

ここでも `for_each` にmap/list型の変数を渡す。変数の要素を参照するときはブロック名を使う。ここでは `condition` ブロックを生成しているので、`condition.value.title` のように値が参照できる。

### 要素を生成しないときは空のリスト `[]` を渡す

`dynamic` ブロックでは、`for_each` に空のリスト `[]` を渡すことで、当該ブロックが0個の状態を表現できる。`google_project_iam_member` リソースでは、以下のように3項演算子を使って空のリストを渡している。

```
    for_each = contains(keys(each.value), "conditions") ? each.value["conditions"] : []
```

`contains()` はlist型の値に特定の要素が含まれていれば `true` を、そうでなければ `false` を返す。`keys()` はmap型の値のキーをlistとして返す。これを組み合わせることで、`condition` キーの有無を判定し、キーがある場合は対応する値を、無い場合は空のリスト `[]` を返している。

https://www.terraform.io/docs/configuration/functions/contains.html
https://www.terraform.io/docs/configuration/functions/keys.html

もっと素直な書き方があるような気がする……


# まとめ

- `for_each`と`dynamic`ブロックを活用することで、繰り返し構文によるリソース生成が簡単に書けるようになった。
- 0.12 でmap/list型の入れ子構造がサポートされたことで、`for_each` や `dynamic` ブロックが使いやすくなった。
    - 0.11 以前の interporation syntax ではかなり厳しかった。。。
- 一方でぱっと見の可読性は落ちるので注意して使う必要がある。
    - もしかしたら今回の書き方も複雑に見えるかもしれない。。。