Terraformのtfstateをざっと理解する

この記事は[OpenSaaS Studio Advent Calendar 2019](https://adventar.org/calendars/3928)の20日目の記事です。


# はじめに

- なんとなくplanとapplyが動かせるからいいかな〜ってノリでTerraformを使ってる人々（かつての自分）のためのメモです。
- tfstateが分からないと、[Terraformの構成を変えたい！](https://qiita.com/masaaania/items/09d85d938d778e1e0f09)と思ったときにリファクタリングすることもできない。
  - Terraformの構成を最初からうまいこと作るのって本当に難しい。。。
  - リファクタリングのしにくさがTerraformの課題のひとつだと思う。

ということでまとめてみました。


# tfstate
tfstateファイルはTerraformが管理しているリソースの現在の状態を表すファイルです。

デフォルトではローカルにtfstateファイルが生成されますが、多くの現場ではs3などのリモートバックエンドで管理していると思います。

## tfstateの存在意義

`terraform plan` を実行すると、現状と記述した定義との差分を表示してくれますが、
これはtfstateとHCLで記述された定義との差分を出しています。
tfstateではなく、実際のリソースとの差分を見ればいいじゃないか！と思っちゃうんですが、
パフォーマンスやリソース間の依存関係などの観点から、Terraform自体が状態の把握をしておかなければいけないんだよ、
ということが公式のドキュメントに記載されていますね。
https://www.terraform.io/docs/state/purpose.html

## tfstateファイルの作成単位

端的に言って`terraform apply`コマンドを実行した単位です。
dev環境, stg環境, 本番環境ごとに`terraform apply`しているならば、
tfstateファイルはdev.tfstate, stg.tfstate, prd.tfstateの3つになります。

他にも、リソースの種類ごとにコマンド実行単位を分ける方法があると思います。
これなら変更頻度が高そうな、EC2, ECSなどのアプリレイヤに近いリソース更新と、
あまりバシバシ変えたくないVPCなどのネットワークレイヤのリソース更新を完全に分けることができますね。

私が関わったプロジェクトだと、環境で分けて、かつ、リソースでも分ける方式が多かったです。
tfstateファイルを保存しているs3を見てみるとこんなかんじ。
バケット自体を環境ごとに分けて、リソースごとにtfstateファイルがあります。

![スクリーンショット 2019-12-21 14.16.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159225/36f422a6-b199-0399-0acd-e70abab505b7.png)

([simply](https://juju-62q.hatenablog.com/entry/2019/12/06/000000)というのは今私が関わってるプロジェクトの名前です :pray: )

tfstateファイルが分かれていると、前述の通り、変更の範囲を限定することができるので安全性や実行時間の短縮などのメリットが得られます。
しかしメリットばかりではなく、デメリットもあります。
Terraformのディレクトリ構成を変える必要に迫られた際に、tfstateファイルの作成単位も変わるケースがあり、
旧tfstateファイルが管理するリソースを新tfstateファイルに移動したり、改めてimportしなおしたりする必要があるわけですが、この作業が複雑になりがちです。
このときにtfstateの構造を知っておかないと効率的に作業をすすめられません。

## tfstateの構造

さて、tfstateの実際の内容は以下のようなjsonファイルです。
ECSのserviceのtfstateです。説明しない部分(resources.instances以下の...部分)は削ってますので実際のファイルとは違います。


```
{
  "version": 4,
  "terraform_version": "0.12.14",
  "serial": 1,
  "lineage": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "outputs": {},
  "resources": [
     {
      "module": "module.ecs",
      "mode": "managed",
      "type": "aws_ecs_service",
      "name": "xxx-api",
      "provider": "provider.aws",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "cluster": "arn:aws:ecs:ap-northeast-1:xxxxxxx:cluster/dev",
            "desired_count": 1,
            "id": "arn:aws:ecs:ap-northeast-1:xxxxxxx:service/dev-xxx-api",
            "launch_type": "FARGATE",
            "name": "dev-xxx-api",
            "task_definition": "dev-xxx-api:1",
            ...
          },
          "dependencies": [
            "module.ecs_service.xxx-api.aws_ecs_task_definition.task"
          ]
        }
      ]
    },
    ...
  ]
}
```

Terraform 0.12.14で実行した場合、tfstateファイルのversionは4になります。
version4のtfstateファイルのフォーマットはこちら。

```
type stateV4 struct {
	Version          stateVersionV4           `json:"version"`
	TerraformVersion string                   `json:"terraform_version"`
	Serial           uint64                   `json:"serial"`
	Lineage          string                   `json:"lineage"`
	RootOutputs      map[string]outputStateV4 `json:"outputs"`
	Resources        []resourceStateV4        `json:"resources"`
}
```
https://github.com/hashicorp/terraform/blob/master/states/statefile/version4.go#L520-L527

実際に管理されているリソース達の情報は`resources`以下に追加されていきます。
`resources`以下には

```
"module": "module.ecs",
"mode": "managed",
"type": "aws_ecs_service",
"name": "xxx-api",
"provider": "provider.aws",
"instances": [ {...}, ...]
```
という記述がありますが、それぞれ以下の意味を表しています。

- module: `module`タグの名前。 `module "ecs" { ... }`のHCL記述に対応。
- mode: managed or dataの値になる。managedは`resource`タグで管理しているもの。dataは`data`タグで管理しているもの。
- type: `resource`タグのtype。`resource "type" "name" { ... }`のHCL記述に対応。
- name: `resource`タグのname。`resource "type" "name" { ... }`のHCL記述に対応。
- provider: このリソースのプロバイダ。
- instances: 管理されているリソースの情報。

Terraformにおいて、ひとつのリソースを一意に特定するには、
`${module}.${type}.${name}`としなければいけません。
ちなみに`module`タグを使用していない場合はmoduleの項目自体がtfstate上には含まれなくなります。

`resources`以下のデータの構造はコードではこの部分。

```
type resourceStateV4 struct {
	Module         string                  `json:"module,omitempty"`
	Mode           string                  `json:"mode"`
	Type           string                  `json:"type"`
	Name           string                  `json:"name"`
	EachMode       string                  `json:"each,omitempty"`
	ProviderConfig string                  `json:"provider"`
	Instances      []instanceObjectStateV4 `json:"instances"`
}
```

https://github.com/hashicorp/terraform/blob/master/states/statefile/version4.go#L545-L553

↑に例示したtftateファイルには`EachMode`が含まれていないのですが、Terraform 0.12から追加されたfor_eachを利用した場合にはこのパラメータも含まれてきます。
https://www.hashicorp.com/blog/hashicorp-terraform-0-12-preview-for-and-for-each/

# まとめ

tfstateを調べた経緯はTerraformのディレクトリ構成を変えたいという強い気持ちがあったからでした。
ディレクトリ構成を変更するためには、tfstateを編集するための`terraform state`コマンドや、既存リソースをtfstateに取り込む`terraform import`コマンドを実行する必要がありますが、tfstateの概要をさらっとでも知っておくことで、作業の効率化を考えたりできて良かったですね。
ただ、それでもtfstateを自分で編集していく作業は苦行です。
[terraformer](https://github.com/GoogleCloudPlatform/terraformer)などのツールが進化していくことを祈ります(module対応してくれ) :pray: :pray: :pray: