リモートのtfstateを書き換えずに安全にterraform state mv後のplan差分を確認する手順

# はじめに

Terraformを長く運用しているとリソース名をリネームしたいというようなリファクタリングが稀によくあります。
これは `terraform state mv` コマンドを使うとできるのですが、このコマンドはtfstate(Terraformの状態管理ファイル)をデフォルトではその場で書き換えてしまいます。
https://www.terraform.io/docs/commands/state/mv.html

しかしながらチーム開発をしている場合、 `*.tf` はgitでバージョン管理し、 tfstateはリモートのbackend(AWS S3とか)に保存するという管理方法が一般的です。つまり、リモートのtfstateをその場で書き換えてしまうと、masterブランチの状態と差分が出てしまいます。

tfファイル変更のレビュー前に、リモートのtfstateを書き換えずに、terraform state mv後のplan差分がなくなることを事前に確認したいところです。軽くググった感じ、terraform state mvに言及している記事は単純な例ばかりで、リモートのtfstateとの組み合わせで、安全な正しい手順が簡単に見つけられなかったので、確立した手順を共有しておきます。

# 環境

本稿執筆時点の最新版のTerraformとAWSプロバイダで稼働確認しました。

- terraform v0.12.29
- terraform-provider-aws v3.0.0

手順の流れ自体はAWS以外のクラウドプロバイダでも基本的に同じなので、適宜読み替えて下さい。

# 事前状態
## 初期状態

説明のため以下のようなtfファイルを用意しました。

```main.tf
terraform {
  required_version = "0.12.29"

  backend "s3" {
    region = "ap-northeast-1"
    bucket = "yourbacket"
    key    = "test/terraform.tfstate"
  }
}

provider "aws" {
  version = "3.0.0"
  region  = "ap-northeast-1"
}

resource "aws_security_group" "foo" {}
resource "aws_security_group" "bar" {}
```

あらかじめ `terraform init` && `terraform apply` して以下のような状態になっているものとします。

```
$ terraform state list
aws_security_group.bar
aws_security_group.foo
```

## リソース名のリネーム

ここでは説明のため、tfファイルで `foo` を `foo1` にリネームしたとします。

```
$ git diff
diff --git a/main.tf b/main.tf
index 0819129..e504c28 100644
--- a/main.tf
+++ b/main.tf
@@ -13,5 +13,5 @@ provider "aws" {
   region  = "ap-northeast-1"
 }

-resource "aws_security_group" "foo" {}
+resource "aws_security_group" "foo1" {}
 resource "aws_security_group" "bar" {}
```

当然plan結果は差分になります。

```
$ terraform 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.

aws_security_group.bar: Refreshing state... [id=sg-0339c0b83295dd56a]
aws_security_group.foo: Refreshing state... [id=sg-0465466ea3f389dde]

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

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:

  # aws_security_group.foo will be destroyed
  - resource "aws_security_group" "foo" {
      (snip.)
    }

  # aws_security_group.foo1 will be created
  + resource "aws_security_group" "foo1" {
      (snip.)
    }

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

```

# state mv手順

で、ここからが本題です。

## state pull

まず最新のtfstateをremoteからpullしてきて、一旦ローカルに保存します。

```
$ terraform state pull > tmp.tfstate
```

## backendをlocalに切り替え

次に、一時的にbackendをlocalに切り替えます。
tfファイル内のbackendを直接エディタで編集してもよいのですが、若干煩雑なので、 overrideファイルをその場で生成してbackendの定義を上書きします。 `override.tf` が他の用途で既に存在する場合は、 `_hoge_override.tf` などぶつからない名前にして下さい。
https://www.terraform.io/docs/configuration/override.html

> For these rare situations, Terraform has special handling of any configuration file whose name ends in _override.tf or _override.tf.json. This special handling also applies to a file named literally override.tf or override.tf.json.

```
$ cat << EOF > override.tf
terraform {
  backend "local" {
  }
}
EOF
```

backendを定義を変更した場合は、initしなおす必要があります。

```
$ terraform init -reconfigure

Initializing the backend...

Successfully configured the backend "local"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```

> Successfully configured the backend "local"! Terraform will automatically

localに切り替わってることが確認できます。

## state mv

で、必要な state mv します。
このとき、 `-state` フラグでローカルのtfstateファイルを指定するのを忘れないで下さい。

```
$ terraform state mv -state=tmp.tfstate aws_security_group.foo aws_security_group.foo1
Move "aws_security_group.foo" to "aws_security_group.foo1"
Successfully moved 1 object(s).
```

## plan

state mvできたらplanで差分が出ないことを確認します。
planにも `-state` フラグでローカルのtfstateファイルを渡します。

```
$ terraform plan -state=tmp.tfstate -detailed-exitcode
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.

aws_security_group.bar: Refreshing state... [id=sg-0339c0b83295dd56a]
aws_security_group.foo1: Refreshing state... [id=sg-0465466ea3f389dde]

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

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.

$ echo $?
0
```

planに `-state` フラグがあるので、一見するとバックエンドがリモートのままでも、ローカルのファイルが指定できそうに見えますが、このオプションはバックエンドがリモートの場合は無視される罠があるので注意。

https://www.terraform.io/docs/commands/plan.html

> -state=path - Path to the state file. Defaults to "terraform.tfstate". Ignored when remote state is used.

## レビュー

tfファイルの変更を適当なブランチを切ってコミットし、Pull Requestをチーム内にレビュー依頼します。
このとき、参考に検証に使ったstate mvコマンドをPull Requestに貼っておくとよいでしょう。

レビュー待ちのあいだにうっかりローカルのまま他の作業をしてしまうと事故るので、backendをremoteに戻しておきます。

```
$ rm override.tf
```

```
$ terraform init -reconfigure

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```

> Successfully configured the backend "s3"! Terraform will automatically

リモートに切り替わってることが確認できます。

レビューが完了したら、masterブランチにマージして、git pullしてきます。

## remoteへの反映

レビュー待ちのあいだに誰かが変更しているかもしれないので、再度masterブランチで同じ手順でbackendをlocalに切り替えてstate mvしてplanの差分がないことまで確認します。

```
$ terraform state pull > tmp.tfstate
$ cat << EOF > override.tf
terraform {
  backend "local" {
  }
}
EOF
$ terraform init -reconfigure
$ terraform state mv -state=tmp.tfstate aws_security_group.foo aws_security_group.foo1
$ terraform plan -state=tmp.tfstate -detailed-exitcode
$ echo $?
```

問題なければ、backendをremoteに戻します。

```
$ rm override.tf
$ terraform init -reconfigure
```

stateをremoteにpushします。

```
$ terraform state push tmp.tfstate
```

※CI/CDで自動applyしている場合は、state pushとタイミングがぶつからないように注意して下さい。自動applyのapproveのタイミングをコントロールできないと、masterにマージ前にstateを書き換えないと自動applyで死んでしまいそうですが。

remoteのstateに反映されていることを確認します。

```
$ terraform state list
aws_security_group.bar
aws_security_group.foo1
```

念のためplan差分がないことを確認します。

```
$ terraform plan -detailed-exitcode

(snip.)

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.

$ echo $?
0
```

一時的なローカルのtfstateのゴミを消しておきます。

```
$ rm tmp.tfstate*
```

おしまい。

# 追記

という手順がくっそ煩雑でめんどくさかったので、これらをまるっと自動化するtfmigrateというツールを書きました↓
[Terraformのstate操作をgitにコミットしたくてtfmigrateというツールを書いた
](https://qiita.com/minamijoyo/items/670eb100552b0834dd5e)