Terraform の Backend(=http)について調べてみた

## TerraformのBackendにはいろいろある

[Terraform Backend Types](https://www.terraform.io/docs/backends/types/index.html) にあるように、TerraformのRemote Backendっていろいろあります。

- artifactory
- azurerm
- consul
- etcd
- etcdv3
- gcs
- http
- manta
- s3
- swift

とまぁこのくらい。
今回はこれについて調べてみました。

ちなみにデフォルトはLocal Backendになっていて、 `terraform.tfstate`というファイルに保存されます。

## http について調べてみる

実用的っぽいと思ったのはs3, swiftあたりだったのですが、あえていろいろ考えて `http` を調べてみました。

[Backend (http編)](https://www.terraform.io/docs/backends/types/http.html)

ここにかかれている `Example Usage`, `Example Referencing` を見たのですが、全然分かりませんね。

何を送って、何を返してくれればOKなんでしょう、httpの場合。さっぱりです。

ということで、まずは簡単なテストサーバを立てて、それを [Charles](https://www.charlesproxy.com/) でキャプって見るとわかるかなと思いまして、こういうことをやりました。

### まずtfファイルのbackend設定を変える

1. 自分が今までTerraformで使っていた `.tf` があった
2. で、それに `backend` を単に足してみた

そうするとこうなります。

```
provider "openstack" {
  auth_url          = "*****"
  user_name         = "*****"
  tenant_id         = "*****"
  tenant_name       = "*****"
  password          = "*****"
  user_domain_id    = "*****"
  project_domain_id = "*****"
}

terraform {
  backend "http" {
    address = "http://localhost.com:5000"
  }
}

resource "openstack_compute_keypair_v2" "my_kp_1" {
  name = "terraform-kp-1"
}

```

足したのは、

```
terraform {
  backend "http" {
    address = "http://localhost.com:5000"
  }
}
```

の部分です。

`localhost.com` というホスト名がbackend内に出てきますが、これはhostsで127.0.0.1に解決させています。

なんでlocalhostのままではダメなのか・・・というと、localhostのままだとCharlesでキャプチャしてくれないのですよね。

この解決策として、ブラウザからとかだと `localhsot.`(末尾に".")でキャプチャしてもらえたりしますが、Terraformだとダメなようです。

### 次にリクエスト先を何かしら作る

次に `localhost.com:5000` が何かしらHTTP的に着弾してくれないと困ります。

ということで、こんなFlaskアプリを作ってみました。

```python

from flask import make_response, Flask

app = Flask(__name__)


@app.route('/')
def hello():
    return make_response()


if __name__ == "__main__":
    app.run(debug=True)
```

これを立ち上げておきます。

```bash
% python /path/to/flaskapp.py
```
そうすると `localhost:5000` で待ち受けてくれるはずです。backendが出来ましたね。


### terraform initしてみる

さて、この状態で、 `terraform init` してみます。

```
% terraform init

Initializing the backend...

Initializing provider plugins...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.openstack: version = "~> 1.12"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```

おお、なんだか成功しましたね。 `terraform init`されたっぽいです。

### この時何が起きていたか

この時のリクエスト/レスポンスをCharlesで見てみます。

#### terraform init時のリクエスト

```
GET / HTTP/1.1
Host: localhost.com:5000
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip
```

#### terraform init時のレスポンス

```
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 0
Server: Werkzeug/0.14.1 Python/2.7.10
Date: Wed, 19 Dec 2018 08:49:27 GMT
Connection: close
```

なるほど。backendで指定したURLにGETが飛ぶわけですね。

`init`時だからだと思いますが、レスポンスは空でOKのようです。

（まぁ、レスポンスはこういうふうにしか返さないってだけなんですけどね、Flaskが）

### terraform applyしてみる

では次に `terraform apply` してみましょう。

```
% terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + openstack_compute_keypair_v2.my_kp_1
      id:          <computed>
      fingerprint: <computed>
      name:        "terraform-kp-1"
      private_key: <computed>
      public_key:  <computed>
      region:      <computed>


Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

openstack_compute_keypair_v2.my_kp_1: Creating...
  fingerprint: "" => "<computed>"
  name:        "" => "terraform-kp-1"
  private_key: "" => "<computed>"
  public_key:  "" => "<computed>"
  region:      "" => "<computed>"
openstack_compute_keypair_v2.my_kp_1: Creation complete after 1s (ID: terraform-kp-1)
Failed to save state: HTTP error: 405


Error: Failed to persist state to backend.

The error shown above has prevented Terraform from writing the updated state
to the configured backend. To allow for recovery, the state has been written
to the file "errored.tfstate" in the current working directory.

Running "terraform apply" again at this point will create a forked state,
making it harder to recover.

To retry writing this state, use the following command:
    terraform state push errored.tfstate
```

エラーになりました。まぁそれはそうですよね。

```
Failed to save state: HTTP error: 405
Error: Failed to persist state to backend.
```
POST分作ってませんからね・・・。

さて、これをキャプチャしたCharles側で見てみます。

#### terraform apply時のリクエスト

```
POST / HTTP/1.1
Host: localhost.com:5000
User-Agent: Go-http-client/1.1
Content-Length: 3281
Content-Md5: BzoOkMeCZ1DPgz266a8Ckg==
Content-Type: application/json
Accept-Encoding: gzip

{
    "version": 3,
    "terraform_version": "0.11.10",
    "serial": 1,
    "lineage": "0159803e-4f38-ddaf-d403-4118c48cff6a",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "openstack_compute_keypair_v2.my_kp_1": {
                    "type": "openstack_compute_keypair_v2",
                    "depends_on": [],
                    "primary": {
                        "id": "terraform-kp-1",
                        "attributes": {
                            "fingerprint": "4d:6a:92:2c:f2:44:db:84:12:76:5d:e9:2d:d9:c7:06",
                            "id": "terraform-kp-1",
                            "name": "terraform-kp-1",
                            "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAA(略)-----END RSA PRIVATE KEY-----\n",
                            "public_key": "ssh-rsa AAAAB3NzaC(略) Generated-by-Nova\n",
                            "region": ""
                        },
                        "meta": {},
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": "provider.openstack"
                }
            },
            "depends_on": []
        }
    ]
}
```

レスポンスは当然ながら405エラー（Method Not Allowed)なのでここには載せません。

つまるところ、

- `terraform init`すると、backendに対してはGETが飛ぶ。この時レスポンスBODYは空でOKである模様
- `terraform apply`した場合に、apply後のterraform.state相当のJSONがリクエストBODYとしてPOSTされる

という挙動であることがわかります。


## http Backendの宛先はこんな挙動で良いのでは？

ということは、http Backend向けには、おそらくこういうWebアプリケーションを作ればいいということですね。

- POSTが飛んできたら、リクエストBODYをJSON化してストアしておく。これは常に上書く。
- GETが飛んできたら、ストアしたJSONをレスポンスBODYに入れて返す。もしストアしたデータがない場合は空レスポンスでOK
- あとロック/アンロックとかもあるようなので、これはどこかでステートごとにフラグでも持たせておいて、フラグに応じてレスポンスコードを変える

つまるところ、Backendのページに書かれていた・・・

```
Stores the state using a simple REST client.

State will be fetched via GET, updated via POST, and purged with DELETE. The method used for updating is configurable.
```

・・・という情報が、

- REST
- GET/POST/DELETE

`って書いてんだから行間読め` と言っているのでしょう。まぁ、確かにそうなるわな。

しかしながら、実際にはユーザを識別するための何かしらとか、認証とか考えないといけないのでしょうけど（Provider自体の認証とは別にという意味です）、まずは大体の挙動はわかったので本記事はここまでにします。


そのうち、実際にこうすればいいんじゃないかという案を作って書いてみます。