tfstateを分割管理するためのTips

# tfstateを分割管理するためのTips

## はじめに

この記事は[terraform Advent Calendar 2019](https://qiita.com/advent-calendar/2019/terraform)の4日目です。

Terraformのstateを分割すると享受できるメリットがある一方、stateを分割することで発生する課題もあります。そこで、Terraformのstateを分割管理する中で考慮したことをまとめます。

## stateを分割する影響

### メリット
- plan/applyが高速化する
- 各ステートの命名がシンプルになる（`this`を使った命名がしやすい）
    - 例えば`resource "aws_security_group" "this" {}`

### デメリット
- 管理するファイルが増える
- 同じような設定が増える

## どのような単位でstateを分割するか

リソースのライフサイクルごとに分割すると運用が楽です。
例えば、作成後削除することのないネットワークやデータベースと、状況により台数が増減したりリソースを入れ替えるWebサーバーは分割します。

## state間のデータ共有

別のstateに情報を提供するには、stateファイルをS3のようなリモートに保存し、提供する情報を`output`として出力します。
別ののstateの情報を利用する側は、[`terraform_remote_state`](https://www.terraform.io/docs/providers/terraform/d/remote_state.html)のデータソースを使い、`output`で出力された情報を参照します。

```hcl:提供側
terraform {
    backend "s3" {
        bucket  = "bucket"
        key     = "network.tfstate"
    }
}

resource "aws_subnet" "this" {
    vpc_id = aws_vpc.this.id
    availability_zone = var.availability_zone
    cidr_block = var.cidr_block
}

output "subnet_id" {
    value = aws_subnet.this.id
}
```

```hcl:利用側
data "terraform_remote_state" "network" {
    backend = "s3"
    config = {
        bucket = "bucket"
        key = "network.tfstate"
    }
}

resource "aws_instance" "this" {
    subnet_id = data.terraform_remote_state.network.outputs.subnet_id
}
```

state間で共有する情報は、極力リソースを特定する最小限の情報にします。AWSの場合、リソースの種類によりますが、`id`や`arn`になります。
`id`や`arn`以外のリソースの属性を利用する場合は[データソース](https://www.terraform.io/docs/configuration/data-sources.html)を使って参照します。
データソースを経由することで、提供側と利用側の依存を減らし、利用側が常に最新の値を参照できるようになります。リモートステート経由で属性値を参照すると、ステートに記録された値と実際の値に差分がある場合に不整合が発生する可能性があります。

次のコードは[`aws_subnet`のデータソース](https://www.terraform.io/docs/providers/aws/d/subnet.html)を利用してサブネットの属性値の`cidr_block`を参照する例です。

```hcl
data "terraform_remote_state" "network" {
    backend = "s3"
    config = {
        bucket = "bucket"
        key = "network.tfstate"
    }
}

data "aws_subnet" "this" {
    subnet_id = data.terraform_remote_state.network.outputs.subnet_id
}

resource "aws_security_group" "this" {
  vpc_id = data.aws_subnet.this.vpc_id

  ingress {
    cidr_blocks = [data.aws_subnet.this.cidr_block]
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
  }
}
```

## コードをDRYにする

### 共通の変数をDRYに

```:プロジェクト構造
├── state1
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── state2
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
└── state3
    ├── main.tf
    ├── outputs.tf
    └── variables.tf
```

上記のようなプロジェクト構造で共通の変数`environment`がある場合、愚直に書くと次のようになります。

```hcl:state1/variables.tf
variable "environment" {
    default = "dev"
}

variable "variable1" {
    default = 1
}
```

```hcl:state2/variables.tf
variable "environment" {
    default = "dev"
}

variable "variable2" {
    default = 2
}
```

```hcl:state3/variables.tf
variable "environment" {
    default = "dev"
}

variable "variable1" {
    default = 3
}
```

このようなパターンでは、ディレクトリごとの環境変数を管理する[direnv](https://github.com/direnv/direnv)で変数に設定する値をDRYにできます。

Terraformはプレフィックスが[TF_VAR_](https://www.terraform.io/docs/commands/environment-variables.html#tf_var_name)の環境変数を変数への入力として扱います。
例えば、`TF_VAR_environment`の値は`variable "environment" {}`に設定されます。
この機能を利用し、direnvで変数へ設定する値を管理します。

```:direnvを導入したプロジェクト構造
├── .envrc            # 共通の環境変数を設定
├── state1
│   ├── .envrc        # state1固有の環境変数を設定
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── state2
│   ├── .envrc        # state2固有の環境変数を設定
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
└── state3
    ├── .envrc        # state3固有の環境変数を設定
    ├── main.tf
    ├── outputs.tf
    └── variables.tf
```

```hcl:state1/variables.tf
variable "environment" {}

variable "variable1" {}
```

```hcl:state2/variables.tf
variable "environment" {}

variable "variable2" {}
```

```hcl:state3/variables.tf
variable "environment" {}

variable "variable1" {}
```

```:.envrc
export TV_VAR_environment=dev # 共通の入力environmentを設定
```

```:state1/.envrc
source_up                     # 上位の.envrcを継承

export TV_VAR_variable1=1     # 個別の入力variable1を設定
```

```:state2/.envrc
source_up                     # 上位の.envrcを継承

export TV_VAR_variable2=2     # 個別の入力variable2を設定
```

```:state3/.envrc
source_up                     # 上位の.envrcを継承

export TV_VAR_variable3=3     # 個別の入力variable3を設定
```

direnvで設定する環境変数は.envrcに設定します。上位のディレクトリの.envrcを継承する場合は`source_up`を設定します。


### 変数以外もDRYに

最新のTerraform v0.12では`terraform`ブロックの中で動的な値を利用する手段は提供されていません。
そのため、各stateでリモートステートを保存するkeyだけ変えれば良くても、次のように`terraform`ブロックをすべて設定する必要があります。

```hcl
terraform {
  backend "s3" {
    bucket         = "bucket"
    key            = "path/to/terraform.tfstate"
    region         = "ap-notheast-1"
    encrypt        = true
  }
}
```

このようなTerraform自体では提供されていない機能をカバーするラッパーとして[Terragrunt](https://github.com/gruntwork-io/terragrunt)があります。

Terragruntの使い方自体は公式のドキュメントに譲りますが、次のような機能が利用できます。

- リモートステートの設定をテンプレート化
- terraformコマンドのデフォルトの引数を設定
    - 例えば`plan`コマンドにデフォルトで[`--parallelism`オプション](https://www.terraform.io/docs/commands/apply.html#parallelism-n)を設定して高速化
- リモートステート間の依存関係を定義
- すべてのstateに対して`plan`, `apply`, `destroy`を実行