terraformのcountリソースを、再作成なしでfor_eachリソースに変更する

# はじめに

terraformの0.12.6から、[resourceをfor_eachで作成できるようになった](https://qiita.com/keiichi-hikita/items/9fd20e8ad6afe4e5ef72)、というものがある。
そこで、自分のリソースで、現在countで管理しているデータもfor_eachで管理してみたくなったわけだが、
手間取ったので、備忘録として書いておく

# 注意

**この操作はtfstateを直接書き換えるものであり、場合によってはterraform管理が壊れてしまう可能性があるため、必ずtfstateのバックアップと、事前検証を行っておくこと**

# 使用するリソース

今回は以下のようなリソースを用意して検証する。

```
locals {
    resourcemap = [
        {
            name = "a"
        },
        {
            name = "b"
        },
        {
            name = "c"
        }
    ]
}

resource "null_resource" "test" {
    count = length(local.resourcemap)
}
```

上記リソースを、

```
locals {
    resourcemap = [
        {
            name = "a"
        },
        {
            name = "b"
        },
        {
            name = "c"
        }
    ]
}

resource "null_resource" "test" {
    for_each = { for x in local.resourcemap: x.name => x }
}
```

のように、nameをキーにしてマッピングするという想定をする。

# 普通に移行すると

普通に書き換えた場合、以下のようなplanが出力される。

```
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

null_resource.test[1]: Refreshing state... [id=153962400052254689]
null_resource.test[2]: Refreshing state... [id=962039978484673663]
null_resource.test: Refreshing state... [id=1112149254847114420]

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

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
  - destroy

Terraform will perform the following actions:

  # null_resource.test will be destroyed
  - resource "null_resource" "test" {
      - id = "1112149254847114420" -> null
    }

  # null_resource.test[1] will be destroyed
  - resource "null_resource" "test" {
      - id = "153962400052254689" -> null
    }

  # null_resource.test[2] will be destroyed
  - resource "null_resource" "test" {
      - id = "962039978484673663" -> null
    }

  # null_resource.test["a"] will be created
  + resource "null_resource" "test" {
      + id = (known after apply)
    }

  # null_resource.test["b"] will be created
  + resource "null_resource" "test" {
      + id = (known after apply)
    }

  # null_resource.test["c"] will be created
  + resource "null_resource" "test" {
      + id = (known after apply)
    }

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

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

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
```

これを行うと、既存のリソースの再作成が実行される。
今回はnull_resourceを使用しているので問題ないが、実際は仮想マシンインスタンス等が対象となるため、実際のサービスへの大きな影響や、適用中の失敗する可能性が懸念され、かつ実行に時間がかかることも予想される。

# リソースの再作成を抑止するには

さて、ではどうすればリソースの再作成を抑止できるだろうかということで、試行錯誤して見つけた方法を以下に書く。
簡単に言うと、

1. 既存のtfstateを取得
2. tfstateを編集
3. 編集したtfstateを適用

というものとなる。
前提として、無用なトラブルを避けるため、**countからfor_eachへ切り替える以外の変更は全て排除しておくこと**。

## 既存のtfstateの取得

既存のtfstateを取得するには、`terraform state pull`を使用する。
オプションは無く、常に標準出力に表示されるため、リダイレクトしてファイルに保存する。
なお、形式はjson形式となる。

ただし、**エンコーディングはBOM無しUTF-8**でなければならないため、windowsでpowershell等使用している場合は要注意

## tfstateを編集

取得したtfstateに対して、以下のような修正を加える。

* `/serial`の数字を1増やす
* `/resources`以下の、`each`の値が`map`になっているオブジェクトを探す
* `each`の値を`map`に変更する
* `instances`以下の`index_key`を、想定するマッピングのキーに変更する

例として、

```txt
    ...
    "serial": 1
    ...
    {
      ...
      "each": "list",
      "instances": [
        {
          ...
          "index_key": 0,
          ...
        },
        ...
      ]
    }
```
を

```txt
    ...
    "serial": 2
    ...
    {
      ...
      "each": "map",
      "instances": [
        {
          ...
          "index_key": "a",
          ...
        },
        ...
      ]
    }
```
のように編集する。

フォーマットは基本的にjsonなので、対象リソースが多くて大変な場合はスクリプト等で回した方が良いかもしれない。

## tfstateを適用する

適用は`terraform state push [編集されたtfstateファイル]`で行う。
この時、json的に文法がおかしかったり、serialを上げていない等の明確な間違いがあった場合は弾かれる。
**この時、実際にtfstateが変更されるため、必ずその前に元のtfstateのバックアップを行っておくこと**。

## 結果の確認

狙い通りになった場合、`terraform plan`しても、create/destroyが0になるはず。今回用意したリソースでは、以下のようになる。

```
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

null_resource.test["b"]: Refreshing state... [id=153962400052254689]
null_resource.test["a"]: Refreshing state... [id=1112149254847114420]
null_resource.test["c"]: Refreshing state... [id=962039978484673663]

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

No changes. Infrastructure is up-to-date.

This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.
```

# 終りに

繰り返しになるが、**tfstateを直接編集しているので、必ず事前にバックアップを取っておくこと**を推奨する。
今回はtfstate直接編集という手段を用いたが、その他に安全な方法があるならばぜひ教えてほしい。

## 参考リンク

* [terraformのfor_each構文について](https://www.terraform.io/docs/configuration/resources.html#for_each-multiple-resource-instances-defined-by-a-map-or-set-of-strings)