[Fastly] 登録されている各backendのhealth状態(req.backend.healthy)を監視する方法

### 追記
より汎用性が高い方法はこちらです(version 2):

- [[Fastly] backendのhealth状態(req.backend.healthy)を監視する方法 (version 2, 全POPでのhealth状態取得)](https://qiita.com/jrsyo/items/34ece9cdf0832044950f#_reference-6ca85e8ec4e16131214a)

### 背景
サービスに登録されている各backend（origin）がダウンした場合、Fastlyでは設定に応じて様々な切り替えが可能です。

例)
 - backendが`503`を返した場合、[カスタムレスポンス](https://docs.fastly.com/guides/basic-configuration/creating-error-pages-with-custom-responses) を使ってFastlyからエラーページを返す
 - backendに [health check設定を追加](https://qiita.com/Satoshi-Ishii/items/3c57431a79053fe31513#%E3%83%98%E3%83%AB%E3%82%B9%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%AE%E8%A8%AD%E5%AE%9A) して、originがダウンした場合に副系のbackendに切り替える
 - [stale-if-error](https://docs.fastly.com/guides/performance-tuning/serving-stale-content) を使ってキャッシュサーバに残っているコンテンツを返す

backendにhealth check設定が追加されている場合、`req.backend.healthy` をVCLの中で使うことができるようになり、この変数はbackendがhealthyな場合は `1`, unhealthyな場合は`0`を返します。
しかし現在のところ、各backendのhealth状態を外部から取得できるようなAPIは用意されていません。今回はFastly上でVCLを書く事で、各originの `req.backend.healthy` の値をJSON形式で返す擬似APIを作り、外部からそれらを監視できるようにしてみたいと思います。

## Step1. 各backendにhealth check設定を追加する
`req.backend.healthy` を使うにはそのbackendにhealth checkが設定されていることが前提条件になります。
詳細な方法についてはここでは割愛しますが、下記のドキュメントもしくはQiita記事が参考になります。

- [Health checks tutorial](https://docs.fastly.com/guides/basic-configuration/health-checks-tutorial)
- [ヘルスチェックの設定](https://qiita.com/Satoshi-Ishii/items/3c57431a79053fe31513#%E3%83%98%E3%83%AB%E3%82%B9%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%AE%E8%A8%AD%E5%AE%9A) by @Satoshi-Ishii 

## Step2. カスタムVCL or VCLスニペット機能を使って `vcl_recv` サブルーチン内にコードを追加する

```
# 今回は例としてヘルスチェックが設定されたbackendが2つ定義されている前提
backend ${backend_name_1} { ... }
backend ${backend_name_2} { ... }
 
 
sub vcl_recv {
  if (req.url ~ "/api/status") {
    # URLのクエリパラメータを subfield関数を使って簡易分解し、
    # "/api/status?backend=${backend_name}" の ${backend_name} 部分を切り出す。
    # 参考: https://community.fastly.com/t/accessing-query-string-parameters/1089/3
    set req.http.x-api-parameter = subfield(req.url.qs, "backend", "&");
 
    # パラメータの値に応じて一時的に req.backend にそのbackendを代入し、
　　　　　　　　# JSONのカスタムレスポンスを返すためvcl_error へ移動
    if (req.http.x-api-parameter == "${backend_name_1}") {
      set req.backend = ${backend_name_1};
      error 610;
    } else if (req.http.x-api-parameter == "${backend_name_2}"){
      set req.backend = ${backend_name_2};
      error 610;
    }

    # 無効なパラメータには 403 を返す
    error 403 "Forbidden";
  }
 
 
 ...
}
```

上記の例は `/api/status?backend=${backend_name}` をURLとして想定していますが、当然このURLがサービスの本当のAPIと競合することもあるかと思います。その場合は必要に応じてサービスと競合しないURLに調整してください。

また、上記の方法では例えばbackendが50個あれば50個分の条件分岐が必要になり冗長です。
その場合、Fastlyの [Edge Dictionaries](https://docs.fastly.com/guides/edge-dictionaries/) を使って各backendのリストを持つtableを作成し、この辞書に対して `table.lookup` を行うことで冗長な条件分岐を避けることが出来ます。

URLにアクセス可能なSource IPを制限したい場合は、Fastlyの [ACL機能](https://docs.fastly.com/guides/access-control-lists/) を使って、if条件にANDでACLチェックを加えることで実現可能です。
`if (req.url ~ "/api/status" && client.ip ~ acl_name) { ... }`

## Step3. カスタムVCL or VCLスニペット機能を使って `vcl_error` サブルーチン内にコードを追加する
FastlyのカスタムレスポンスはWebコンソールからGUIで作成することもできますが、下記のように `synthetic` を定義することでも作成可能です。
`vcl_error` 内で処理を実行していますが、今回はJSONレスポンスを返す事が目的なので `200 OK` を返すように設定します。

```
if (obj.status == 610) {
  synthetic "{" LF
      {"  "timestamp": ""} now {"","} LF
      {"  "req.backend": ""} req.backend {"","} LF
      {"  "req.backend.healthy": "} req.backend.healthy LF
      "}"
  ;
  set obj.status = 200;
  set obj.response = "OK";
  set obj.http.Content-Type = "application/json";
  return (deliver);
}
```

## Step4. 実際にリクエストを投げてみる
Step1 ~ 3が完了したら準備完了です。実際にリクエストを投げて期待するJSONレスポンスが受け取れるか確認してみます。

```shell-session
$ curl http(s)://${hostname}/api/status?backend=${backend_name}
 
# 下記のようなJSONレスポンスが期待されます　(req.backend.healthy 1:healthy, 0:unhealthy)
{
  "timestamp": "Thu, 23 Nov 2017 10:47:06 GMT",
  "req.backend": "${service_id}--${backend_name}",
  "req.backend.healthy": 1
}
```

数値型で 1 or 0 が返されると思います。これを任意のツールから呼び出すことで外部からの状態監視が実現できます。

## その他
- 今回は `vcl_recv` -> `vcl_error` というフローでレスポンスを返しているので、レスポンスは一切キャッシュされません。ページをリロードする毎に timestamp が更新されていることが分かると思います。
- トラブル時の調査用データとしても、ProductionサービスではFastlyの [Real-Time Log Streaming](https://docs.fastly.com/ja/guides/streaming-logs/setting-up-remote-log-streaming) を設定しておくことを推奨します。任意の変数の値をログフォーマットに加えて出力が可能です。（今回使った `req.backend.healthy` や任意のヘッダの値など）
- `req.backend.healthy` の値が更新されるタイミングは、ヘルスチェックの `frequency(頻度)` 設定に依存します。`frequency` の設定値によってbackendがunhealthyとみなされるまでにかかる時間が変わります。今回作成した疑似APIにアクセスした際に毎回backendへヘルスチェックリクエストを投げているわけではないのでご注意ください。  
[Health check frequency](https://docs.fastly.com/guides/basic-configuration/health-check-frequency)