単一のTerraform Rootディレクトリで複数環境を制御する方法

# はじめに
この記事は [terraform Advent Calendar 2022](https://qiita.com/advent-calendar/2022/terraform) の19日目です。

IaCツールの1つであるTerraformのディレクトリ構成をどうするかについては、Terraformを利用しているエンジニアであれば一度は考えたことがあるのではないでしょうか。
私も、2年前のAdvent Calendarにて投稿させていただきました。 （[記事リンク](https://qiita.com/hkak03key/items/cecfe063cbb7eac15401)）

さて、Terraformのディレクトリ構成で検討するべき項目として、「複数環境（Production環境とStaging環境など）が存在する状況でのRootディレクトリの構成」が挙げられるかと思います。
これについて、
「Workspace機能は使うべきではない」
「Production環境とStaging環境は**ディレクトリで分ける**のが良さそう」
「でもProduction環境とStaging環境の**コード差異の吸収がめんどくさく、何ならちょこちょこ事故る**」
...みたいな記事が散見されておりました。

しかし、結局の所 **「ディレクトリを分けずに環境差分を表現し実行できれば一番」** だと思いませんか？
私はそうしてしまいたいです。
というわけで、なんとか実現できないかを検討してみました。

本記事では、 `-backend-config` と `-var-file` の2つのoptionを活用して、**単一のTerraform Rootディレクトリで複数環境を制御する方法**を示します。

## 前提条件
以下を前提条件とします（違う文脈の場合は適宜読み替えたり応用したりしてください）

- prd環境とstg環境の2環境を単一のTerraform Rootディレクトリで表現したい
- [Terragrunt](https://terragrunt.gruntwork.io) は使わない

# 結論

~~Terragrunt使うほうが楽な気がするけどまだ検証は終わっていない~~

素のTerraformだけで頑張る場合、ディレクトリ構成を以下のようにします: 
```
(repository)
├─ root # 本文で ${TERRAFORM_ROOT} を指す
│   ├─ envs
│   │   ├─ prd.tfbackend # backendの設定として渡す値
│   │   ├─ prd.tfvars # variableとして渡す値
│   │   ├─ stg.tfbackend
│   │   └─ stg.tfvars
│   ├─ state.tf
│   ├─ variables.tf
│   ├─ ...
:   :
```

Terraform Initでは以下のコマンドを叩きます:
```bash
cd ${TERRAFORM_ROOT}
terraform init -backend-config=envs/${ENV}.tfbackend
```

Terraform Plan/Applyでは以下のコマンドを叩きます:
```bash
# applyの場合
cd ${TERRAFORM_ROOT}
terraform apply -var-file=envs/${ENV}.tfvars
```

サンプルコードは、リンク先のGitRepositoryを参照してください。
（サンプルコードは動作可能なようにlocal backendを利用しています。 解説においては実戦を想定しs3 backendを利用しています）
[hkak03key/advent-calendar-sample-code//terraform-2022-root-dir](https://github.com/hkak03key/advent-calendar-sample-code/blob/main/terraform-2022-root-dir/)

# 解説
## 外部ファイルを利用した変数設定と、その制約
Terraformを利用したことがあれば、 **「実行時に外部から変数を注入したい」** という気持ちに駆られたことがあるかと思います。
これは、　`variable` を適宜定義し `terraform apply -var-file=${FILE_PATH}` で可能です。

この流れで **「外部から変数注入可能なら、backendについても変数で制御したい」** と思ったことがあると思います。
そして、こんなコードを書くわけです:

```hcl:${TERRAFORM_ROOT}/state.tf
terraform {
  backend "s3" {
    bucket         = "${var.account_name}-${var.env}-terraform-backend"
    key            = "terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "${var.account_name}-${var.env}-terraform-backend"
  }
}
```

このときのTerraform Initの実行結果はこちら:
```bash:bash
$ terraform init
Initializing the backend...
╷
│ Error: Variables not allowed
│
│   on state.tf line 3, in terraform:
│    3:     bucket         = "${var.account_name}-${var.env}-terraform-backend"
│
│ Variables may not be used here.
...
```

そして **「はーーーーーマジかーーーーーーーーーーー」** ってなるところまでお約束だと思っています。

このように、Backendの設定には変数を利用できないという悲しい現実があります。
そして、「Production環境とStaging環境はディレクトリで分けよう」といったノウハウに繋がっていきました。（私が携わった環境もそうなってるものが大半です）

## `-backend-config` optionの活用による、 backendに対しての外部ファイルを利用した変数設定
しかし、Production環境とStaging環境でディレクトリが分かれているのは、正直あまり嬉しくはありません。
そして、[ドキュメント](https://developer.hashicorp.com/terraform/language/settings/backends/configuration#partial-configuration)を読むと、 `-backend-config` なるoptionが存在します。
これを利用して、外部からbackendの設定を与えることが可能です。

利用方法を以下に示します:

### `state.tf` の定義
まず、 `${TERRAFORM_ROOT}/state.tf` にはbackendとして何を利用するかのみ記述します。
backendのパラメータは記述しないことに注意してください:
```hcl:${TERRAFORM_ROOT}/state.tf
terraform {
  backend "s3" {
    # 本section内に記述する設定は .tfbackend ファイルに記載
    # initは以下のコマンドで実行する必要がある: 
    # `terraform init -backend-config={{.tfbackend ファイルパス}}`
  }
}
```

### `${ENV}.tfbackend` の定義
次に、 `${TERRAFORM_ROOT}/envs/${ENV}.tfbackend` に、hcl形式でbackendのパラメータを記述します。
ここではprd環境を想定します: 

```hcl:${TERRAFORM_ROOT}/envs/prd.tfbackend
bucket         = "some-product-prd-terraform-backend"
key            = "terraform.tfstate"
region         = "ap-northeast-1"
dynamodb_table = "some-product-prd-terraform-backend"
```

### `terraform init` の実行
最後に、コマンド `terraform init -backend-config=envs/${ENV}.tfbackend` を実行します。

```bash:bash
cd ${TERRAFORM_ROOT}
terraform init -backend-config=envs/prd.tfbackend
```

コマンドが完了すれば成功です。

──

stg環境のbackendを設定したい場合は、 `${TERRAFORM_ROOT}/envs/stg.tfbackend` を準備し実行します。
このとき、既に異なる環境で `terraform init` されている場合は `-reconfigure` optionが必要になるので注意してください。

```bash:bash
cd ${TERRAFORM_ROOT}
# 既に terraform init した環境と異なる環境について terraform init する場合は -reconfigure optionを与える
terraform init -backend-config=envs/stg.tfbackend -reconfigure
```

# まとめ
単一のTerraform Rootディレクトリで、複数環境の `terraform init`　が可能であることを示しました。
そして、 `terraform init` 時に `-backend-config` optionを、 `terraform apply` 時に `-var-file` optionを利用することで、単一のTerraform Rootディレクトリで複数環境のDeployが可能です。

一方で、課題もいくつかあります:

* 設定ファイルが `.tfbackend` `.tfvars` の2種類がどうしても必要
  * どうしようもないけどダサい
* Route53のような、 **「保持する環境すべての中で唯一であるべきリソース」** を管理する場合は、単一のTerraform Rootディレクトリではかえって都合が悪い
  * こういうものについてはディレクトリを分けて対応するべき

そもそも、 [Terragrunt](https://terragrunt.gruntwork.io/) のようなツールも出ており（Advent Calenderでも取り上げられていました： [Terragrunt 導入で Terraform コードを DRY にしてみた](https://zenn.dev/simpleform/articles/20221111-01-terraform-with-terragrunt)）、まだ完全に検証は終わってないのですが恐らくいい感じにしてくれそうなので **「素のTerraformでここまで頑張るのか？」** というのは **「それは、そう」** という感じかもしれません。
しかし、「素のTerraformで頑張るとこんな感じになるよ」ということは心の片隅にあっても良いと思っており、Terraformのディレクトリ構成を決定する上での一つのヒントになれば幸いです。

──

僕らのTerraformのディレクトリ構成の旅はまだまだ続く。

# 宣伝
## その1: YouTube出ました
データ分析系のサービスの中でも特に激アツな [Snowflake](https://www.snowflake.com) のコミュニティである [Snowflake Japanユーザーグループ](https://usergroups.snowflake.com/japan/) のYouTubeに、DevOps回でお呼ばれしたので出演してきました！

DevOpsということでTerraformのお話も盛り上がってるので、是非ご覧ください！
https://www.youtube.com/watch?v=Hd4WWHmjjDw

## その2: お仕事のご相談など受け付けております
個人事業主を副業でやってるので、Terraformの活用やデータ分析に関する技術支援、その他何かご用命があればご相談ください！

https://lapras.com/public/hkak03key