Terraform の State を理解する

なんとなく使っている Terraform の State ファイルについて理解を深めてみたのでブログにまとめてみる。

# [State の目的](https://www.terraform.io/docs/state/purpose.html)

## リアルワールドへのマッピング
Terraform のステートファイルは、実際にデプロイされているインフラストラクチャと、そのマッピングに使われる。最初のTerraformのバージョンは、ステートファイルがなかったそうで、Tagで管理していたが、クラウドプロバイダによっては、そういうのがな無いため、すぐに問題になったとのこと。

_terraform.tf_

```
provider "azurerm" {
  version = "=1.39.0"
}
terraform {
    backend "azurerm" {
        resource_group_name = "RemoveTerraform"
        storage_account_name = "tsushistatetf"
        container_name = "tfstate"
        key = "terraform.tfstate"
    }
}
resource "azurerm_resource_group" "test" {
  name     = "testResourceGroup1-${terraform.workspace}"
  location = "West US"

  tags = {
    environment = "${terraform.workspace} branch"
  }
}
```
このコンフィグファイルに対して生成されるステートファイルを見てみよう。これは既に Destroy したリソースだが、インフラストラクチャのIdentifier など具体的なデータつまり、ステートが生成されている。これによって、Terraform は、定義されたインフラが、実際のインフラのどのインスタンスに相当するのかを把握している。

```
{
  "version": 4,
  "terraform_version": "0.12.18",
  "serial": 5,
  "lineage": "3822a78b-94a1-0b60-996a-aec909561c62",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "azurerm_resource_group",
      "name": "test",
      "provider": "provider.azurerm",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "id": "/subscriptions/{MY_SUBSCRIPTION_ID}/resourceGroups/testResourceGroup1",
            "location": "westus",
            "name": "testResourceGroup1",
            "tags": {
              "environment": "test branch"
            }
          },
          "private": "{something}"
        }
      ]
    }
  ]
}

```

## メタデータ
マッピング以外にも、terraform がステートファイルを持つ理由がある。メタデータを保持している。リソース間の依存関係が例えばそれである。リソースを削除するときにも、依存関係がわかっていないと適切に削除することができない。また、AzureやAwsなののプロバイダのコンフィグレーションなども含まれる。

サンプルとして、virtual network を上記のtfファイルに追加してみた。

_terraform.tf_

```
provider "azurerm" {
  version = "=1.39.0"
}
terraform {
    backend "azurerm" {
        resource_group_name = "RemoveTerraform"
        storage_account_name = "tsushistatetf"
        container_name = "tfstate"
        key = "terraform.tfstate"
    }
}
resource "azurerm_resource_group" "test" {
  name     = "testResourceGroup1-${terraform.workspace}"
  location = "West US"

  tags = {
    environment = "${terraform.workspace} branch"
  }
}

resource "azurerm_virtual_network" "test" {
    name = "virtualNetwork1"
    location = "${azurerm_resource_group.test.location}"
    resource_group_name = "${azurerm_resource_group.test.name}"
    address_space = ["10.0.0.0/16"]
    tags = {
        environment = "${terraform.workspace} branch"
    }
}
```

tf ファイルでは明確に指定していないが、dependency が追加されている。

```
{
  "version": 4,
  "terraform_version": "0.12.18",
  "serial": 2,
  "lineage": "cf4a560a-1759-63d1-7b4b-6ecae95fe7c3",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "azurerm_resource_group",
      "name": "test",
      "provider": "provider.azurerm",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "id": "/subscriptions/{MY_SUBSCRIPTION}/resourceGroups/testResourceGroup1-pr-branch-one",
            "location": "westus",
            "name": "testResourceGroup1-pr-branch-one",
            "tags": {
              "environment": "pr-branch-one branch"
            }
          },
          "private": "{something}"
        }
      ]
    },
    {
      "mode": "managed",
      "type": "azurerm_virtual_network",
      "name": "test",
      "provider": "provider.azurerm",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "address_space": [
              "10.0.0.0/16"
            ],
            "ddos_protection_plan": [],
            "dns_servers": null,
            "id": "/subscriptions/{MY_SUBSCRIPTION}/resourceGroups/testResourceGroup1-pr-branch-one/providers/Microsoft.Network/virtualNetworks/virtualNetwork1",
            "location": "westus",
            "name": "virtualNetwork1",
            "resource_group_name": "testResourceGroup1-pr-branch-one",
            "subnet": [],
            "tags": {
              "environment": "pr-branch-one branch"
            }
          },
          "private": "{something}",
          "dependencies": [
            "azurerm_resource_group.test"
          ]
        }
      ]
    }
  ]
}

```

## パフォーマンス
ステートファイルは、リソースのアトリビュートのキャッシュを保持している。`terraform plan` や `terraform apply` をしたときに、デフォルトの振る舞いでは現在デプロイされているリソースを調査するために、APIに対してクエリを投げて確認する。これが、大きなインフラストラクチャになると、そのクエリが問題になる。クラウドプロバイダでは、大抵単位時間のAPIリクエストに対して制限がある上に、１回ですべてのリソースの回答をするAPIは通常存在しない。大きなインフラの場合、これが問題になるケースがある。その場合は、`-refresh=false` と、`-target` フラグを使うと良い。ちなみに、`-target` は、デプロイするリソースを限定するオプション、`-refresh` はステートファイルを、現在のインフラからアップデートするためのオプションでデフォルトは、`true`になる。

## 同期

デフォルトでは、terraform はローカルのワーキングディレクトリにステートファイルを保持しているが、チームで仕事をするときは、ステートをシェアしたいと考えるだろう。その時には、リモートステートを使うことができて、例えば Storage Account にステートを保持して、シェアすることができる。

# 検査と、更新

ステートファイルはJsonファイルだが、それを自ら更新するのは推奨されない。[terraform state](https://www.terraform.io/docs/commands/state/index.html)サブコマンドを使うのが推奨される。

例えば、上記のバーチャルネットワークの部分をモジュールに移動するというケースを考えてみよう。tf ファイルをリファクタリングしてみる。`foo` というサブディレクトリを作って、次の２つのファイルを作る。

_foo/variables.tf_

```
variable location {
    description = "Location of the Virtual Net"
}

variable resource_group_name {
    description = "Resource Group Name for the virtual net"
}
```

_foo/virtualnet.tf_

```
resource "azurerm_virtual_network" "test" {
    name = "virtualNetwork1"
    location = var.location
    resource_group_name = var.resource_group_name
    address_space = ["10.0.0.0/16"]
    tags = {
        environment = "${terraform.workspace} branch"
    }
}
```
_terraform.tf_

```
provider "azurerm" {
  version = "=1.39.0"
}
terraform {
    backend "azurerm" {
        resource_group_name = "RemoveTerraform"
        storage_account_name = "tsushistatetf"
        container_name = "tfstate"
        key = "terraform.tfstate"
    }
}
resource "azurerm_resource_group" "test" {
  name     = "testResourceGroup1-${terraform.workspace}"
  location = "West US"

  tags = {
    environment = "${terraform.workspace} branch"
  }
}

module "virtualnet" {
    source = "./foo"

    resource_group_name = "${azurerm_resource_group.test.name}"
    location = "${azurerm_resource_group.test.location}"
}
```

このようにリファクタリングして、`terraform plan` をしてみる。すると、デプロイされているVirtualNetは変更したくないはずなのに、削除されて、再生成されてしまう。

```bash
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

azurerm_resource_group.test: Refreshing state... [id=/subscriptions/{MY_SUBSCRIPTION}/resourceGroups/testResourceGroup1-pr-branch-one]
azurerm_virtual_network.test: Refreshing state... [id=/subscriptions/{MY_SUBSCRIPTION}/resourceGroups/testResourceGroup1-pr-branch-one/providers/Microsoft.Network/virtualNetworks/virtualNetwork1]

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
  - destroy

Terraform will perform the following actions:

  # azurerm_virtual_network.test will be destroyed
  - resource "azurerm_virtual_network" "test" {
      - address_space       = [
          - "10.0.0.0/16",
        ] -> null
      - dns_servers         = [] -> null
      - id                  = "/subscriptions/{MY_SUBSCRIPTION}/resourceGroups/testResourceGroup1-pr-branch-one/providers/Microsoft.Network/virtualNetworks/virtualNetwork1" -> null
      - location            = "westus" -> null
      - name                = "virtualNetwork1" -> null
      - resource_group_name = "testResourceGroup1-pr-branch-one" -> null
      - tags                = {
          - "environment" = "pr-branch-one branch"
        } -> null
    }

  # module.virtualnet.azurerm_virtual_network.test will be created
  + resource "azurerm_virtual_network" "test" {
      + address_space       = [
          + "10.0.0.0/16",
        ]
      + id                  = (known after apply)
      + location            = "westus"
      + name                = "virtualNetwork1"
      + resource_group_name = "testResourceGroup1-pr-branch-one"
      + tags                = {
          + "environment" = "pr-branch-one branch"
        }

      + subnet {
          + address_prefix = (known after apply)
          + id             = (known after apply)
          + name           = (known after apply)
          + security_group = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 1 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
```

このような時に、ステートの変更をしてみます。

```bash
$ terraform state mv 'azurerm_virtual_network.test' 'module.virtualnet.azurerm_virtual_network.test'
Move "azurerm_virtual_network.test" to "module.virtualnet.azurerm_virtual_network.test"
Successfully moved 1 object(s).
```

すると、Module に移したリソースを、現在のリソースと認識してくれます。ステートを変更したところで、もう一回 `terraform plan` を実施します。

```bash
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

azurerm_resource_group.test: Refreshing state... [id=/subscriptions/{MY_SUBSCRIPTION}/resourceGroups/testResourceGroup1-pr-branch-one]
module.virtualnet.azurerm_virtual_network.test: Refreshing state... [id=/subscriptions/{MY_SUBSCRIPTION}/resourceGroups/testResourceGroup1-pr-branch-one/providers/Microsoft.Network/virtualNetworks/virtualNetwork1]

------------------------------------------------------------------------

No changes. Infrastructure is up-to-date.

```

ただしく、同じリソースだと認識してくれるようになりました。

# フォーマット

ステートのフォーマットは、JSONです。ステートファイルは後方互換性を保持しています。terraform のバグがあった場合、自作のツールなどで簡単に更新できるでしょう。バージョンのフィールドがあり変更があったときに、先に進むかを明確にしてくれます。

手元で試したところ、手動で、`version` を変更すると、そのリソースが存在しないかのような動きをしました。つまり、tfファイルのリソースを別途作ろうとするような振る舞いになりました。`version` を元に戻すと、リソースは、up to date と表示されたので、元に戻るようです。

# まとめ

今回は terraform のステートについて学んでみました。次回は terraform のテストツールに関して学んでみたいと思います。


# Resource 

* [State](https://www.terraform.io/docs/state/index.html)
* [Create a Terraform Module](https://www.linode.com/docs/applications/configuration-management/create-terraform-module/)