【Terraform】複数環境あるシステムでのおすすめディレクトリ(モジュール)構成

# 問題

以下のようなシステム(仮名: *system-foo*)をTerraformでリソース管理したい。

## 1. 複数の環境が存在

本番環境、ステージング環境、開発環境などがある。

## 2. 各環境の構成は完全には一致していない

特定の環境にのみ存在するリソースがある。

例:

* 本番環境にのみ分析用EMRクラスタがある
* ステージング環境のELBのみ社外からのアクセスを拒否

## 3. リソース構成を変更する場合、まずステージング環境で数日テストしてから本番環境へ導入

リソース構成の変更が安全かテストするため、すべての環境で共通のリソースを変更/追加/削除する場合はまずステージング環境で数日テストした後に本番環境へ反映する。

----

またその他以下の条件でterraform用リポジトリを運用する。

1. terraformコンフィギュレーションファイルはgithub[^3]で管理
1. すべての環境についてmasterブランチの状態が常にapply済み[^6]であるようにしたい
    * つまりmasterブランチのterraform設定と実際の各環境が一致している状態を維持したい
1. 一旦Terraform実行のCIは考慮しない

[^6]: planしても差分が出ない

[^3]: 後述の[Module Sources](https://www.terraform.io/docs/modules/sources.html)指定で特定のバージョンが指定できるhosting方式ならBitbucketでもMercurialでも実はなんでもいい。

以上を踏まえるとどのようなモジュール構成が最適か？

# 回答

## tl;dr

以下のようなモジュール構成にする。

```
system-foo
├── README.md
├── common            // 環境共通のリソースが書かれたChild module
│   ├── ecr.tf
│   ├── ecs.tf
│   ├── elb.tf
│   ├── iam.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── route53.tf
│   ├── s3.tf
│   ├── variables.tf
│   └── vpc.tf
├── production        // 本番環境用のRoot module。commonディレクトリをChild moduleとして参照
│   ├── main.tf
│   └── route53.tf
└── staging           // ステージング環境用のRoot module。commonディレクトリをChild moduleとして参照
    ├── main.tf
    └── waf.tf
```

また、commonモジュールへの参照の仕方は必要に応じて[ローカルパス指定](https://www.terraform.io/docs/modules/sources.html#local-paths)と[Github指定](https://www.terraform.io/docs/modules/sources.html#github)を切り替える。

```terraform:$GIT_ROOT/system-foo/production/main.tf
module "common" {
  source = "../common"                                                            // ローカルパス指定
  // ↕ どちらかをケースに合わせて使用する
  source = "git@github.com:bigwheel/system-foo.git?ref=v1.2.0//system-foo/common" // Github指定
  ...
}
```


## 長い説明

冒頭の「問題」のようなケースに対するTerraformのモジュール構成のパターンについては次の資料で列挙した。

[各環境とRoot Module(tfstate)・workspaceの対応付けパターンを比較してみた - Google スライド](https://docs.google.com/presentation/d/1N1GBk72OBgBtz5cYRmR22OqD3N8NIlZIgkGCSdRuPlg/edit?usp=sharing)

今回の回答はその中では②-Bのパターンに当たる[^1]。

### 問題の各項目に対する解決方針

#### 「1. 複数の環境が存在」の対策

以下のディレクトリ構造の通り(再掲)、各環境をそれぞれ1つのRoot moduleへ対応付ける。

```
system-foo
├── README.md
├── common            // 環境共通のリソースが書かれたChild module
│   ├── ecr.tf
│   ├── ecs.tf
│   ├── elb.tf
│   ├── iam.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── route53.tf
│   ├── s3.tf
│   ├── variables.tf
│   └── vpc.tf
├── production        // 本番環境用のRoot module。commonディレクトリをChild moduleとして参照
│   ├── main.tf
│   └── route53.tf
└── staging           // ステージング環境用のRoot module。commonディレクトリをChild moduleとして参照
    ├── main.tf
    └── waf.tf
```

#### 「2. 各環境の構成は完全には一致していない」の対策

環境ごとにRoot moduleが分かれているため、特定の環境固有のリソースも自由に追加できる(上記例の `production/route53.tf` や `staging/waf.tf`)。
共通(common)モジュール内のリソースへ値を渡したい場合や逆に値を参照したい場合はcommonモジュールの `variables.tf` と `outputs.tf` を適時編集する。

#### 「3. リソース構成を変更する場合、まずステージング環境で数日テストしてから本番環境へ導入」の対策

同一リポジトリ内のmoduleを参照するときによく使われるのは以下のような[ローカルパス指定](https://www.terraform.io/docs/modules/sources.html#local-paths)である。

```terraform:$GIT_ROOT/system-foo/production/main.tf
module "common" {
  source = "../common"
  ...
}
```

しかし、この方式だとChild moduleを修正してステージング環境でのみ先に構成を変えると本番環境のリソース定義が(child moduleが変更されたことで推移的に)変わっているにも関わらず未applyのまま残る。
これでは本番環境のリソースへ緊急でセキュリティパッチを当てたいなどのときapplyで非常に困ることになる[^5]。

そこで[Github指定](https://www.terraform.io/docs/modules/sources.html#github)と[サブディレクトリ指定](https://www.terraform.io/docs/modules/sources.html#modules-in-package-sub-directories)を使用する。

```terraform:$GIT_ROOT/system-foo/production/main.tf
module "common" {
  source = "git@github.com:bigwheel/system-foo.git?ref=v1.2.0//system-foo/common"
  ...
}
```

上記のように同じリポジトリ内のmoduleであってもあえてGithub指定を使用することで、過去の特定のコミット時点でのcommonモジュールの状態を参照することができる。
これでもしステージング環境のためにcommonモジュールでリソース構成を変更しても本番環境用のRoot module(system-foo/productionディレクトリ)内からのapplyで差分が出ない。ステージング環境での動作テストが終わり本番環境へ取り込んでも問題ないと判断した時点でrefパラメータの値を書き換えて更新を取り込む形になる[^9]。

[^9]: 備考: この際再度の `terraform init` が必要


[^5]: `-target` オプションでステージング環境でのみ追加したリソース以外を指定する、などの回避策があるが公式にこういった使い方は推奨されていないし単純に手間がかかりスケールしない


[^1]: 蛇足ながら、workspaceを環境切り替えに使用することは公式に非推奨となっている。詳細は次を参照: [Terraform Workspacesの基礎と使い方について考えてみた！ \#AdventCalendar ｜ Developers\.IO](https://dev.classmethod.jp/cloud/how-to-use-terraform-workspace/)

# 備考

* この構成を取ればmasterブランチの状態を常にapplyし続ければいいのでterraformをCIで扱うことが非常に楽になる
* Child Module, Root Moduleなどの用語は[Terraform Glossary \- Terraform by HashiCorp](https://www.terraform.io/docs/glossary.html)参照