tfstate の操作チートシート

こんにちは、エン・ジャパン株式会社のプラットフォームチームに所属している山﨑です。

弊社ではプラットフォームの構築にTerraform を利用しており、tfstate の操作を行なうことが多いのですが、操作を誤ると元の状態に戻すことが困難であったり、時間がかかってしまいます。

素早く安全に操作するため、どういった手法があるのか整理するとともに、ユースケースについて紹介します。

# tfstate の操作手法の比較

:::note warn
tfstate は重要なファイルなので直接編集するのはなるべくやめましょう。また、ワークロードでは可能な限り Block Syntax（※1）で書くようにし、結果をPlanしたり、レビューを受けたりして安全に操作することを推奨します。

※1）後述しますが、`moved` Block や`import` Block、`removed` Block など。
:::

tfstate を操作する方法は2つに大別できます。

Terraform CLI はコマンドラインで操作し、Block Syntax はTerraform コードに記載してapply で操作します。

以下で比較します。

## 操作性の比較

| 操作方法 | 簡単な操作（※2） | 安全な操作（※3） | 備考
| --- | --- | --- | --- |
| Terraform CLI | O | X | ローカルから実行できる
| Block Syntax | X | O | Planで確認できる

※2）操作するための手順が少ないほど簡単な操作としています。
※3）実行する前に結果が分かるか、レビュー可能か、などの観点から判断している。

## 利用可能な操作の比較

多くの操作がBlock Syntaxで実施できます。

| 操作手法 | import | move<br/>（同一のtfstate） | move<br/>（別のtfstate） | remove |
| --- | --- | --- | --- | --- |
| Terraform CLI | O | O | O | O |
| Block Syntax | O | O | X | O |

# 操作方法

:::note info
前提として、tfstate はクラウドに保存されている状態で始めます。tfstate の操作時にはバックアップを取っておきましょう。
:::

Terraform CLI とBlock Syntax の操作方法について紹介します。

## Terraform CLI

Terraform CLI を利用すれば、ローカルで全ての操作を行うことができます。

### terraform state

tfstateの操作はこのコマンドでほとんど実行可能。

#### ✅ terraform state list

どんなリソースを管理しているのか把握するのに便利。

```bash
# input
terraform state list

# output
module.vpc.aws_route_table.api[0]
module.vpc.aws_route_table.api[1]
module.vpc.aws_route_table.private[0]
module.vpc.aws_route_table.private[1]
module.vpc.aws_route_table.public
```

#### ✅ terraform state show

特定のリソースの状態を把握するときに利用。

```bash
# input
terraform state show module.vpc.aws_route.public

# output
resource "aws_route" "public" {
    destination_cidr_block = "0.0.0.0/0"
    gateway_id             = "igw-xxx"
    id                     = "r-rtb-xxx"
    origin                 = "CreateRoute"
    route_table_id         = "rtb-xxx"
    state                  = "active"
}
```

#### ✅ terraform state pull

現在の状態を取得。パイプを使えばファイル保存することが可能で、バックアップでよく使う。

```bash
# backup.tfstateに現在のtfstateをコピー
terraform state pull > backup.tfstate
```

#### ✅ terraform state push

:::note warn
このコマンドは既存のtfstateを上書きするため注意すること。
:::

指定したtfstateを既存のtfstateに上書きする。上書きする際にはtfstate のserial を変更しないといけない場合があるので注意。（その場合、直接tfstate を編集することになる）

```bash
# new.tfstateを新しいtfstateとする（上書きのため注意）
terraform state push new.tfstate
```

#### terraform state mv

:::note warn
Terraform v1.1.0 で`move` Block が導入されたので、まずはそちらの利用を検討しましょう。
:::

tfstateのリソースを操作する。オプションを付けなければ tfstate 内でリソース名を変更し、`-state-out` というオプションを付ければ別のファイルに移動（元のファイルからは削除）することが可能。

```bash
# リソース名を変更する
terraform state mv aws_instance.before aws_instance.after

# リソースを外に出力する
terraform state mv -state-out output.tfstate aws_instance.before aws_instance.after
```

#### terraform state rm

:::note warn
Terraform v1.7.0 で`removed` Block が導入されたので、まずはそちらの利用を検討しましょう。
:::

tfstate からリソースを削除する。実リソースを削除するわけではなく、あくまでtfstate 上から削除するだけ。

```bash
 # aws_instance.this をtfstate 上から削除する
 terraform state rm aws_instance.this
```

### terraform import

:::note warn
Terraform v1.5.0 で`import` Block が導入されたので、まずはそちらの利用を検討しましょう。
:::

import は`terraform state import` ではなく、`terraform import` コマンドで実行する。

実リソースの情報をtfstate に取り込む。実リソースのID 指定方法はリソースによって異なるため公式ドキュメントを参考にすること。

```bash
# S3 Bucket
terraform import aws_s3_bucket.bucket bucket-name

# Route 53 Record
terraform import aws_route53_record.myrecord "Z4KAPRWWNC7JR_dev.example.com_NS"
```

## Block Syntax

tfstate 操作の多くを実行できる。Planで確認したり、レビューしたりすることが可能なので、こちらの利用を推奨

### ✅ import Block

v1.5.0 から導入されたブロック。v1.7.0 から`for_each`が導入されたので、大量のリソースを一気に取り込むことが可能になった。

importコマンドと同様、公式ドキュメントを参考に適切な`id`を指定すること。

```hcl
import {
  to = aws_instance.this
  id = "i-12345678"
}
```

### ✅ moved Block

v1.1.0 から導入されたブロック。リファクタリングでお世話になる。大量の`moved` blockを作成しないといけない場合は生成AIを活用したりすると一気に作成できる。

```hcl
moved {
  from = aws_instance.before
  to   = aws_instance.after
}
```

### ✅ removed Block

v1.7.0 から導入されたブロック。

importやmoveの構文とは少し異なり、`lifecycle`で`destroy = false`を入れる必要がある。`lifecycle`を入れない場合や`destroy = true`の場合はリソース自体が削除されるので、そもそも`removed` Block を使う必要がなくなる。

```hcl
removed {
  from = aws_instance.this
  lifecycle {
    destroy = false
  }
}
```

# ユースケース

Block Syntax ではtfstate の操作を行うのが難しいユースケースについて紹介します。

## 1️⃣ 実行単位を分けたい

実行単位が大きくなってきて分割したい場合、tfstate も分割する必要があります。

複数のtfstate を利用するため、Block Syntax ではなくCLI でtfstate を操作します。

### 移行元のディレクトリにて

```bash
# まずは現状確認（差分0が望ましい）
terraform plan

# バックアップ（backup.tfstate）を取得
terraform state pull > backup.tfstate

# 移行したいリソースをに移動（mv.state）
terraform state mv -state-out=mv.tfstate module.xxx module.xxx

# 移行したいリソースがdestroyされるかどうかを確認
terraform plan
```

上記操作後、`mv.tfstate` を移行先のディレクトリに移動する。

### 移行先のディレクトリにて

```bash
# まずは現状確認（差分が出るはず）
terraform plan

# 先ほど作成したmv.tfstateを管理主体にする
terraform state push mv.tfstate

# 差分が出ないことを確認
terraform plan
```

上記操作後、`mv.tfstate`を削除する。

## 2️⃣ 既に存在する別のtfstateにリソースを追加したい

こちらのユースケースも複数のtfstate を利用するため、Block Syntax ではなくCLI でtfstate を操作します。

### 移行元のディレクトリにて

```bash
# out.tfstateにaws_instance.thisリソースを移行させる
# そのとき、元のtfstateからaws_instance.thisリソースが消える
terraform state mv -state-out=out.tfstate aws_instance.this aws_instance.this
```

出力した`out.tfstate` を移行先のディレクトリに移動させる。

### 移行先のディレクトリにて

```bash
# out.tfstate にあるaws_instance.this の情報を in.tfstate に移行する
terraform state mv -state=out.tfstate -state-out=in.tfstate aws_instance.this aws_instance.this

# in.tfstate をtfstate に上書きする
terraform state push in.tfstate
```

# まとめ

直接tfstate を編集したり、実行結果が分からないままtfstate の操作を行ってしまうと実際のリソースが消えてしまうわけではありませんが、IaC を維持していくことが困難になります。そのため必ずバックアップを取得した上で、tfstate を安全に操作するようにしましょう。