[Terraform] for_each で作ったインデックスつきリソースの一部を removed する

### TL;DR

:::note
Terraform の `templatefile()` と Terragunt の `generate()` を組み合わせることで下記のメリットを得られる
- `removed` 対象が多くても転記ミスに強い
- `removed` の結果を反映した plan 差分を確認できる
:::

# 想定する状況

- **やりたいこと**: `for_each` で作成済みのリソースの一部を一括で（伝われ） `terraform state rm` したい
- **課題**:
  - 数が多いので手で `terraform rm` したくない
  - remove 対象をレビューしたいのでコードにしたい
  - スクリプトにすると手元での実行になりがちなので [`removed` ブロック](https://developer.hashicorp.com/terraform/language/resources/syntax#removing-resources)を使いたい
  - 投稿時点において `removed` ブロックは[インデックス形式のリソース指定をサポート](https://github.com/hashicorp/terraform/issues/34439)していないので [`moved` ブロックを併用したワークアラウンド](https://github.com/hashicorp/terraform/issues/34439#issuecomment-2178128452)が必要
  - ~~面倒なので~~ 転記ミスのリスクがあるのでよい方法を探したい

例えば下記のようにリソースを定義したとする:
```hcl
locals {
  repeats = ["foo", "bar", "baz"]
}

resource "something" "this" {
  for_each = toset(local.repeats)
  name     = each.value
}
```
これを apply すると、下記のリソース管理することになる:
- `something.this["foo"]`
- `something.this["bar"]`
- `something.this["baz"]` 

これらのうち、`foo` と `bar` のリソースだけを `rm` しようとする場合、何度も `moved {}` と `removed {}` を書かないといけないので面倒という話。

# 解決策

Terraform の [`templatefile()`](https://developer.hashicorp.com/terraform/language/functions/templatefile) と [Terragunt](https://terragrunt.gruntwork.io/)[^terragrunt] の [`generate()`](https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#generate) を組み合わせることでうまくいく:
```hcl:terragrunt.hcl
locals {
  something_removed = [
    "foo",
    "bar",
  ]
}

generate "removed" {
  path      = "removed.tf"
  if_exists = "overwrite"
  contents = templatefile("templates/removed.tmpl", {
    remove_targets = local.something_removed
  })
}
```

```ruby:templates/removed.tmpl
%{ for target in remove_targets }
moved {
  from = something.this["${target}"]
  to = something.this_${target}
}

removed {
  from = something.this_${target}
  lifecycle {
    destroy = false
  }
}
%{ endfor }
```
`.tmpl` 中の変数（ `${}` で囲ったもの） は Terraform の変数ではなくテンプレートの変数なので注意。

# バリエーション: Terraform の組み込み関数をはさむ

- 課題: 
  - Terraform では[Terraform リソース名にはハイフンではなくアンダースコアを使う](https://arc.net/l/quote/hnipcaxe)という慣習があるが、実際のリモートリソース名にはハイフンを使いたいので変数はハイフンを利用した名前で持っておきたい
  - `moved` & `removed` ブロック中のリソース名はアンダースコアを徹底する必要がある

## 解決策

Terraform の [replace()](https://developer.hashicorp.com/terraform/language/functions/replace) を使って下記のように書く:
```hcl:terragrunt.hcl
locals {
  something_removed = [
    "something-1",
    "something-2",
  ]
  something_removed_sanitized = {
    for name in local.something_removed : name => replace(name, "-", "_")
  }
}

generate "removed" {
  path      = "removed.tf"
  if_exists = "overwrite"
  contents = templatefile("templates/removed.tmpl", {
    remove_target_names_map = local.something_removed_sanitized
  })
}
```

```ruby:templates/removed.tmpl
%{ for name_hyphen, name_underscore in remove_target_names_map }
moved {
  from = something.this["${name_hyphen}"]
  to = something.this_${name_underscore}
}

removed {
  from = something.this_${name_underscore}
  lifecycle {
    destroy = false
  }
}
%{ endfor }
```
大事なのでもう一度言うが、`.tmpl` 中の変数 `${}` は Terraform の変数ではなくテンプレート中の変数なので注意。

下記のようにしても、`local.target_sanitized` という**テンプレート変数**がない旨のエラーが出て動かない:
```ruby:bad-example.tmpl
# does not work
%{ for target in remove_targets }

locals {
  target_sanitized = replace($target, "-", "_")
  }  
}

moved {
  from = something.this["${target}"]
  to = something.this_${local.target_sanitized}
}

removed {
  from = something.this_${local.target_sanitized}
  lifecycle {
    destroy = false
  }
}
%{ endfor }
```


[^terragrunt]: しれっと別ツールを持ち出してしまったが、実務の IaC を助けてくれる便利機能（要件次第ではほぼ必須なこともある）がいろいろ揃っているのでおすすめ。本記事で扱った課題を解決するためだけに導入しようという意味ではない

# Terragrunt が便利だよという補足

テンプレートを使ったループ展開は Terraform の機能なので、本記事で扱った問題は Terragrunt なしでも解決できそうにも見える。

下記のように書いてみると、一応は期待通りの `removed.tf` が生成されそうな動きをするのだが、実際に apply するまで `removed.tf` の実体はないため、plan 時点では `removed` の結果を反映した plan 差分は確認不可能。
```hcl
locals {
  teams_removed = [
    "team-1",
  ]
  teams_removed_sanitized = {
    for name in local.teams_removed : name => replace(name, "-", "_")
  }
}

resource "local_file" "removed" {
  filename = "removed.tf"
  content = templatefile("templates/removed.tmpl", {
    team_names_map = {
      for name in local.teams_removed : name => replace(name, "-", "_")
    }
  })
}
```

一方、Terragrunt の場合には plan 時点で `removed.tf` が生成されるため `removed {}` の結果も反映した plan 差分を検討できる（`removed.tf` の ignore を忘れずに）。

# 参考にした情報

- https://github.com/hashicorp/terraform/issues/34439#issuecomment-2178128452
- https://github.com/gruntwork-io/terragrunt/issues/1443#issuecomment-735771468