terraform.tfstateをローカルやS3で管理する方法

# はじめに
Terraformを使っていた際に、ローカルやS3でterraform.tfstateを管理する方法に結構つまづきました。
そのため管理する各場所に対し、どんなコードを書いたりコマンドを実行したりするのかまとめました。

# つまづきと解決
※管理方法のみを見たい人は、この項目はスキップしてください。
## つまづき
いろんなサイトでterraform.tfstateを管理する方法が記載してあったため、それらのサイトを基に、S3のバケットで保存する方法を試していました。その際、紹介されていたコマンド(```terrafrom init```)を実行しても、S3のバケット内にterraform.tfstateが作成されませんでした。

なぜだろうと考え、以下2つの視点で原因を探りました。
1. 誰かが経験したかもしれないのでGitHub Issueやブログ等を確認する
2. ログを見ながらTerraformのソースコードを追いかける

1に関しては、以下のような点に着目した記事がありました。ですが実際に試しても問題は解決しませんでした。

- この記事で書いてあるようなファイルの書き方
- バケットのポリシー設定方法

2に関しては、Terraformの[公式ページ](https://www.terraform.io/docs/internals/debugging.html)を基にして、terraformコマンド実行時に```TF_LOG=TRACE```をつけてログを確認しました。そのログを使ってTerraformの[ソース](https://github.com/hashicorp/terraform)を見始めましたが、Go初心者である私にはざっとした流れしか分かりませんでした。

そのため1、2のどちらの方法でも、解決には至りませんでした。

## 解決
試行錯誤しながら進めていたとき、S3のバケットをふと見てみると、terraform.tfstateができている事に気づきました。そのためそれまで自分が行った修正やコマンドを振り返りながら、1つずつ切り分けし、ここに記載したパターンに行き着きました。

Goが分かる人であればコードで理解でき、パターンがすぐに分かったと思います。私はまだGoが分からない状態だったので、このような方法で解決に至りました。

# 環境
- Mac：macOS Catalina Version 10.15.5
- Terraform：v0.12.28

# 前提
ここではTerraformでVPCを作成する例をあげています。
また、この記事で紹介しているIAMユーザのポリシーやバケットの設定は、記事用としての設定です。実際の環境では、必要に応じたポリシーや設定を行ってください。


# IAMユーザを作成する
TerraformでVPCを作成するため、以下の２つを満たしたIAMユーザを作成しておきます。

- AmazonVPCFullAccessのアタッチ
- AWSアクセスの種類に「プログラムによるアクセス・アクセスキーを使用」を設定

IAMユーザ作成時にアクセスキー、シークレットキーが表示されるので、後述するaccess_key、secret_keyに設定します。
<img width="830" alt="Screen Shot 2020-07-22 at 19.34.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/9debd382-6711-d33e-796a-7a00470242e1.png">

# ローカルで管理する
## ファイルの作成とコマンド実行

以下のようにmain.tfを作成します。

```main.tf
provider "aws" {
  region  = "ap-northeast-1"
  access_key = "xxxxx"
  secret_key = "yyyyy"
}

# VPC
resource "aws_vpc" "vpc" {
  cidr_block = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-test"
  }
}
```

ｍain.tfを作成したら、以下3つのコマンドを実行します。
※ここでは```terraform apply```時に```-auto-approve```オプションをつけていますが、なくても問題ありません。

```
$ terraform init
$ terraform plan
$ terraform apply -auto-approve
```

```terraform apply```実行後、以下のように出力され、terraform.tfstateがローカルに作成されます。

```
$ terraform apply -auto-approve
aws_vpc.vpc: Creating...
aws_vpc.vpc: Creation complete after 4s [id=vpc-aaaaa]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
```

# S3で管理する
## IAMユーザにバケットアクセスのポリシーを追加する

Terraformで新たにバケットにアクセスするため、IAMユーザにポリシーを追加します。
アタッチするポリシーはAmazonS3FullAccessです。
<img width="867" alt="Screen Shot 2020-07-23 at 1.37.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/ea15447e-376c-a360-21a8-13bd1eba6463.png">

## S3でバケットを作成する

ここではバケット名をbucket-for-tfとして、「次へ」を押します。
<img width="1097" alt="Screen Shot 2020-07-21 at 21.10.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/5b4f0814-6689-fcfd-9a6c-c4baf9489e8a.png">

特に設定を行わずに「次へ」を押します。
<img width="901" alt="Screen Shot 2020-07-21 at 20.28.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/4cbcaae0-936b-b9cf-0a8d-48e817329deb.png">

「パブリックアクセスをすべてブロック」のチェックを外し、「現在の設定により・・・を了承します。」にチェックをつけます。
<img width="901" alt="Screen Shot 2020-07-21 at 6.59.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/408c523f-cc76-b6af-6034-cbbcb417abfe.png">

確認画面が出るので、「バケットを作成」ボタンを押してバケットを作成します。
<img width="1097" alt="Screen Shot 2020-07-21 at 21.11.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/25e74272-039e-7229-e5c2-aafca7ab9b5c.png">

S3上に、指定したバケット(buekct-for-tf)が作成されます。
<img width="856" alt="Screen Shot 2020-07-21 at 21.13.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/d1f56960-22c0-4ffd-2ced-0df749c2b51a.png">

## バケットにバケットポリシーを設定する

terraformがterraform.tfstateをS3に配置するためには、バケットポリシーの設定が必要です。terrformの[公式ページ](https://www.terraform.io/docs/backends/types/s3.html)を参照し、作成したIAMユーザやバケット名を使用して、以下のようにバケットポリシーを設定します。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111:user/terraform-bucket-manager"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::bucket-for-tf"
        },
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::11111:user/terraform-bucket-manager"
            },
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::bucket-for-tf/*"
        }
    ]
}
```
<img width="976" alt="Screen Shot 2020-07-21 at 21.25.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/144152d6-4935-6e65-e400-aa9593f82625.png">

## ファイルの作成とコマンド実行

以下のようにmain.tfを作成します。
基本的には「ローカルでterraformを管理する」項目と同じです。異なる点は、backendにs3を指定している点です。

```main.tf
provider "aws" {
  region  = "ap-northeast-1"
  access_key = "xxxxx"
  secret_key = "yyyyy"
}

terraform {
  backend "s3" { 
    access_key = "xxxxx"
    secret_key = "yyyyy"
    bucket  = "bucket-for-tf"
    region  = "ap-northeast-1"
    key     = "terraform.tfstate"
    encrypt = true
  }
}

# VPC
resource "aws_vpc" "vpc" {
  cidr_block = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-test"
  }
}
```

作成したら以下3つのコマンドを実行します。

```
$ terraform init
$ terraform plan
$ terraform apply
```

```terraform init```を行うと、バックエンドにs3を設定完了のメッセージが表示されます。

```
$ terraform init

Initializing the backend...

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

```terraform apply```まで行うと、main.tfで指定したS3のバケット(bucket-for-tf)にterraform.tfstateが出来上がります。
<img width="822" alt="Screen Shot 2020-07-21 at 21.40.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/1d91beb0-179f-a69d-e1ad-9c155949aed9.png">

# ローカルでの管理からS3での管理に移す
## ローカルでの管理

以下のようにmain.tfを作成します。
基本的には「S3でterraform.tfstateを管理する」項目と同じです。異なる点は、backendにlocalを指定している点です。

```main.tf
provider "aws" {
  region  = "ap-northeast-1"
  access_key = "xxxxx"
  secret_key = "yyyyy"
}

terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
}

# VPC
resource "aws_vpc" "vpc" {
  cidr_block = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-test"
  }
}
```

作成したら以下3つのコマンドを実行します。

```
$ terraform init
$ terraform plan
$ terraform apply -auto-approve
```

```terraform init```を実行すると、バックエンドにlocalを設定完了のメッセージが表示されます。

```
$ terraform init

Initializing the backend...

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

## S3での管理へ変更
```terraform apply```完了後、main.tfのbackendをs3に書き換えます。
ここで使用するIAMユーザ、S3のバケットについては、「IAMでユーザを作成する」、「S3でバケットを作成する」を参照してください。

```main.tf
provider "aws" {
  region  = "ap-northeast-1"
  access_key = "xxxxx"
  secret_key = "yyyyy"
}

terraform {
  backend "s3" { 
    access_key = "xxxxx"
    secret_key = "yyyyy"
    bucket  = "bucket-for-tf"
    region  = "ap-northeast-1"
    key     = "terraform.tfstate"
    encrypt = true
  }
}

# VPC
resource "aws_vpc" "vpc" {
  cidr_block = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-test"
  }
}
```

backendを変更したので以下コマンドを再実行します。

```
$ terraform init
```

terraform.tfstateをs3にコピーして良いか尋ねられるのでyesを入力します。

```
$ terraform init

Initializing the backend...
Backend configuration changed!

Terraform has detected that the configuration specified for the backend
has changed. Terraform will now check for existing state in the backends.


Terraform detected that the backend type changed from "local" to "s3".
・・・略・・・
 Do you want to copy this state to the new "s3"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes
```

yesを入力後、バックエンドにs3を設定完了のメッセージが表示されます。

```
$ terraform init
・・・略・・・
  Enter a value: yes

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

このタイミングで、S3のバケットにterraform.tfstateが作成されます。
<img width="830" alt="Screen Shot 2020-07-23 at 0.01.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/bce0f688-afa1-04af-4e0b-c856cedd95c2.png">

この後に```terraform apply```を実行します。
S3のバケットを見てみると、terraform.tfstateが更新されていることが分かります。

```
$ terraform apply -auto-approve
aws_vpc.vpc: Refreshing state... [id=vpc-aaaaa]

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
```

<img width="830" alt="Screen Shot 2020-07-23 at 0.02.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/bf052728-ca42-1ccb-0efa-42ad0c6a9170.png">

# おわりに
ローカルやS3でterraform.tfstateを管理する方法を記載しました。
Goが分かっていればもっと楽に解決できたと思います。

この記事が、未来の私や困っている誰かのお役に立てれば幸いです。

# 参考資料
[terraformのbackendをs3にしてinitするまでの最低限の手順](https://qiita.com/sinshutu/items/7d3cc7438871c50ea63c)
[Backend の S3 や DynamoDB 自体を terraform で管理するセットアップ方法](https://qiita.com/saiya_moebius/items/a8f8aa3683c2347d607c)
[Terraformをしばらく書いて覚えた個人的なTipsについて](https://dev.classmethod.jp/articles/my-terraform-tips/)
[terraformを利用したtfstateファイルのS3へのアップロードができない](https://teratail.com/questions/232053)