IBM Cloud CIS(Cloud Internet Services)のフォールバック・プール、いわゆるSorryサーバーをCode Engineで立ててみる

2022年10月13日追記：Code Engine内部のIstioの動きに変更があったようで、コンテナが5xxエラーを返した場合に、自動的に30回ほどリトライするようになりました。（また変わるかもしれません）
これにより、ICOSに対するリトライが無駄に大量に発生し、トータルのレスポンスも悪化するようになってしまいました。将来的にリトライ回数が制御できるようになることを祈ります。。。

## はじめに

IBM CloudのCDNサービスであるCIS(Cloud Internet Services)を経由してサービスを提供する際、そのオリジンに指定したサーバーやその間のネットワークなどに問題がある際に、いわゆるSorryページを表示させるためのフォールバック・プールを設定することが出来ます。

このフォールバック・プールのオリジンに設定する、いわゆるSorryサーバーは、（多分一般的に）どのようなURLがリクエストされても、レスポンスコード503で静的なHTMLを返す、という要件があります。

これをCode Engineで実現してみます。

## 前提

+ 作業環境
    + docker導入済み
    + ibmcloud CLI導入済み
        + https://cloud.ibm.com/docs/cli?topic=cli-install-ibmcloud-cli
    + Container Registory、Code Engineプラグイン導入済み
        + https://cloud.ibm.com/docs/cli?topic=cli-plug-ins
+ Container Registoryに名前空間(jp-tokリージョン)を作成済み
+ Code Engineにプロジェクト(jp-tokリージョン)を作成済み

## 素材の準備

IBM CloudがCode Engineのサンプルアプリとして提供している以下をカスタマイズする形で作成してみます。

https://github.com/IBM/CodeEngine/tree/main/hello

ローカルに素材をクローンします。

```
$ git clone https://github.com/IBM/CodeEngine
Cloning into 'CodeEngine'...
remote: Enumerating objects: 1942, done.
remote: Counting objects: 100% (311/311), done.
remote: Compressing objects: 100% (91/91), done.
remote: Total 1942 (delta 235), reused 262 (delta 214), pack-reused 1631
Receiving objects: 100% (1942/1942), 2.47 MiB | 643.00 KiB/s, done.
Resolving deltas: 100% (1307/1307), done.
```

CodeEngine/helloにある素材を使用します。

```
$ cd CodeEngine/hello
$ ls -l
合計 20
-rw-r--r--. 1 root root 111  9月 13 16:52 Dockerfile
-rw-r--r--. 1 root root 417  9月 13 16:52 README.md
-rwxr-xr-x. 1 root root 593  9月 13 16:52 build
-rwxr-xr-x. 1 root root 671  9月 13 16:52 run
-rw-r--r--. 1 root root 370  9月 13 16:52 server.js
```

## まずはサンプルをビルド

ibmcloud CLIでログインします。

```
$ ibmcloud login --sso -r jp-tok -g [使用するリソースグループ名]
```

Container Registoryにもログインします。

```
$ ibmcloud cr login
```

あらかじめ作成しておいたContainer Registoryの名前空間を環境変数に指定します。

```
$ export REGISTRY=jp.icr.io/[名前空間名]
```

ビルドします。

```
$ ./build
```

すると、`jp.icr.io/[名前空間名]/hello:latest`というイメージがプッシュされます。

## サンプルアプリの実行

まず、事前に作成したCode Engineプロジェクトを現行コンテキストとして選択します。

```
$ ibmcloud ce project select -n [プロジェクト名]
```

アプリケーションを作成します。`./run`を実行、、といきたいところですが、前提となるレジストリー・アクセス・シークレットの設定が足りないようで、コンソールから実施します。プロジェクトのアプリケーション画面から、アプリケーションを作成します。

名前にhelloと入れて、「イメージの構成」をクリックします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307720/11751f18-f3e4-3e17-a9c2-aa1fc02b3eeb.png" width="700">

レジストリー・サーバーに`private.jp.icr.io`を入力し、名前空間を自身のもの、リポジトリーはhelloを選択して、「完了」をクリックします。このとき裏で、`ce-auto-icr-private-jp-tok`という名前のレジストリー・アクセス・シークレットが作成されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307720/950bc0cc-2252-8ea0-cbe5-f24a8b08bc7a.png" width="500">

さらに「作成」をクリックして、アプリケーションを作成します。

これ以降のアプリケーションの更新は、以下のコマンドで実行可能です。（runスクリプトでは、アプリケーションを一度削除してから作り直していますが、これではURLが毎回変わってしまいます）

```
$ ibmcloud ce app update -n hello --image private.${REGISTRY}/hello:latest --rs ce-auto-icr-private-jp-tok
```

アプリケーションのURLは、コンソールのEndpointsタグページか、以下のコマンドで確認出来ます。

```
$ ibmcloud ce app get -n hello -o url
```

curlで動作を確認します。（`**********`の部分は、ご自身の環境に合わせてください）

```
$ curl https://hello.**********.jp-tok.codeengine.appdomain.cloud
Hello World
```

## Sorryページの作成

まず、Dockerfileに1行追加(3行目)して、必要なモジュールをインストールします。

```Dockerfile:Dockerfile
FROM icr.io/codeengine/node:12-alpine
RUN npm install
RUN npm install express morgan
COPY server.js .
EXPOSE 8080
CMD [ "node", "server.js" ]
```

次にserver.jsの中身を丸っと入れ替えます。

```Javascript:server.js
var express = require('express');
var morgan = require('morgan');
var app = express();

// Custom access log
app.use(morgan(':req[CF-Connecting-IP] - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'));

// For health check
app.get('/ready', function (request, response) {
   response.writeHead(200, {'Content-Type': 'text/plain; charset=UTF-8'});
   response.end('OK');
});

// Return 503 with sorry.html
app.all('/*', function (request, response) {
   msg = process.env.MSG ? process.env.MSG : 'Sorry. Out of service now.';
   response.writeHead(503, {'Content-Type': 'text/html; charset=UTF-8'});
   response.end(msg);
});

app.listen(8080);
```

大まかに以下の処理を実施しています。

+ アクセスログの設定。加えて、クライアントのIPアドレスを、CISが設定するヘッダ`CF-Connecting-IP`から引用する
+ CISからのヘルスチェック(GET /ready)に対して、200 OKを返す
+ その他のリクエスト全てに対し、503でHTMLコンテンツ（環境変数MSGで設定）を返す

ビルドして、アプリケーションを更新します。

```
$ export export REGISTRY=jp.icr.io/[名前空間名]
$ ./build
$ ibmcloud ce app update -n hello --image private.${REGISTRY}/hello:latest --rs ce-auto-icr-private-jp-tok
```

Sorryページと、ヘルスチェック用ページをそれぞれcurlで確認します。

```
$ curl -v https://hello.**********.jp-tok.codeengine.appdomain.cloud
・・・中略・・・
< HTTP/1.1 503 Service Unavailable
< content-type: text/html; charset=UTF-8
< date: Tue, 13 Sep 2022 09:54:29 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 10
< x-powered-by: Express
< transfer-encoding: chunked
< 
* Connection #0 to host hello.**********.jp-tok.codeengine.appdomain.cloud left intact
Sorry. Out of service now.
```

```
$ curl -v https://hello.**********.jp-tok.codeengine.appdomain.cloud/ready
・・・中略・・・
< HTTP/1.1 200 OK
< content-type: text/plain; charset=UTF-8
< date: Tue, 13 Sep 2022 09:55:26 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 5
< x-powered-by: Express
< transfer-encoding: chunked
< 
* Connection #0 to host hello.**********.jp-tok.codeengine.appdomain.cloud left intact
```

## Sorryページの設定

環境変数MSGに任意の文字列を設定することで、Sorryコンテンツをカスタマイズ出来ます。以下を実行することで、新しいリビジョンが作成され、アプリケーションが更新されます。

```
$ ibmcloud ce app update --name hello --env MSG="ごめんなさい"
```

```
$ curl https://hello.**********.jp-tok.codeengine.appdomain.cloud
・・・中略・・・
< HTTP/1.1 503 Service Unavailable
< content-type: text/html; charset=UTF-8
< date: Wed, 14 Sep 2022 07:14:24 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 7888
< x-powered-by: Express
< transfer-encoding: chunked
< 
* Connection #0 to host hello.**********.jp-tok.codeengine.appdomain.cloud left intact
ごめんなさい
```

HTMLファイルを仕込むことも可能です。

```HTML:sorry.html
<html>
<header>
    <title>すまん</title>
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Cache-Control" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <meta charset="UTF-8">
</header>
<body>
<h1>ごめんなさい</h1>
<h2>いやほんと</h2>
<h3>すんません</h3>
</body>
</html>
```

```
$ ibmcloud ce app update --name hello --env MSG="$(cat sorry.html)"
```

```
$ curl -v https://hello.**********.jp-tok.codeengine.appdomain.cloud
・・・中略・・・
< HTTP/1.1 503 Service Unavailable
< content-type: text/html; charset=UTF-8
< date: Wed, 14 Sep 2022 07:11:39 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 90
< x-powered-by: Express
< transfer-encoding: chunked
< 
<html>
<header>
    <title>すまん</title>
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Cache-Control" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <meta charset="UTF-8">
</header>
<body>
<h1>ごめんなさい</h1>
<h2>いやほんと</h2>
<h3>すんません</h3>
</body>
</html>
```

Sorryページの中で使用したい画像ファイルは、Base64でテキストデータに変換してHTMLに仕込むのが良いと思います。

## CIS設定

まず、Sorryサーバー用のヘルス・チェックを作成します。

+ 名前 : 任意
+ モニター・タイプ : HTTPS
+ パス : /ready

次に、フォールバック・プールに指定する起点プールを作成します。

+ 名前 : 任意
+ 起点
    + 起点名 : 任意
    + 起点アドレス : Sorryページのホスト名
    + ホスト・ヘッダー : 同上
+ ヘルス・チェック : 作成したヘルス・チェックを選択

最後にこの起点プールを、ロード・バランサーに加え、フォールバック・プールとして指定します。

これでいつでも謝れます（出来れば見たく無いページですが）