Cloudflare Workersでメンテナンスモードを実装する

この記事では、Cloudflare Workers を使用してウェブサイトのメンテナンスモードを実現する方法を解説しています。また、作業中のユーザーにはオリジンサイトを、一般ユーザーにはメンテナンスページを表示させる方法を説明しています。
2023.10.16

Cloudflare Workersでメンテナンスモードを実装する

こんにちは。Classmethod Europe でフロントエンドエンジニアをしている watsuyo です。 ベルリンに来てから 1 ヶ月半が経過し、肌寒く、そして日照時間の短い冬の気配が感じられる季節になりました。

さて、ウェブサイトに大規模な変更を加える際、サイトの一時的な利用停止が必要な場面があります。Cloudflare で管理されているウェブサイトの場合、Waiting Room を使用することで、コードの編集を行わずに一時的なメンテナンスページを表示できます。

しかし、Cloudflare の契約プランによっては、Waiting Room を有効にするためにはプラン変更が必要な場合があります。また、作業中のユーザーにはオリジンサイトへのアクセスを許可し、同時に一般ユーザーにはメンテナンスページを表示する必要があったため、Cloudflare Workers を利用してメンテナンスモードを実装しました。

今回は、その時のお話をしようと思います!

本記事で取り上げる内容

  • Cloudflare Workers や Storage Options を使用したメンテナンスモードの実装方法

本記事で取り上げない内容

  • Cloudflare Waiting Room を使用したメンテナンスモードの構成
  • Cloudflare Workers の詳細の解説
  • Cloudflare Workers の Storage Option 詳細の解説
  • Cloudflare Workers のコンソール画面からの操作方法
  • Wrangler の説明

今回の要件

今回の主な要件は以下の通りです。

  • メンテナンス作業の所要時間は約 1 時間
  • 作業中のユーザーには、オリジンサイトへのアクセスを許可する(IPv4 形式の IP アドレスのホワイトリストのみを許可)
  • 一般のユーザーには、メンテナンスページへアクセスを誘導
  • メンテナンスモードの切り替えは作業開始と終了時に手動で行う

解決したい課題

今回の解決したい課題は以下の通りです。

  1. メンテナンスページのスムーズな表示
  2. Waiting Room を使わないメンテナンスモードの実装
  3. メンテナンスモードの切り替えにおける人的ミスの防止

解決のアプローチ

課題を解決するために、以下の技術と手法を採用しました。

  • メンテナンスページの HTML を事前に Cloudflare R2 に保存
  • IP アドレスホワイトリストを作成し、Cloudflare KV に保存
  • Cloudflare Workers を使用し、Client IP アドレスを元に作業中のユーザーと一般ユーザーを制御
  • Worker を事前にデプロイし、メンテナンス作業の開始と終了時に Cloudflare Workers Routes を API 経由でメンテナンスページをコントロール

まず、この記事を読み進める上で重要な、Cloudflare Workers の概念と、今回使用する Storage Option について簡単に説明します。

Cloudflare Workers とは?

Cloudflare Workers は、エンドユーザーに最も近い場所に配置されたエッジサーバー上で実行されるサーバーレスアプリケーションプラットフォームです。これにより、ユーザーからのリクエストは地理的に最適なエッジサーバーで処理され、応答時間が最小化されることでユーザーエクスペリエンスが向上します。Cloudflare Workers を利用することで、リクエストのリライトやリダイレクト処理など、柔軟なリクエスト処理が可能です。

今回は、IP アドレスのホワイトリストを活用し、オリジンサーバーのリクエストをエッジサーバー上で分岐させることで、解決したい課題の 1 と 2 の解決ができます。

また、IP アドレスのホワイトリストは、Cloudflare Workers の Storage Options にある KV を使用して管理します。これにより、コードに直接記述せずに設定を柔軟に変更できます。メンテナンスページは、静的な HTML ファイルとして用意しているので、R2 を使用して保存します。

続いて、Workers Storage Option の一部である KV と R2 のそれぞれについて説明します。

Workers KV とは?

Workers KV は Key-Value データストアで、低レイテンシーの読み取りが必要なアプリケーションに適しています。Cloudflare のグローバルネットワーク上にキャッシュされているため、高速なデータアクセスが可能です。

今回のケースでは、IP アドレスのホワイトリストなどの設定ファイルを API やコンソール画面から柔軟に変更し、Cloudflare Workers の実装コードに直接記述せずにデータを扱うことができます。

Workers R2 とは?

Workers R2 は AWS S3 互換のオブジェクトストレージで、S3 互換の API を使用して大容量のファイル(静的ファイルやバックアップデータなど)を保存できます。また、Egress 料金が無料なので、CDN のキャッシュデータとしても適しています。

今回のケースでも、多くのユーザーに静的ファイルを配信する際にもコストを抑えることができます。

Workers Routes とは?

Workers Routes は Cloudflare Workers の一部で、リクエストを特定の Worker にルーティングするための仕組みです。これを使用することで、異なるリクエストパスや条件に基づいてリクエストを異なる Worker スクリプトに振り分けることができます。Workers Routes は、トラフィックのカスタマイズとリクエスト処理の最適化に役立ちます。

今回の場合は、メンテナンス終了時に対象ドメインに対して、実装した Worker のルーティングを外すことで通常のトラフィックに戻すことができます。

要件に対する技術手法の概要

  • Cloudflare Workers の実装
    • Client IP アドレスを取得し、ホワイトリストに含まれる IP アドレスであれば、オリジンサイトのレスポンスを返す
      • それ以外の場合は、メンテナンスページの HTML を返す
  • Cloudflare Workers Routes の設定と削除
    • Cloudflare Workers は GUI や CLI で無効化できないため、Workers Routes の作成と削除によって、Worker のオンオフ切り替えを行う
    • 人間の介入を最小限に抑えるため、対話形式のシェルスクリプトを実装する
  • Cloudflare KV とCloudflare R2 の活用
    • IP アドレスのホワイトリストは Cloudflare KV に、メンテナンスページの静的 HTML はCloudflare R2 に保存する

Cloudflare Workers の実装

実装したコードは汎用的に使用できるような状態にし、GitHub で公開していますのでこちらをご確認下さい。

まずは、Cloudflare のドキュメントの通り、ローカルに Worker project を作成します。これにより、ブラウザ上に Hello World! を表示させたり、Worker をデプロイするところまでが完了します。

次に、リクエストヘッダーから Client IP アドレスを取得する方法を確認します。Cloudflare 公式の Example で紹介されている方法で取得します。

// クライアントのIPアドレス
request.headers.get('CF-Connecting-IP');

IPアドレス ホワイトリストを KV に格納し、それを Worker で扱っていきます。 Wrangler コマンドで KV を作成、 更にホワイトリストを追加し、Worker 上からホワイトリストを取得します。

# bunx の部分は、各自の実行環境で使用するコマンドに置き換えて下さい

$ bunx wrangler kv:namespace create your_kv

次に、作成した KV に key と value を指定してホワイトリストを追加します。

$ bunx wrangler kv:key put --binding=YOUR_KV_NAMESPACE "whiteList" "'192.168.1.1/29','192.168.1.2/29'"

データが保存されているかを確認することができれば、OK です。

$ bunx wrangler kv:key get --binding=YOUR_KV_NAMESPACE "whiteList"

'192.168.1.1/29','192.168.1.2/29'

Worker でホワイトリストを取得するには、以下のように記述します。

const whiteList = await env.YOUR_KV_NAMESPACE.get('whiteList');

次に、HTML ファイルを R2 bucket に保存し、Worker 上で取得してみます。

KV と同様に、Wrangler コマンドを実行します。

$ bunx wrangler r2 bucket create your_r2_bucket

次に、表示させたいメンテナンスページの HTML ファイルを bucket に保存します。

$ bunx wrangler r2 object put your-bucket/maintenance.html --file=./maintenance.html

bucket から HTML ファイル が取得できれば OK です。

$ bunx wrangler r2 object get your-bucket/maintenance.html

Worker 上でも HTML ファイル が取得できる確認してみます。

const object = await env.YOUR_BUCKET_NAMESPACE.get('maintenance.html');

これで、Workers の作成と、KV、R2 の準備が整いました。

次は、Workers でメンテンスモードを実装していきます。まずは、KV から IP アドレスのホワイトリストを取得し、配列形式にパースします。

const whiteList = await env.YOUR_KV_NAMESPACE.get('whiteList');
const ipRanges = whiteList?.split(',');

次に Client IP アドレスを取得し、ホワイトリストの IP アドレスある範囲かをチェックします。isIPAddressInRanges では、IP アドレスをバイト単位で分割し、数値の配列に変換した後に条件に一致するかどうかをチェックしています。

const clientIP = request.headers.get('CF-Connecting-IP');
const isAllowed = clientIP && ipRanges ? isIPAddressInRanges(clientIP, ipRanges) : false;
const isIPAddressInRanges = (ipAddress: string, ipRanges: string[]) => {
    const ipAddressBytes = ipAddress.split('.').map(Number);
    return ipRanges.some((ipRange) => {
        const [startIP, endIP] = ipRange.split('-');
        const startBytes = startIP.split('.').map(Number);
        const endBytes = endIP.split('.').map(Number);

        return (
            ipAddressBytes.slice(0, 3).every((byte, index) => byte === startBytes[index]) &&
            ipAddressBytes[3] >= startBytes[3] &&
            ipAddressBytes[3] <= endBytes[3]
        );
    });
};

ホワイトリストに含まれていれば、リクエストをそのままエンドユーザーのクライアントに返します。これによって、作業中のユーザーにはオリジンサイトを返すことができます。

if (isAllowed) {
    return await fetch(request);
}

次に、メンテナンスページを表示させたい場合の実装です。

bucket から取得した HTML を含むオブジェクトをエンドユーザーのクライアントに返します。

const maintenancePageObject = await env.YOUR_BUCKET_NAMESPACE.get('maintenance.html');
if (maintenancePageObject) {
    const maintenancePage = new Response(maintenancePageObject, {
        status: 503,
        headers: {
            'Content-Type': 'text/html',
            'Cache-Control': 'no-store, must-revalidate',
        },
    });
    return maintenancePage;
}

メンテナンスモードの切り替えを行う

今回のケースの場合、メンテナンス作業中のみ Worker を実行させる必要があります。そのため、 Route の作成と削除を手動で行うことでメンテナンスモードの切り替えを行います。

Cloudflare API を使用する場合は、以下のようになります。また、API トークンは Cloudflareのコンソール画面からアカウントとゾーンを選択して発行できます。

$ curl --request POST \\
  --url "<https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/workers/routes>" \\
  --header "Authorization: Bearer {YOUR_API_TOKEN}" \\
  --header 'Content-Type: application/json' \\
  --data '{
      "pattern": {Route},
      "script": {Worker}
  }'
$ curl --request DELETE \\
  --url "<https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/workers/routes/{ROUTE_ID}>" \\
  --header "Authorization: Bearer {YOUR_API_TOKEN}" \\
  --header 'Content-Type: application/json'

今回は、当日の作業中に誤って別の Route を削除してしまわないように、対話形式のシェルスクリプトを用意しました。Route を作成した際に返ってくる route_id を変数に保持して、 delete 時にコピペする必要が無いようにしました。

スクリプトは Gist に公開しています。こちらは汎用的に使用できるよう、API Token、Zone ID、Worker、Worker 名、および Route パターンを入力するようになっていますので、ご自由に変更してください。

他社の取り組み

メンテナンスの仕組みを刷新してみた件 - VisasQ Dev Blog

留意点

今回は紹介していませんが、SEO の観点からもメンテナンスページはクローラーの対象外にしておくべきです。サーバーレスポンスに 503 を返したり、HTML の meta 要素に属性を追加することで対応できます。

おわりに

Cloudflare Workers を使用して、ウェブサイトのメンテナンスモードを実装しました。これにより、ウェブサイトの大規模な変更やメンテナンス時に、作業中のユーザーにはオリジンサイトを表示し、一般のユーザーにはメンテナンスページを表示できるようになりました。

さらに、Cloudflare Workers KV、R2、Routes を活用することで、IPアドレスに基づくホワイトリスト管理や静的ファイルの配信、メンテナンスモードの切り替えを可能にしました。

この記事が参考になった方は、ぜひこの記事をシェアしていただけると嬉しいです。また、watsuyo までコメントやフィードバックも大歓迎です。

その他の参考ドキュメント