terraform のディレクトリとワークスペースとステートについて

# はじめに

terraform-jp のワールドカフェでもたびたび話題になるディレクトリとワークスペースとステートについての話をします。
2019年もそろそろ終わりなのでカレンダーとは関係なくまとめておこうかなって。
って書き終わってから確認したらカレンダーに穴空いてる。。。ので、[terraform Advent Calendar 2019](https://qiita.com/advent-calendar/2019/terraform)の12日目扱い（もう14日も終わるけど）で投稿します。

# 結論

- １サービス１リポジトリでマルチディレクトリ、デフォルトワークスペース戦略が最も使い勝手がよい


# 前提

- クラウドはAWS前提だけど、多少の誤差はあってもどのクラウドでも同じ
- 一般的なWeb系インフラやSRE向けに書いた。特殊な環境やサービスについては考慮しない


# 戦略の分類

戦略は大きくわけて４つ

| # | ディレクトリ | ワークスペース | ステートの数 | 備考 |
|:---:|-----|-----|-----|-----|
|a| １ディレクトリ|デフォルトワークスペース|1||
|b| １ディレクトリ|マルチ(y)ワークスペース|y||
|c| マルチ(x)ディレクトリ|デフォルトワークスペース|x|今回のおすすめ|
|d| マルチ(x)ディレクトリ|マルチ(y)ワークスペース|(xy)||


要はディレクトリを分けるか、ワークスペースを使うか、の２軸

モジュール（外部にしても内部にしても）を使うのは当然なので議論しない。

ステートの数は、中に何個のリソースが含まれているか、を考える目安に。体感的に200を超えるリソースが入っているステートは重い（[parallelism](https://www.terraform.io/docs/commands/plan.html#parallelism-n)で調整できるけど）。



# 各戦略の概要とメリデメ

まず、ワークスペースのデメリットは、

- 初学者の学習コストと操作ミスへの対応負担
- 操作が煩雑（現在のワークスペースの確認や変更が必要になる。操作ミスを誘発しやすいといえる）
- 変数設定部分が読みにくい（mapが多用される。多段mapを読み解くのは手間）
- 部分的な変更に弱い（一部のワークスペースだけ別の形にしたいなどの要件に弱い）


の４点であり、これだけで一般的なWeb系インフラ構築には向いていない。
というか、今後ワークスペースの是非については議論の余地なく破棄でよい。異論は認める。


## a. １ディレクトリ－デフォルトワークスペース

１つのディレクトリに全てのコードを入れて管理する。サンプルやテスト、単一のHerokuアプリの管理など、特定の要件下で有効。オレオレモジュールを作る時はこの状態。

### メリット

- 簡潔
- 管理対象がそれほど多くなければ必要十分
- バージョンアップが楽（対象が全て１つにまとまっているので）

### デメリット

- 規模が多くなると修正箇所の発掘や処理に時間がかかる
- plan爆発（force newなどで関連リソースが連鎖して差分になる）が発生するととても見にくい
- 上司「あれとこれとあとそれも追加しておいて」「AMIを新しくしたから差し替えておいて」

## b. １ディレクトリ－マルチ(y)ワークスペース

開発環境や本番環境などの実行環境別やサービス別など、ワークスペースを駆使して管理する。管理対象の分類が名称の差異など小さいものに限られる場合に有効。例えばチームメンバー別ワークスペースとし、メンバーの数だけ同じものを作る、という作業が効率化される。

### メリット

- 差異が名称（リソースの命名）やインスタンスの台数程度の差しかない場合（＝ほぼ同じものを複数作る場合）にコード管理が容易
- 特定の機能要件を複数管理する場合に向いている。モジュール化されているものをたくさん同時に管理するなど。具体的には aws-vpc モジュールの部分だけを複数同時に管理するなど。

### デメリット

前述のとおり。
上司「コスト下げたいから開発環境はRDSとElastiCacheじゃなくてEC2使って」「ステージングでも本番DBにつなぎたいんですけど。むしろステージングDB要らないんですけど。あ、でもいつか戻すかもなー」


## c. マルチ(x)ディレクトリ－デフォルトワークスペース

環境や機能でディレクトリを分割し、ワークスペースを使用せずに管理する。ディレクトリ分割の正否で使い勝手やコード管理の煩雑さが変わる。１サービス１リポジトリで管理する場合にすっきりした見た目になる。

### メリット

- １つのディレクトリ内は簡潔（になるようにディレクトリを分割する）
- 差異の大きい同機能があっても分割してあれば変更が容易（他に影響を与えにくい）
- 全体的な見通しの良さ（ディレクトリの分割内容次第）

### デメリット

- ディレクトリの数だけバージョンアップ時の作業が手間
- 常に全てが差分のない状態であるか、を確認する作業が手間
- 上司「大至急手作業で本番環境変更して。他の環境にも後で反映しておいて」「なんか開発環境とステージング環境で動きが違うんだけど？」


## d. マルチ(x)ディレクトリ－マルチ(y)ワークスペース

環境でディレクトリを分割し、サービスをワークスペースでわけるなど、同一のサービス（環境や機能）をたくさん管理する場合に使える。具体的にはユーザ別に環境を必要とするSaaSサービスのインフラなど。

2019/12/18 追記ここから

@minamijoyo の[この発言](https://twitter.com/minamijoyo/status/1207111034965618688)から察するに、workspace を apply 前のテスト環境として扱う、はこのパターンに合致すると思われる。
具体的には適用範囲をディレクトリ毎に分割して細分化しつつ、各ディレクトリのワークスペースをテスト用と実環境用に分ける。
分けたワークスペースはそれぞれ別のアカウントにすることで、s3 bucket名など全世界で一意じゃないとダメなリソース以外はうまいことコントロールできる。
こうすれば、CIでも、テストワークスペースで apply して問題ないことを確認してから destroy して、実環境ワークスペースで apply することで安全性が高まる（applyしないと成否が確認できない系リソースにも安心して適用できる）
数の多寡の問題になるので、コスト面と要ご相談だが、ワークスペースを使う正しい意義があったことにようやく気がついた。
まぁコストと運用面と管理面で個人的には採用したくないお気持ちなのは変わらないけれどもｗ

2019/12/18 追記ここまで


### メリット

- （複雑怪奇な見通しになるが）全てのコードを１リポジトリに押し込めることができる
- （操作ミスがないなら）同じものを運用維持管理し続けるケースで楽ができる

### デメリット

前述のワークスペースのデメリットに加えて、マルチディレクトリのデメリットも追加される。正直この選択はマゾい。
上司「Ａ社の分のＢ機能のところ”だけ”機能変更したいんだけど」


# マルチディレクトリ－デフォルトワークスペースのリポジトリ内構成

つまるところ、SREやインフラエンジニアが管理するべき単位として、１サービス１リポジトリとして、次のような構造にしてあると楽かなと。
ansible ディレクトリについては[こちら](https://qiita.com/raki/items/06bcf762c984f052040e)。
terraform ディレクトリは、

- base ディレクトリのような標準ディレクトリ構成を決めてしまう
- dev(sandbox)環境、stg,prod など環境別にコピーして再利用することで環境毎の特殊な差異に対応しやすくする
- 各ディレクトリ内では、モジュールを利用して極力リソースを置かないようにすることで、DRYになるように工夫する
- AWSの場合、環境別にリソースを分けないほうがいいものも多数あるので common ディレクトリでそれらを管理する

その他おまけで、

- tfenv と direnv を利用することで、複数サービスの面倒を見ないといけない場合にも対応しやすくする
- GitHub Actions で [PR 時に自動で plan を走らせる](https://qiita.com/raki/items/7464887fb747216367e4)
- awspec は terraform で管理できていればもういいかなというお気持ち
- base と common の分割粒度は、正直各社ごとに違うべきなのかなって。state mv が使えればいつでもどうとでもなるので、試行錯誤してもいい気はする。


```
$ cd my-service-infra
$ tree
|-- README.md
|-- ansible
`-- terraform
    |-- base
    |   |-- 100_projects
    |   |-- 200_vpc
    |   |-- 300_db
    |   |-- 400_cache
    |   `-- 500_compute
    |-- common
    |   |-- 000_common
    |   `-- 101_compliance
    `-- modules
```