Terraform の `The "for_each" value depends on resource attributes that cannot be determined until apply` に対する対応

この記事は **[リンクバルアドベントカレンダー2023](https://qiita.com/advent-calendar/2023/linkbal)** の6日目の記事です。

昨日は @Togo_Yokoyama さんの記事です！

https://qiita.com/Togo_Yokoyama/items/4fff312af1d0baf61c42


## はじめに
Terraformで `for_each` を使ってループさせるとき、以下のエラーが起こることがあります。

`The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on.`

## これはなんのエラーか
`for_each` で指定する値に、作成前のリソースの情報があると起こるエラーです。

詳しくは以下の公式ドキュメントや参考記事を御覧ください。

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

https://kazuhira-r.hatenablog.com/entry/2020/07/04/161848


## エラーが起こる例
例えば以下のような場合です。
AWS前提で書きます。

- moduleで、`aws_iam_role_policy_attachment` をする
- `aws_iam_role_policy_attachment` するポリシーにカスタムポリシーが含まれる

ec2にアタッチするIAMロールにポリシーをアタッチする場合で考えてみましょう。

※勢いで書いて動作確認していないので、他でエラーが出るかもです🙏

### module
```hcl:path/to/main.tf
locals {
  custom_policy_arns = [
    var.sample_policy_arn,
    xxx,
    ...,
  ]
  iam_policy_arns = concat(
    var.role_policies,
    custom_policy_arns
  )
}

# 略

resource "aws_iam_role_policy_attachment" "ec2" {
  for_each = toset(local.iam_policy_arns) # エラーになる

  role       = var.iam_role_name
  policy_arn = each.value
}

# 略
```

### module利用側
```hcl
module "ec2_web" {
  source  = "path/to/sample"

  role_policies = [
    "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
    "arn:aws:iam::aws:policy/AmazonSSMPatchAssociation",
  ]

  iam_role_name     = aws_iam_role.ec2_web.name
  sample_policy_arn = aws_iam_policy.sample.arn

  # 略
}

resource "aws_iam_role" "ec2_web" {
  name               = "aaaaa"
  assume_role_policy = # 略
}

resource "aws_iam_policy" "sample" {
  # 略
}

# 略
```


## 解決策
何点かあります。

1. エラーが起こるリソースだけ `-target` オプションか手動で先に作る
1. 諦めて一つ一つリソースを作る
    - さきほどの例だと、カスタムポリシーのリソース数だけ `aws_iam_role_policy_attachment` を作ります
1. **`length` と `count` を使う**　※注意点あり

3つ目の例を紹介します。

`aws_iam_role_policy_attachment` の部分だけ書きますね。

```hcl
# before
resource "aws_iam_role_policy_attachment" "ec2" {
  for_each = toset(local.iam_policy_arns) # エラーになる

  role       = var.iam_role_name
  policy_arn = each.value
}

# after
resource "aws_iam_role_policy_attachment" "ec2" {
  count = length(local.iam_policy_arns)

  role       = var.iam_role_name
  policy_arn = var.role_policy_arns[count.index]
}
```

これだとエラーにならずに無事リソース作成ができます！
なぜかは知らん。


## `length` と `count` 使用時の注意点
**末尾以外の `aws_iam_role_policy_attachment` をなくすとリソースの再作成が起こります。**

`count` は `list` なので、今回の例だと以下のようなstate名になります。
（ここも動作確認していないので、ちょっと違うかも）

```hcl
module.ec2_web.aws_iam_role_policy_attachment.ec2[0]
module.ec2_web.aws_iam_role_policy_attachment.ec2[1]
module.ec2_web.aws_iam_role_policy_attachment.ec2[2]
# ポリシーの数だけ続く
```

要素数が3つだけだとして、インデックスが0のリソースを消したら、次のようにリソースは再作成されてしまいます。

- `module.ec2_web.aws_iam_role_policy_attachment.ec2[1]` → `module.ec2_web.aws_iam_role_policy_attachment.ec2[0]`
- `module.ec2_web.aws_iam_role_policy_attachment.ec2[2]` → `module.ec2_web.aws_iam_role_policy_attachment.ec2[1]`

`count` と `list` の仕様上これは避けられません。
ただし、リソースが削除後に `state mv` をすれば、変更だけで済む？（未検証）
リソース次第な気ががしますね。。。

```sh
$ terraform state mv module.ec2_web.aws_iam_role_policy_attachment.ec2[1] module.ec2_web.aws_iam_role_policy_attachment.ec2[0]
$ terraform state mv module.ec2_web.aws_iam_role_policy_attachment.ec2[2] module.ec2_web.aws_iam_role_policy_attachment.ec2[1]
```

基本的に `for_each` を使ったほうがいいです。
詳細は後述の参考記事をご覧ください。


## 参考
### `length` と `count` の方法
https://discuss.hashicorp.com/t/the-for-each-value-depends-on-resource-attributes-that-cannot-be-determined-until-apply/25016

https://stackoverflow.com/questions/63768921/the-for-each-value-depends-on-resource-attributes-that-cannot-be-determined-t

### `count` より `for_each` を使ったほうがよい理由
https://zenn.dev/kyo2bay/articles/a6f98473141f36

https://tellme.tokyo/post/2022/06/12/terraform-count-for-each/

なお、上記記事では「count を使用したリソース作成有無 (0/1) の判定は昔から行われていた手法であるが、その場合においては count を使うほうが好ましい。」とありますが、 `for_each` でも同じようなことはできます！

https://qiita.com/ldr/items/04a5d046dc1ab6222dcf


## おわりに
おわり