Terraform ステートを試してみた

# 背景・目的
前回、[TerraformでS3を作成してみた](https://qiita.com/zumax/items/4a7e1002050a5f2e5903)では、Terraformを介して、S3バケットを作成してみました。
今回は、詳解 [Terraform 第3版](https://www.oreilly.co.jp/books/9784814400522/)の、「第3章 Terraformステートを管理する」を元に、ステート管理を学びます。


# まとめ
下記に特徴をまとめます。

|特徴|説明|
|:--|:--|
|Terraform ステートファイル|・どのようなインフラを構築した情報を記録する場所<BR>・作成した場所にterraform.tfstateというファイルを作成する。<BR>・作成は、`terraform apply`実行後|
|クラウドリソースとの紐づけ|下記でクラウドリソースを紐づけている。<BR>・Terraformのtypeとname<BR>・id|
|ステートファイルの共有ストレージ|リモートバックエンドとローカルバックエンドがある|
|リモートバックエンドの利点| AWSなどのクラウドプロバイダーのストレージでは、共有、ロック、機密性をサポートしているので適している|
|Terraformバックエンドの制限|・Terraformステートを保存するS3バケットをTerraformで作る鶏卵<BR>・backendブロックは、変数や参照を使用できない|
|ステートファイルの分離|・ワークスペースと、ファイルレイアウトによる分離がある<BR>わかりやすさと、明確な分離のためには、ファイルレイアウトによる分離が良い。|


# 概要
Terrfaromがインフラの状態を追跡する方法と、Terraformプロジェクトのファイルレイアウト、分離、ロックへ与える影響について整理します。

## Terraformステートとは
- Terraformステートファイル
    - Terraformでは、どんなインフラを構築した情報を記録する場所
    - `/foo/bar`フォルダで実行した場合、デフォルトで```/foo/bar/terraform.tfstate```というファイルを作成する
    - terraform applyを実行すると、terraform.tfstate作成する
- リソースの対応付け
    - Typeとname、idでAWSリソースを紐づける

- 個人プロジェクトで使用するなら、ローカルPCでよいが、実際のプロジェクトでは下記の問題がある
    - ステートファイルの共有
        - 各チームメンバーで使用するには、共有された場所に置く必要がある
    - ステートファイルのロック
        - ロックの仕組みがないと、同時に更新した場合に競合が発生し、設定の衝突、破壊などの問題が起こり得る
    - ステートファイルの分離
        - インフラに変更する場合は、テストや本番など環境を分離するべき

## ステートファイルの共有ストレージ
- 複数チームが共通のファイルアクセスできるようにする方法として、Git等のバージョン管理システムは下記の理由から推奨されていない
    - PushやPullの忘れによりデグレードが発生する
    - 同時のapplyを実行するのを防止するロックの仕組みがない
    - Terraformリソースの一部は機密情報を保持する必要がある。プレーンテキストとして登録されてしまう
- 最適なものは、Terraformに組み込まれたリモートバックエンドの機能である
    - Terraformがどのようにステートをロードしたり、保存するかきめるもの
    - ローカルバックエンド
        - デフォルト
        - ローカルディスクに保存
    - リモートバックエンド
        - ステートファイルをリモート共有ストレージに保存
        - S3、Azure Blob、GCS、HashicorpのTerrafrom Cloud等
- リモートバックエンドにより上記の問題を解決する
    - planやapplyを実行する都度、ステートファイルをバックエンドから自動的にロードする。apply後はステートファイルをバックエンドに自動で保存する
    - リモートバックエンドは、ネイティブでロックをサポートしている
    - リモートバックエンドは、アクセス権の設定が可能、送受信の暗号化、ストレージレベルの暗号化をサポート

## Terraformバックエンドの制限
- Terraformステートを保存するS3バケットをTerraformで作る点。鶏卵問題
    - 一度作成してから、バックエンドの設定をしたあと削除する
- backendブロックは、変数や参照を使用できない
    - Terraformモジュールごとに毎回S3バケット名等をコピペしなければならない
    - 他のTerraformモジュールのステートを上書きしないように、keyの値はコピーせずに、一意なKeyを指定する必要がある。そのため、間違いが発生しやすい
    - コピペを減らす方法は、Terraformコード内のbackend設定から、一部の設定を削除し、代わりに`terraform init`時に`-backend-config`コマンドライン引数を渡す
    

## ステートファイルの分離
Terraformステートが1つの場合、1つの間違いで全体を破壊する可能性がある。例えばテスト環境の変更で、本番を壊すなど。
ステートファイルを分離するには、下記の2つがある
- ワークスペースによる分離
    - 同じ設定に対して簡単、かつ分離されたテストを行う
- ファイルレイアウトによる分離
    - 環境感で協力な分離が必要になる。本番でのユースケースに便利


### ワークスペースによる分離
Terraformワークスペースを使うと、Terraformステートをワークスペースに保存できる
- 別のワークスペースから分離され、それぞれに名前をつけられる
- 指定しない場合は、defaultと呼ばれる1つのワークスペースが使われる
- 新しいワークスペースや、ワークスペース間を移動する場合、terraform workspaceコマンドを使う

下記の課題がある
- すべてのワークスペースファイルが同じバックエンドに保存されるため、環境の分離に課題がある
- どのワークスペースを使用しているか分かりづらい

### ファイルレイアウトによる分離
環境の完全な分離を実現するには、下記の方法が必要
- 各環境のTerraformファイルを、それぞれ別のフォルダに入れる
- 環境ごとに異なるバックエンドを設定し、それぞれに対して異なる認証情報を使い、違うアクセス権を設定する

ファイルレイアウトによる分離により、わかりやすくなり、影響範囲を局所化できる。
また、環境だけではなくコンポーネントごとに分離することで更に影響範囲を局所化できる。下記のようなイメージである。

```
|-stage
  |--vpc
  |--services
|-prod
  |--vpc
  |--services
```

各コンポーネントのフォルダ内には、下記のような命名規則に従った設定ファイルが含まれる

- variables.tf
    - 入力変数
- outputs.tf
    - 出力変数
- main.tf
    - リソースとデータソース

Terraformを実行すると、カレントディレクトリに拡張子.tfを探すため、実際はどのようなファイル名を使用しても問題ない。
しかし、命名規則に従ったほうがチームで開発がしやすくなる。
更に進めるとしたら、下記のようなものがある

- dependencies.tf
    - データソースをすべて含めることで、コードが外部に依存している部分をわかりやすくなる
- providers.tf
    - providerブロックを含めると、使用するプロバイダと認証情報がわかる
- main-xxx.tf
    - main-vpc、main-iamなどに分けることで、グループ化されてわかりやすくなる

# 実践
## 事前準備
### SSOの設定
AWS SSOを事前に設定しておきます。([参考:AWS SSOの設定](https://qiita.com/zumax/items/ee18a9b115f2c8a5a49e#aws-sso%E3%81%AE%E8%A8%AD%E5%AE%9A))

### バックエンド用のS3を用意
:::note info
バックエンド用のS3バケットは、手動で作成しています
:::

1. AWSにサインインします
1. S3で、バケットを作成します
1. ロールバックできるようにバージョニングを有効にします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/26356872-7a71-9fbb-da1e-e04fdaea8b24.png)



## はじめに(stateファイルを確認)
1. main.tfを準備します
    ```
    data "aws_caller_identity" "this" {}
    
    terraform {
      required_version = "~> 1.8.0"
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.44.0"
        }
      }
    }
    
    provider "aws" {
      region = "ap-northeast-1"
    
      profile="my-admin"
    }
    
    resource "aws_s3_bucket" "terraform_demo" {
      bucket = "terraform-demo-${data.aws_caller_identity.this.account_id}"
    }
    ```
1. `terraform apply`します
    ```
    $ terraform apply
    data.aws_caller_identity.this: Reading...
    data.aws_caller_identity.this: Read complete after 0s [id=XXX]
    
    Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
      + create
    
    Terraform will perform the following actions:
    
      # aws_s3_bucket.terraform_demo will be created
      + resource "aws_s3_bucket" "terraform_demo" {
          + acceleration_status         = (known after apply)
          + acl                         = (known after apply)
          + arn                         = (known after apply)
          + bucket                      = "terraform-demo-XXXX"
          + bucket_domain_name          = (known after apply)
          + bucket_prefix               = (known after apply)
          + bucket_regional_domain_name = (known after apply)
          + force_destroy               = false
          + hosted_zone_id              = (known after apply)
          + id                          = (known after apply)
          + object_lock_enabled         = (known after apply)
          + policy                      = (known after apply)
          + region                      = (known after apply)
          + request_payer               = (known after apply)
          + tags_all                    = (known after apply)
          + website_domain              = (known after apply)
          + website_endpoint            = (known after apply)
        }
    
    Plan: 1 to add, 0 to change, 0 to destroy.
    
    Do you want to perform these actions?
      Terraform will perform the actions described above.
      Only 'yes' will be accepted to approve.
    
      Enter a value: yes
    
    aws_s3_bucket.terraform_demo: Creating...
    aws_s3_bucket.terraform_demo: Creation complete after 2s [id=terraform-demo-XXX]
    
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
    $ 
    ```

1. terrafrom.tfstateファイルができていました。 backupファイルもあります
    ```
    $ ls -l
    total 32
    -rw-r--r--  1 abc  staff    59  4 28 12:22 README.md
    -rw-r--r--  1 abc  staff   379  4 28 19:25 main.tf
    -rw-r--r--  1 abc  staff  3346  4 28 19:27 terraform.tfstate
    -rw-r--r--  1 abc  staff   757  4 28 19:27 terraform.tfstate.backup
    $
    ```

1. terraform.tfstateを確認してみます。typeとname、idが記載されています。これで紐づけされているとのこと
    ```
    $ cat terraform.tfstate
    {
      "version": 4,
      "terraform_version": "1.8.0",
    
       ~~~省略~~~
    
        },
        {
          "mode": "managed",
          "type": "aws_s3_bucket",
          "name": "terraform_demo",
          "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
          "instances": [
            {
              "schema_version": 0,
              "attributes": {
                "acceleration_status": "",
                "acl": null,
                "arn": "arn:aws:s3:::terraform-demo-XXX",
    
       ~~~省略~~~
            "id": "terraform-demo-XXX",
    ```

## Terraformバックエンドを設定する
1. 下記を設定します
    ```
    terraform {
      backend "s3" {
        bucket  = "terraform-s3-bucket-backend-XXX"
        key     = "global/s3/terraform.tfstate"
        region  = "ap-northeast-1"
        encrypt = true
        profile = "my-admin"
      }
    }
    ```
1. `terraform init`を実行します。つぎに、ローカルをバックエンドにコピーするか聞かれるのでyesとします
    ```
    $ terraform init
    
    Initializing the backend...
    Do you want to copy existing state to the new backend?
      Pre-existing state was found while migrating the previous "local" backend to the
      newly configured "s3" backend. No existing state was found in the newly
      configured "s3" backend. 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
    ```

1. 正常に終了しました
    ```
    Successfully configured the backend "s3"! Terraform will automatically
    use this backend unless the backend configuration changes.
    
    Initializing provider plugins...
    - Reusing previous version of hashicorp/aws from the dependency lock file
    - Using previously-installed hashicorp/aws v5.44.0
    
    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.
    $
    ```
1. S3にコピーされていました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/cf78545f-53df-f453-d2b0-211c087c83be.png)

1. なお、ローカルに出力されていた tfstateは、空になっていました
    ```
    $ cat terraform.tfstate                      
    $
    ```


## バックエンドの変数化
1. 上記で定義した、backendの設定をbackend.hclに移します
    ```
    bucket  = "terraform-s3-bucket-backend-XXXX"
    region  = "ap-northeast-1"
    encrypt = true
    profile = "my-admin"
    ```
1. keyは残しておきます
    ```
    terraform {
      backend "s3" {
        key     = "global/s3/terraform.tfstate"
      }
    }
    ```

1. `terraform init`に、`-backend-config`を渡します
    ```
    $ terraform init --backend-config=backend.hcl
    
    Initializing the backend...
    
    Initializing provider plugins...
    - Reusing previous version of hashicorp/aws from the dependency lock file
    - Using previously-installed hashicorp/aws v5.44.0
    
    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.
    $
    ```

## backend を分離する
今まで、main.tf内に定義したbackendを分離します
1. 下記をbackend.tfに移します
    ```
    terraform {
      backend "s3" {
        key     = "global/s3/terraform.tfstate"
      }
    }
    ```
1. `terraform init`を実行します
    ```
    $ terraform init --backend-config=backend.hcl
    
    Initializing the backend...
    
    Initializing provider plugins...
    - Reusing previous version of hashicorp/aws from the dependency lock file
    - Using previously-installed hashicorp/aws v5.44.0
    
    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.
    $
    ```
## provider を分離する
今まで、main.tf内に定義したproviderを分離します
1. 下記をprovider.tfに移します
    ```
    terraform {
      required_version = "~> 1.8.0"
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.44.0"
        }
      }
    }
    
    provider "aws" {
      region  = "ap-northeast-1"
      profile = "my-admin"
    }
    ```
1. `terraform init`を実行します
    ```
    $ terraform init --backend-config=backend.hcl
    
    Initializing the backend...
    
    Initializing provider plugins...
    - Reusing previous version of hashicorp/aws from the dependency lock file
    - Using previously-installed hashicorp/aws v5.44.0
    
    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.
    $
    ```

## 環境を分離する
環境ごとにフォルダを分けます

1. prodディレクトリを作成します
    ```
    mkdir -p environments/prod
    ```
1. mvします
    ```
    $ mv .terraform environments/prod 
    $ mv .terraform.lock.hcl environments/prod 
    $ mv backend.hcl environments/prod 
    $ mv backend.tf environments/prod 
    $ mv main.tf environments/prod 
    $ mv provider.tf environments/prod 
    $ ls -l environments/prod 
    total 32
    -rw-r--r--  1 XXX  staff  116  4 28 22:01 backend.hcl
    -rw-r--r--  1 XXX  staff   79  4 28 22:43 backend.tf
    -rw-r--r--  1 XXX  staff  156  4 28 22:47 main.tf
    -rw-r--r--  1 XXX  staff  223  4 28 22:47 provider.tf
    $
    ```
1. `terraform init`を実行します
    ```
    $ terraform init --backend-config=backend.hcl
    
    Initializing the backend...
    
    Initializing provider plugins...
    - Reusing previous version of hashicorp/aws from the dependency lock file
    - Using previously-installed hashicorp/aws v5.44.0
    
    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.
    $
    ```


# 考察
今回、Terraformステートを整理してみました。次回はこちらを参考に、VPCやEC2等を作成してみます。


# 参考
https://www.oreilly.co.jp/books/9784814400522/

https://qiita.com/zumax/items/4a7e1002050a5f2e5903

https://qiita.com/zumax/items/ee18a9b115f2c8a5a49e