Terraform remote state/workspaceなどを使って共同作業を楽にできれば


今時点ではないのですが、仮に別な方とTerraformで共同作業したいと思った場合にどうしたらいいのだろうとずっと考えていたので、その考えをまとめてみます。
実際にそこまで深く実践をしていないので、 `こういうケースだとそれだとだめだぜ` ってのがあると思うのでそうした部分はぜひご指摘を・・・ :sweat_smile: 

やるべきことは大きく以下なのかなと思っています。

1. コンフィグ( `.tf` ファイル群)を共有する
2. stateをremote化する
3. 複数環境をコントロールする(prod/stage)

## 1. コンフィグ( `.tf` ファイル群)を共有する

これをしておかないと、stateがremoteに存在したとしても、以下のようなことが起きてしまうので共同作業が出来ません。

- `リソース1`, `リソース2` が書かれているtfファイルをAさんとBさんが共有
- Aさんが、tfファイルに `リソース3` を足して `terraform apply`
- remote stateには、 `リソース1〜3` が記録されている状態
- Bさんが、`terraform apply` 。この時のリソースファイルには `リソース1` , `リソース2` しか書かれていないので、`リソース3` がDestroy

### gitで共有でどうでしょ

tfファイル群をgit管理しておくのでよいのかなと。

作業前は必ずgit pull, 作業後は必ずgit push。
ラップしてスクリプト化しておけば良いのかなと。

というか、その上でローカルではplanまでの動作確認にとどめておき、実際のapplyはCIからしかやらないようにしておけば良いのかなと思います。

なお、`.terraform` 配下を同管理すべきかは文末にて考察してみました。

## 2. stateをremote化する

今回はConsulでまずはお試し環境を作ってみました。

### Consulのインストール

s3に払うお金が個人的にはないのでconsuleを使います。
consul自体のインストールは、


- [公式](https://www.consul.io/) からバイナリを持ってきてパスを通す
- `consul agent -dev -ui` くらいでagent起動しておく

だけでOK。


### terraform.tfの作成

`terraform.tf` 等として、Terraform自体の定義ブロックを作り、その中にbackendの設定をします。
実際にはもっと詳細な設定が必要なのだと思うのですが、今回はとりあえず動けばいいレベル。

terraform.tf

```go
terraform {
  backend "consul" {
    path = "terraform/tfstate"
    lock = true
  }
}
```

### backend変更の反映

backendの変更には `terraform init` が必要です。

initしてapplyしてみてください。

この状態で、 `consul kv get terraform/tfstate` とタイプし、stateのJSONが表示されれば問題なしです。
なお、このコマンドは、consulのkey/valueストアのデータを取得するというコマンドです。
詳しくは [こちら](https://learn.hashicorp.com/consul/getting-started/kv)。

```bash
% consul kv get terraform/tfstate
{
  "version": 4,
  "terraform_version": "0.12.0",
  "serial": 1,
  "lineage": "56e7253a-8ff5-a369-c966-45a6f37e2fb2",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "ecl_compute_instance_v2",
      "name": "instance_1",
      "provider": "provider.ecl",
      "instances": [
        {
        ...
```

#### `lock = true` の効果

誰かが `terraform apply` をしている間に、別な `同remote stateを参照している人` がapplyなどを実行しようとすると以下のようなエラーになります。

```
% terraform apply
Acquiring state lock. This may take a few moments...

Error: Error locking state: Error acquiring the state lock: Lock Info:
  ID:        [uuid-1]
  Path:      terraform/tfstate
  Operation: OperationTypeApply
  Who:       yuzu@[PC名]
  Version:   0.12.0
  Created:   2019-06-04 00:14:47.01916 +0000 UTC
  Info:      consul session: [uuid-2]


Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.
```

lockが可能なケースではonにしておくほうが良いでしょう。多分。

#### ちなみに・・・

今回の構成は暫定的な環境でしかなく、agent止めちゃうとstateが揮発しちゃうので注意です。
一度stateを通常の状態に戻して、`terraform init` しておくといいかも。（stateがファイルに戻る）
（永続化みたいなやり方あるんだとは思うのですが、consul自体の勉強が足りてないんで今はこれで。
　というかs3使う人のほうが多いでしょうし）

##### Consulでのstate管理をやめる

- 後述の `terraform.tf` のstate定義をコメントアウト
- `terraform init` を実行
- stateが一旦 `terraform.tfstate` に戻る

##### Consulでのstate管理を再開

- 後述の `terraform.tf` のstate定義をコメント解除
- `terraform init` を実行
- stateがconsulに戻る

## 3. 複数環境をコントロールする(prod/stage)

workspaceを使って切り替えるのが良いのか、ディレクトリで分けるのが良いのか。
直感的なのは後者かなと思うのと、世の中を拝見していると同じく後者派が多いような気がしています。

ということで本記事ではあえてworkspace（前者）で行きます。

・・・workspaceって、公式のドキュメント上、専用の章が無いように見えますよね。
　だから使われないんじゃ・・・。 :sweat: 

### `workspace` とは？

workspaceとは、いわゆる仮想環境みたいなもんですね。
それごとにstateが管理されます。
HashiCorp調べだったか忘れましたが、あまり使われてない機能だ・・・と書かれていたのを見たことがあります。なんでなのでしょう。

workspaceは、`terraform workspace new` で作成し、 `terraform workspace select` で使用します。
（newした場合は自動的にselectもされます）

なお、何もしなければ `default` というworkspaceで作業をしているので、みなさんもworkspaceは少なくとも使ってはいます。

また、現在利用中のworkspace名は、 `.terraform/environment` というところに格納されています。
ちょっと見てみます。

```bash
% cat .terraform/environment
default%
% terraform workspace new ws1
Created and switched to workspace "ws1"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
% cat .terraform/environment 
ws1%
```

### workspaceが変わると何が美味しいのか

stateが個別管理できます。

例えば、

- 現在resourceとして `server1` だけがstage(検証用)環境でもprod(商用用)環境でも動作しているとする
- これに `server2` を足したい
- ということで、 `main.tf` に `server2` を足す（`server1` はすでに記載があってstageで動いているので）
- とはいえいきなりprod側に実際に変更を適用する(`terraform apply`)のは怖い
- そこで workspace = stage(検証用) でapplyして環境がうまく動くかを確認する
- うまく行ったのでworkspace = prod(商用用) に切り替える
- ここで `terraform apply` して、初めて workspace = prod(商用用)にも `server2` が足された

というような流れを作れます。

### workspaceが増えたり減ったりしたのを別な作業者にどうやって伝えたら良いのさ？

remote stateにしているので勝手に伝わります。
何故か・・・というと、workspaceを作った時点で、remote側のstateが増えているからです。
実際誰か一人が `terraform workspace new` した時点で、別な作業者が `terraform workspace list` すると一覧に出てくるはずです。
最初ビビりました。 :astonished: 

### その時のstateはどう管理されているのか

`workspaceごとに管理されるstate` の実体はどこにあるかというと、consulのkey/value的に、

`[terraform.tfで指定したパス]-env:ws1`

というパスで格納されています。（s3とかだとまた違うパスなのだろうと思います）
今回は、terraformブロックにて `terraform/tfstate` というパスを指定しているので、 `ws1` というworkspaceのためのstateは、

`terraform/tfstate-env:ws1`

というパスで格納されています。
実際見てみるとこうなってます。

```bash
% consul kv get terraform/tfstate-env:ws1
{
  "version": 4,
  "terraform_version": "0.12.0",
  "serial": 0,
  "lineage": "fc9b977b-c5f5-9b8f-a1df-349e09d9a3f8",
  "outputs": {},
  "resources": []
}
```

別なworkspaceとのstate干渉がないので、独立したライフサイクルで環境を運用できるようになっているわけです。

この `workspaceを切り替えながら環境ごとに構築を進めていく` というやり方で、複数環境を同時にコントロールしていくことが可能です。


ということで一通りの内容について書いたので、ここから余談です。

## git管理対象の考察: `.terraform` 配下について

`.gitignore` で `.terraform` は無視する形にしましたが、ここについてはどうするかいろいろ検討が必要かもなと思いました。

`.terraform` には以下のものが入っています。（他にもあるんでしょうけど今自分は知らない :sweat_drops:)

1. `environment` 
2. `terraform.tfstate`
3. プラグインのバイナリ

これらをどう扱うかはチームごとに検討すれば良いのかなと思いました。
ま、プラグインをコミットする(上記の3）ってのはないですよね。ということで上記1, 2について書きます。

### `.terraform/environment` とは

おさらいですが、現在利用中のworkspace名が格納されるファイルです。

ということで、 `.terraform/environment` も一緒にgit管理すると、最新の `作業中のworkspace名` も一緒に管理されることになります。
例えば、 `stage` , `prod` という2つのworkspaceがあって、前者がステージング、後者が商用だとします。
そうした場合に、

- 基本的に `stage` という名前のworkspace以外での作業はしない
- `terraform workspace select prod` して、 `terraform apply` したあと、そのまま `workspace = prod` な状態を保持するのは危険。うっかり別作業しちゃったら怖い

みたいなことを考えたい場合、 `.terraform/environment` もgit管理対象にしておけば、pullしたときにworkspaceを強制的にそっちに戻す・・・などという運用も可能です。 


### `terraform.tfstate` とは

backend等、terraform自体への設定が格納されているようです。

`backend` 以外の項目も格納されているので、本来そのためのファイルというわけではなさそう。
とはいえ、例えば `required_version` とかだけ指定しても、当該ファイルって作成されないんですよね・・・。

まぁ、それはおいておいて、以下のようなファイルになります。

```bash
% cat .terraform/terraform.tfstate
{
    "version": 3,
    "serial": 1,
    "lineage": "[uuid]",
    "backend": {
        "type": "consul",
        "config": {
            "access_token": "",
            "address": "",
            "ca_file": "",
            "cert_file": "",
            "datacenter": "",
            "gzip": false,
            "http_auth": "",
            "key_file": "",
            "lock": true,
            "path": "terraform/tfstate",
            "scheme": ""
        },
        "hash": 3609260479
    },
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {},
            "depends_on": []
        }
    ]
```

どのみち対象リソースが増えた際にplugin追加することもあるでしょう。
その際、どうせ `terraform init` するでしょう。と考えると管理する意味はあまりないのかなと思ったりしますが、管理しても問題自体はなさそうな気もします。


## ちょっと話がずれますが `terraform refresh` の挙動について

`terraform refresh` にて、最新のリソース状態をstateに反映し直すことが出来ます。
stateをremote管理してても特に挙動に変わりはありません。

refreshの挙動は至ってシンプルで、stateに存在するリソース群をreadメソッドを利用して更新するだけだと思われます。
完全にstateのみの世界で完結しており、そのリソースがtfファイル上存在するかどうかは動作に関係がないようです。
`tfファイルに存在するリソース` には当然リアルなIDとかの記載がないのだから当たり前ですね。

実際以下のような動きをします。（検証と呼べない類）

- tfファイルに存在し、stateに存在するもの -> refreshされる
- tfファイルに存在し、stateに存在しないもの -> refreshされない（というか、refreshしようがないだけ）
- tfファイルに存在せず、stateに存在するもの -> refreshされる
- tfファイルに存在せず、stateに存在しないもの -> refreshされない（というか、refreshしようがないだけ）

また、リソースをTerraformを通さずに更新している場合、 `terraform refresh` 後、tfファイルを一切触らずに `terraform apply` してしまうと、その変更を打ち消しに行ってしまいます。なのでそもそも止めましょう。

例えば、

- OpenStackのInstanceをTerraform経由でflavor=m1.tinyで作成
- 作成したInstanceをTerraformを通さずにflavor=m1.smallに変更
- `terraform refresh` を実行 -> stateにおける当該インスタンスのflavor情報がm1.smallに書き換わる
- 再度 `terraform apply` を実行
    - state上はm1.small
    - tfファイル上はm1.tinyのまま
    - なので、Terraformは m1.small -> m1.tiny のresizeを行おうとする

という状況が発生します。これも当然といえば当然ですね。

## まとめ

ということで、共同作業に向けてこんな感じでどうかなぁというのをまとめてみました、
実際にはこれは基本でしかないと思っていて、それをシェルでラップする等して運用していく形になるだろうなぁと思っていますが、ノウハウある方ぜひ教えてください。

以上です。