Terraformの`tfstate`を端末が変わっても管理できるようにS3に配置する

## 要約
最終的なソースコード全体は以下のリンクからご確認いただけます。

https://github.com/mochi-yu/qiita-material/tree/main/terraform-remote-state

## 関連記事
**Terraformの開発環境を整理**したのを備忘録も兼ねて複数の記事にしています。この記事はそのうちの2本目として、「Terraformの`tfstate`をS3で他者と共有して管理する」ことについてまとめます。

- [Terraformで開発環境と本番環境を分けて管理する](https://qiita.com/mochi_2225/items/5287ca115054746d90f9)
- **Terraformの`tfstate`をS3で他者と共有して管理する**（この記事）

## 背景
複数人のチームで開発をしていると、**インフラの構成を管理する人が複数人いる**ということもあるかと思います。または、**先代の担当者からインフラの管理を引き継ぐ**こともあるかもしれません。

「構成はTerraformで管理しているからGitのソースを見て」と渡されていざ見てみると、確かにTerraformの設定はされています。ただ`terraform plan`とかを実行すると全てのリソースが新規追加になってしまいました。

普段`terraform plan`を実行すると、**今のリソースと比べて変更される箇所、作り直される箇所、変わらない箇所などを教えてくれます**。これは「**今のリソース**」の情報をTerraform側で記憶しているからできることです。これを管理しているのが **`terraform.tfstate`** といった名前のファイルです。

ただこの`terraform.tfstate`には実際の**クラウド上のリソースの機微情報が紛れ込む**ことがあり、**Git管理することは望ましくありません**。したがって、[`Terraform.gitignore`](https://github.com/github/gitignore/blob/main/Terraform.gitignore)にも`*.tfstate`が含まれています。

なので上の状況を振り返ると、**どのようなリソースかの設定**はGit管理されていましたが、それに対して**今がどういう状況か**はGitでは管理されておらず、`terraform plan`しても比較対象がないため全て新規追加という扱いになっていました。これではだいぶ不便です。なにもチーム開発だけの話ではなくて、**個人開発の場合でも**複数のPCを使っていれば、**PCごとにどう`tfstate`を共有するのか**という問題になります。

https://zenn.dev/spacemarket/articles/b7bd1422b896ca

そこでよく使われる方法は、この **`tfstate`をローカルで保持するのではなく**、S3のような**リモートのオブジェクトストアで管理する**という手段です。この記事では、AWS S3に`tfstate`をアップロードするようにして、複数のPC間あるいはメンバー間からでも状態管理が行えるようにしたいと思います。

## 実装
`tfstate`をリモートで管理するという設定は、**terraformの`backend`の設定**をすることで実現できます。公式のドキュメントにはS3を使用した例も説明されています。S3の例ではDynamoDBを使って変更をロックする実装も説明されていますが、**内容が増えてしまうのでこの記事ではロックは設定しません**。

https://developer.hashicorp.com/terraform/language/settings/backends/configuration

https://developer.hashicorp.com/terraform/language/settings/backends/s3

ファイル構成は、[前の記事](https://qiita.com/mochi_2225/items/5287ca115054746d90f9)の環境ごとに分けたものから引き続き、以下のようになっています。

```
.
├── .gitignore
├── environments
│   ├── dev
│   │   ├── .terraform.lock.hcl
│   │   └── main.tf
│   └── prod
│       ├── .terraform.lock.hcl
│       └── main.tf
└── modules
    ├── main.tf
    ├── variables.tf
    └── vpc.tf
```

### backendの設定
実装の方針としてS3のバケット名やstateのファイルパスは環境ごとに変わることを想定し、**ハードコーディングではなく初期化時に渡す**という方法を取ります。`/environments`以下の各環境の`main.tf`を編集し以下の内容を追加しました。

後で指定が必要だよというのを分かりやすくするために、`bucket`と`key`の項目もコメントアウトして残すようにしました。

どちらの環境も、設定している内容が全く一緒なので **`/modules`以下におけば良いじゃん**と思い、最初はそうしていました。ただその場合だと **`terraform init`を実行した時にwarningが出てしまいました**。曰く、これらの設定は子のモジュールが持つのではなく、**ルートのモジュール（すなわち`/environments/dev`直下のファイル）に記述することが望ましい**ようです。

```terraform:/environments/dev/main.tf（追加箇所）
terraform {
  backend "s3" {
    region  = "ap-northeast-1"
    encrypt = false
    # bucket  = "XXXXXXXXXXXXXXX"
    # key     = "XXXXXXXXXXXXXXX"
  }
}
```

```terraform:/environments/prod/main.tf（追加箇所）
terraform {
  backend "s3" {
    region  = "ap-northeast-1"
    encrypt = false
    # bucket  = "XXXXXXXXXXXXXXX"
    # key     = "XXXXXXXXXXXXXXX"
  }
}
```

### 初期化時にバケット名とキーを指定する
上記の設定ができたら、`terraform init`を実行する時に`bucket`と`key`の項目を指定する必要があります。指定方法は[公式ドキュメント](https://developer.hashicorp.com/terraform/language/settings/backends/configuration#partial-configuration)にも説明がありますが、「ファイルで渡す」「コマンドライン引数で渡す」「対話型CLIで渡す」といった選択肢があるようです。今回は「**コマンドライン引数で渡す**」方法で実施してみます。

`/environments/prod`フォルダ、または`/environments/dev`フォルダに移動して、以下のコマンドを実行します。**バケット名は各自のものを設定**してください。

```shell
terraform init -backend-config=bucket="hoge" -backend-config=key="test_vpc/dev.tfstate"
```

こうすることで、ローカルで`terraform apply`と実行しても **`/environments/dev/`直下に`terraform.tfstate`ファイルは作られず**、リモートの**S3上に`dev.tfstate`のファイルが作成されている**ことを確認できるかと思います。

このリモートのstateファイルを、都度ローカルでの実行時に参照することで、別の端末でも、複数のユーザでも同じ挙動をすることができるようになりました。ですので、`terraform apply`を実行するユーザには、この**S3バケットを参照するIAMポリシーを設定**する必要がありますので、あらかじめご確認ください。

## 参考

https://developer.hashicorp.com/terraform/language/settings/backends/configuration

https://developer.hashicorp.com/terraform/language/settings/backends/s3