Terraform0.12  のfor_eachで幸せになれるってよ

# はじめに

Terraform 0.12.6が出ました！
このリリースにより、[for_each](https://www.terraform.io/docs/configuration/resources.html#for_each-multiple-resource-instances-defined-by-a-map-or-set-of-strings)の幅が広がりました。

具体的にどういった恩恵が授かれるのかをIAM User&Groupの作成で紹介できればと思います。

# 環境

以下の環境で実施します。

* Cloud: AWS
* Terraform: 0.12.6

# ツラミ

Terraform を使ってIAM UserやGroupを管理していてツラミにぶち当たった人は少なからずいると思います。
それは、ユーザを追加や削除する時です。
そして見覚えがあると思います。。以下のエラー

> User with name X already exists.

もうツラミでしかない。。

ここからは、いままでのやり方（僕の）やこれからのやり方について書いていきたいと思います。

# いままで

まずはこのエラーが発生する場面を体験したいと思います。

## Terraformコードの作成

Terraformのファイル構成は以下です。
リソース単位でファイルを分けておりますが、まとめても問題ありません。

```bash
.
├── backend.tf
├── iam_group.tf
├── iam_group_policy.tf
├── iam_membership.tf
├── iam_user.tf
├── policy
│   └── example_policy.json
├── provider.tf
├── variable.tf
└── versions.tf
```

前提としてtfstateファイルを格納するS3バケットはあるもとのとします。

```backend.tf
terraform {
  backend "s3" {
    bucket = "example-bucket"
    key    = "state/example"
    region = "ap-southeast-2"
  }
}
```

リージョンは、お好きなところを選んでください。

```provider.tf
provider "aws" {
  region = "ap-southeast-2"
}
```

`Versions.tf`はなくても問題ありませんが、運用を考えるとあった方がいいので入れておきます。

```versions.tf
terraform {
  required_version = ">= 0.12"
}
```

IAM User、Groupとグループポリシーのリソースを書きます。


```iam_user.tf
resource "aws_iam_user" "example" {
  count = length(local.members.example)
  name  = local.members.example[count.index]
}
```

```iam_group.tf
resource "aws_iam_group" "example" {
  name = "admin-${local.project_name}"
}
```

```iam_group_policy.tf
resource "aws_iam_group_policy" "example" {
  name   = "admin-policy"
  group  = aws_iam_group.example.id
  policy = file("policy/example_policy.json")
}
```

ポリシーは、jsonファイルで定義し、[file function](https://www.terraform.io/docs/configuration/functions/file.html)で取得します。
ちなみに、このポリシーはとりあえず管理者権限を付与しているだけです。

```policy/example_policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        }
    ]
}
```

IAM UserとGroupの紐付けをします。

```iam_membership.tf
resource "aws_iam_user_group_membership" "example" {
  count      = length(local.members.example)
  user       = local.members.example[count.index]
  groups     = [aws_iam_group.example.name]
  depends_on = [aws_iam_user.example]
}
```

最後に`varible.tf`です。
3ユーザが作成されるようにリストに記述しています。

```variable.tf
locals {
  project_name = "example"

  members = {
    example = [
      "user01",
      "user02",
      "user03"
    ]
  }
}
```

## ここからが問題

`terraform apply`を実行し、ユーザを作成します。

```bash
$ terraform apply
~省略~
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
```

ここまでは問題ありませんね。
次に3ユーザのうちの1ユーザを削除します。
例えば、`user02`を削除すると以下な感じですね。

```variable.tf
locals {
  project_name = "example"

  members = {
    example = [
      "user01",
      "user03"
    ]
  }
}
```

それでは、`apply`します。
変更箇所が多い！これは。。

```terragrunt.hcl
n execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place
  - destroy

Terraform will perform the following actions:

  # aws_iam_group_membership.example[0] will be updated in-place
  ~ resource "aws_iam_group_membership" "example" {
        group = "admin-example"
        id    = "test"
        name  = "test"
      ~ users = [
            "user01",
          - "user02",
          - "user03",
        ]
    }

  # aws_iam_group_membership.example[1] will be updated in-place
  ~ resource "aws_iam_group_membership" "example" {
        group = "admin-example"
        id    = "test"
        name  = "test"
      ~ users = [
          - "user01",
          - "user02",
            "user03",
        ]
    }

  # aws_iam_group_membership.example[2] will be destroyed
  - resource "aws_iam_group_membership" "example" {
      - group = "admin-example" -> null
      - id    = "test" -> null
      - name  = "test" -> null
      - users = [
          - "user01",
          - "user02",
          - "user03",
        ] -> null
    }

  # aws_iam_user.example[1] will be updated in-place
  ~ resource "aws_iam_user" "example" {
        arn           = "arn:aws:iam::446105290347:user/user02"
        force_destroy = false
        id            = "user02"
      ~ name          = "user02" -> "user03"
        path          = "/"
        tags          = {}
        unique_id     = "AIDAWPXPSQZVRQTUGGAO4"
    }

  # aws_iam_user.example[2] will be destroyed
  - resource "aws_iam_user" "example" {
      - arn           = "arn:aws:iam::446105290347:user/user03" -> null
      - force_destroy = false -> null
      - id            = "user03" -> null
      - name          = "user03" -> null
      - path          = "/" -> null
      - tags          = {} -> null
      - unique_id     = "AIDAWPXPSQZV6WEUDV65V" -> null
    }

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

はい！期待通りのエラーです。

```
Error: Error updating IAM User user02: EntityAlreadyExists: User with name user03 already exists.
	status code: 409, request id: xx

  on iam.tf line 1, in resource "aws_iam_user" "example":
   1: resource "aws_iam_user" "example" {
```

このエラーが出たら、今までは仕方ないものとして対処していました。
しかし、これが解決されます！

## これから

ここからはどうやって対応して先ほどのエラーを解消するかを記載します。

### コードの書き換え

今まで苦しめてきた問題を解決するために`for_each`を利用します。
書き換えるのは、以下の二つのファイルです。

* iam_membership.tf
* iam_user.tf

[toset function](https://www.terraform.io/docs/configuration/functions/toset.html)を使用してfor_eachに渡します。
ちなみに、`each.key`でも`each.value`どちらでも要素にアクセス可能です。

```iam_user.tf
resource "aws_iam_user" "example" {
  for_each = toset(local.members.example)
  name     = each.value
}
```

上記と同じように`membership`側も対応します。

```iam_membership.tf
resource "aws_iam_user_group_membership" "example" {
  for_each   = toset(local.members.example)
  user       = each.value
  groups     = [aws_iam_group.example.name]
  depends_on = [aws_iam_user.example]
}
```

### リベンジ実行するよ！

先ほどと同様にユーザを新規で作成します。
そして、`user02`を`variable.tf`から削除してから実行します。

```bash:result
$ terraform apply

  - destroy

Terraform will perform the following actions:

  # aws_iam_user.example["user02"] will be destroyed
  - resource "aws_iam_user" "example" {
      - arn           = "arn:aws:iam::446105290347:user/user02" -> null
      - force_destroy = false -> null
      - id            = "user02" -> null
      - name          = "user02" -> null
      - path          = "/" -> null
      - tags          = {} -> null
      - unique_id     = "xxx" -> null
    }

  # aws_iam_user_group_membership.example["user02"] will be destroyed
  - resource "aws_iam_user_group_membership" "example" {
      - groups = [
          - "admin-example",
        ] -> null
      - id     = "terraform-xxx" -> null
      - user   = "user02" -> null
    }

Plan: 0 to add, 0 to change, 2 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

aws_iam_user_group_membership.example["user02"]: Destroying... [id=terraform-xxx]
aws_iam_user_group_membership.example["user02"]: Destruction complete after 1s
aws_iam_user.example["user02"]: Destroying... [id=user02]
aws_iam_user.example["user02"]: Destruction complete after 1s

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

`user02`のみが削除されてエラーも発生しないことが確認できました！
今までは、以下のようにインデックスに数字が入っていましたが、リストで定義した値が入っていることがわかります。

```tf:apply実行時
### before
aws_iam_user.example[0]
### after
aws_iam_user.example["user02"]
```

# まとめ

`for_each`で幸せになることができましたね！
少しでも誰かのお役に立てれば幸いです。
ありがとうございました！

# 参考

[Resources](https://www.terraform.io/docs/configuration/resources.html#for_each-multiple-resource-instances-defined-by-a-map-or-set-of-strings)