[Fastly] Slack アプリのフロントに Fastly を入れてメンテナンスモード+セキュリティ強化

# TL;DR;

- Slack は自社インフラの IP アドレスを公開していないので、Slack アプリを作る場合にバックエンドで ACL による IP 制限ができない。Fastly を透過的に入れる。
- Slack アプリ (今回は slash command を例にとる) のバックエンドをメンテナンスしている時に、ユーザーにメンテナンスメッセージを表示させたい。

# ACL 使いたい

私のケースでは、バックエンドに GAE を使っていて、GAE 自体に Firewall 機能があるのですが、残念なことに Slack が自社インフラの IP アドレス情報を公開していないため、"Slack の IP アドレスだけからアクセスを許可" というのが出来ません。
このため、Fastly を GAE のフロントに透過的に導入します。Fastly は IP アドレス情報を公開しているため、この情報に基づいて ACL を設定することができます。

- [Fastly の IP アドレス範囲へのアクセス
](https://docs.fastly.com/ja/guides/accessing-fastlys-ip-ranges) 

## Fastly の設定

Slack アプリの場合は基本的にレスポンスをキャッシュさせたいケースはあまりないはずなので、今回は細かい事は何も考えず、ただ単に透過的に導入、全てのリクエストを PASS (キャッシュさせない) で構成します。

下記のような [VCL スニペット](https://docs.fastly.com/ja/vcl/vcl-snippets/using-regular-vcl-snippets/) を `vcl_recv` に作成します。

```c:vcl_recv

  # whitelist check
  if (table.lookup(whitelistedPath, req.url, "false") != "true") {
    error 403;
  }

  # force pass
  return(pass);
```

\* Shielding を利用する際には VCL スニペットではなく、[カスタム VCL](https://docs.fastly.com/ja/vcl/custom-vcl/creating-custom-vcl/) を使って `#FASTLY recv` マクロ行よりも下に `return(pass);` を追加してください。

### Edge Dictionary を使ってアクセス可能な URL を制限する

前述のコードで `# whitelist check` の処理の説明をしていませんでしたが、これは単純に予めバックエンドへのアクセスを許可する URL を edge dictionary に定義しておき、これにマッチした場合にのみリクエストを許可、それ以外は 403 を返すという処理です。

Edge dictionary は Fastly の管理画面 UI から簡単に作成・更新ができます (API からも操作可能)。

<img width="1110" alt="Screen Shot 2020-03-20 at 6.55.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19942/42f9e966-84db-4061-7f83-3e1ec532a798.png">

上記の例では、

- /foo/interactive
- /bar/slash

のような2つの URL のみを許可しています。これ以外の URL に対してのアクセスはバックエンドにリクエストが到達することなく、Fastly から 403 が即座に返されることになります。

# メンテナンスモードやりたい

例えば Slash command のバックエンドのアプリを一時的に落としてメンテナンスする必要がある場合や、バックエンドに何らかの障害が起きてしまった場合に備えて、そうした最中に slash command でアプリを呼び出したユーザーにはメンテナンスメッセージを表示させたいとします。

このケースでも先程同様に edge dictionary を活用します。
先程の VCL (`vcl_recv`) を下記のように変更します (`# check maintenance mode` を追加)。

```c:vcl_recv
  
  # whitelist check
  if (table.lookup(whitelistedPath, req.url, "false") != "true") {
    error 403;
  }

  # check maintenance mode
  if (table.lookup(maintenance, "isMaintenance", "false") == "true") {
    error 666;
  }

  # disable cache
  return(pass);

```

加えて、`vcl_error` にもコードを追加します。

```c:`vcl_error`
  if (obj.status == 666) {
    synthetic "{" LF
      {" "response_type": "ephemeral","} LF
      {" "text": "Sorry, this app is currently under maintenance. Please try again later.""} LF
      "}"
    ;
    set obj.status = 200;
    set obj.response = "OK";
    set obj.http.Content-Type = "application/json";
    return (deliver);
  }
```

そして、新しい edge dictionary `maintenance` を追加します。

<img width="1096" alt="Screen Shot 2020-03-20 at 7.06.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19942/16e70332-92dc-ad4a-f884-88f85471e5ef.png">

- `false` => メンテナンスモード OFF
- `true` => メンテナンスモード ON

上記の設定を行うことで、リクエストが来ると必ずこの edge dictionary の value を確認し、もし true になっていた場合にはバックエンドにリクエストを送らず、Fastly から即座にメンテナンスメッセージを含む JSON レスポンスを Slack に返します。カスタムレスポンス (`synthetic`) の詳しい使い方については [ドキュメント](https://docs.fastly.com/en/guides/custom-responses-that-dont-hit-origin-servers) を確認してみてください。

`true` の状態で slash command からアプリが呼び出されると、ユーザーは下記のようなメッセージを受け取ります。

<img width="542" alt="Screen Shot 2020-03-20 at 7.13.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19942/b2768fcd-eaad-0258-0b03-f2f50116412c.png">

## メンテナンスモード応用

上記の例では計画的なメンテナンスを想定していますが、例えばバックエンドサーバに何らかの障害が起きた場合に自動的にメンテナンスモードを有効にしたい、という場合には、[API](https://docs.fastly.com/en/guides/working-with-dictionary-items-using-the-api) を使って edge dictionary の値を外部から簡単に更新できるため、既存の監視システムに組み込む事は難しくないと思います。

また、もう1つの方法としては、Fastly からバックエンドに対しての [ヘルスチェック](https://docs.fastly.com/en/guides/working-with-health-checks) の設定を有効にすることで、自動的にバックエンドの障害を検知してメンテナンスページを表示させるということも可能です。
この場合の条件は下記のように少し変更が必要です。

```c
  # check maintenance mode
  if (table.lookup(maintenance, "isMaintenance", "false") == "true" || !req.backend.healthy) {
    error 666;
  }
```

[ヘルスチェック](https://docs.fastly.com/en/guides/working-with-health-checks) を有効にすると、`req.backend.healthy` 変数を使って上記のように

- メンテナンスモードが有効にされている
- Fastly -> バックエンドサーバ のヘルスチェックが失敗している

上記いずれかの場合にメンテナンスページを返す、という処理になります。