カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

【サクッと実装】CloudFront Functionsでつくる軽量メンテナンスページ

はじめまして、こんにちは。6月にカミナシに入社したソフトウェアエンジニアの渡邉(匠)です。「カミナシ 設備保全」の開発に携わっています。

みなさんはシステムのメンテナンスが必要になったときどう対応をしていますか?「カミナシ 設備保全」でもメンテナンスが必要になり、メンテナンス中はユーザーがサービスにアクセスできないようにメンテナンスページの作成が必要となりました。

※ 本記事で紹介する構成は、ユーザーがオリジン(S3バケットやALBなど)に直接アクセスできず、必ずCloudFrontを経由してアクセスする構成になっていることを前提としています。

開発方針

今回、メンテナンスページを実装する上で以下の要件を満たすことを条件としました。

  • メンテナンス中はユーザーにメンテナンスページが表示される
  • メンテナンス外ではユーザーがメンテナンスページへのアクセスができない
  • メンテナンス中に社内メンバーがサービスにアクセスできる

CloudFront Functions vs Lambda@Edge

「カミナシ 設備保全」では、CDNとして CloudFront を使っています。メンテナンスページを実装するにあたって、 CloudFront でリクエストを処理してトラフィックが切り替えられる CloudFront Functions または Lambda@Edge の利用を検討しました。

両者の違いについてはAWSのデベロッパーガイドで確認することができます。デベロッパーガイドより下表に両者の違いについて一部抜粋します。

CloudFront Functions Lambda@Edge
プログラミング言語 JavaScript (ECMAScript 5.1 準拠) Node.js / Python
イベントソース ビューワーリクエスト
ビューワーレスポンス
ビューワーリクエスト
ビューワーレスポンス
オリジンリクエスト
オリジンレスポンス
実行時間 1ミリ秒未満 最大 5 秒 (ビューワーリクエスト、ビューワーレスポンス)
最大 30 秒 (オリジンリクエスト、オリジンレスポンス)
最大メモリ 2 MB 128 MB (ビューワーリクエスト、ビューワーレスポンス)
10,240 MB (10 GB) (オリジンリクエスト、オリジンレスポンス)
コードの最大サイズ 10 KB 50MB
ネットワークアクセス なし あり
ファイルシステムアクセス なし あり
リクエストボディへのアクセス なし あり
スケール 最大数百万リクエスト / 秒 リージョンあたり 1万リクエスト/秒

参考:https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/edge-functions-choosing.html

今回の実装では、以下の理由から「CloudFront Functions」を利用することにしました。

  • メンテナンスページを表示するというシンプルな処理であることを考えると、CloudFront Functionsのネットワークアクセスや実行時間の制約は問題にならない
  • より長い実行時間や外部へのネットワークアクセスが可能なLambda@Edgeは、今回の目的においては明らかにオーバースペック
  • Lambda@Edge は us-east-1 にリソースを置く必要があり、普段利用しているリージョンと異なるが、CloudFront Functionsは普段利用しているリージョンで管理可能

具体的な制御方法として、ビューワーリクエスト(CloudFrontがキャッシュをチェックする前)のタイミングで関数を実行させることでメンテナンス中の制御をおこないます。また、メンテナンスの切り替えは、開始前に手動でCloudFront Functionsを有効化することにしました。これは手動によるプロセスの確実性を優先しました。

メンテナンスページをどこから返すか?

メンテナンスページの実装方針として大きく2パターンを検討しました。

  • 新しくメンテナンスページを作成する(S3やAWS Lambdaなど)
  • CloudFront Functions から直接メンテナンスページを返却する

以下の観点から「CloudFront Functions から直接メンテナンスページを返却する」という軽量な方針を選択しました。

  • メンテナンス画面に求められる要件は「メンテナンス中である旨が記載された簡単なHTMLが表示されること」だけで十分
  • このシンプルな要件に対し、もし別途メンテナンスページ(S3など)を実装すると、以下の追加検討が必要になる
    • メンテナンスページのアーキテクチャ
    • メンテナンス終了後の画面遷移の設計

これらの追加検討を不要とし、シンプルかつ迅速に実現できるため、Functionsから直接メンテナンスページを返す方法が最適だと判断しました。

実装手順

Step1. CloudFront Functions を定義する

CloudFront FunctionsをTerraformで作成します。
ランタイムと関数コードを指定するだけで簡単に作成できます。

resource "aws_cloudfront_function" "example" {
  name    = "example"
  runtime = "cloudfront-js-2.0"
  publish = true

  code = file("${path.module}/example_handler.js")
}

参考:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_function

関数コードには handlerという関数を実装した jsファイルを指定します。今回はCloudFront のビヘイビアと関数の関連付けはメンテナンス開始時に手動でおこなうこととしました。そのため、常にメンテナンスページを返すような関数を実装します。

function handler() {
    return {
        statusCode: 503,
        headers: {
            "content-type": { value: "text/html" },
        },
        body: {
            encoding: "text",
            data: `<html lang="ja"><body><h1>メンテナンス中</h1></body></html>`,
        },
    };
}

参考:https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/writing-function-code.html

これでCloudFront のビヘイビアと関数の関連付けをおこなうことでメンテナンスページを返すようにできました。
しかし、開発方針として「メンテナンス中に社内メンバーがサービスにアクセスできる」ようにする必要があります。

関数コードのhandlerは入力として eventオブジェクトを受け取ります。 eventオブジェクトにはCloudFrontがビューワーから受け取ったリクエストにアクセスすることが可能です。今回は特定のCookieがある場合、オリジンにリクエストを転送するように変更します。

function handler(event) {
    const request = event.request;
    // クッキーから特定のキーの値を取り出してチェックする
    const skipMaintenance = request.cookies["skip-maintenance"];
    if (skipMaintenance && skipMaintenance.value === "true") {
        return request
    }

    return {
        statusCode: 503,
        headers: {
            "content-type": { value: "text/html" },
        },
        body: {
            encoding: "text",
            data: `<html lang="ja"><body><h1>メンテナンス中</h1></body></html>`,
        },
    };
}

参考:https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html

Step2. AWSコンソールでCloudFront Functionsを有効化する

実際にAWSコンソール上でCloudFront FunctionsをCloudFrontに関連付けてみます。
メンテナンスページへの切り替えをしたいCloudFrontのディストリビューションを選択します。ビヘイビアタブで今回作成したCloudFront Functionsを有効にしたいビヘイビアを選択し、編集をします。

編集ページ内に関数の関連付けのフォームがあるため、ビューワーリクエストに関数の関連付けを設定します。

  • 関数タイプ:CloudFront Functions
  • 関数 ARN/名前:作成した関数

関連付けした関数の反映は保存してから数十秒程度で完了します!
関連付けの解除も同様の手順で可能なため、簡単にメンテナンスへの移行と解除が実施できます。

実際に「カミナシ 設備保全」ではこのようなメンテナンスページをCloudFront Functionsから返して表示していました。

より発展させるなら…

今回の実装では対応しませんでしたが、CloudFront KeyValueStore を利用することで 「メンテナンスページ上でメンテナンスの期間を動的に表示する」ような動的な制御をコードの変更なしで可能です。CloudFront KeyValueStore は、CloudFront Functions内から読み取り可能な低レイテンシーなキーバリューのデータストアです。

CloudFront FunctionsからCloudFront KeyValueStoreを参照する場合、あらかじめCloudFront Functionsに参照したいCloudFront KeyValueStoreの関連付けをおこなう必要があるため注意が必要です。
CloudFront KeyValueStoreへのアクセスはヘルパーメソッドが用意されています。ヘルパーメソッドを利用することで次のようにアクセスすることができます。

import cf from 'cloudfront';

const kvsId = '<KEY_VALUE_STORE_ID>';
const kvsHandle = cf.kvs(kvsId);

async function handler(event) {
    const key = 'maintenance-date-range'
    // メンテナンス期間をKVSから取得する
    let dateRangeStr = ''
    try {
        dateRangeStr = await kvsHandle.get(key);
    } catch (err) {
      // 簡単のため、エラー時はメンテナンス期間を空で表示する
        console.log(`kvs key lookup failed for ${key}: ${err}`);
    }

    return {
        statusCode: 503,
        headers: {
            "content-type": { value: "text/html" },
        },
        body: {
        encoding: "text",
        data: `
          <html lang="ja">
              <body>
                  <h1>メンテナンス中</h1>
                  <div>メンテナンス期間:${dateRangeStr}</div>
              </body>
          </html>`,
        },
    };
}

参考:https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-custom-methods.html

おわりに

今回初めてCloudFront Functionsを利用しましたが、実装が簡単で、CloudFrontへの関連付けから環境に反映されるまで数十秒程度とすぐに反映できる点が体験としてとても良かったです。

関数コードが最大10KBまでという制限があるため、リッチなページを作ることはできませんが、メンテナンスページという限られた用途であれば十分実用的であると感じました。

シンプルで簡単軽量なCloudFront Functionsを使ってみるのはいかがでしょうか?