ブループリントからLambda@Edgeの機能を理解する #reinvent

2016.12.03

はじめに

こんにちは、中山です。

個人的にかなり熱いアップデートであるLambda@Edgeについてエントリを書きます。今回はブループリントを元に実際の所Lambda@Edgeはどういったコードになるのかをまとめたいと思います。そもそもLambda@Edgeがどういったものなのかは以下のエントリを参照してください。

以前のエントリでも書きましたが現時点(2016/12/2)ではまだプレビュー段階です。利用したい場合はこちらから申請する必要があります。プレビュー版なので今のところ公開されている情報からしかお伝えできない点はご了承ください。

ブループリント

今回は現在で用意されている以下の2つのブループリントを対象にします。

  • cloudfront-modify-response-header
  • cloudfront-ab-test

ちなみにですが、ブループリントの選択画面で以下のように「Edge Node.js 4.3」と表示されるようです。便利。

lambda-at-edge1

二つのブループリントはそれぞれレスポンス/リクエストイベントを処理するLambda関数が含まれています。それぞれのイベントがどういったデータ構造を持っているのかについては以下のドキュメントを参照してください。

それでは早速見ていきましょう。

cloudfront-modify-response-header

このブループリントにはレスポンスヘッダを解析して特定のヘッダが含まれていた場合に書き換えるLambda関数が含まれています。ブループリントからコードを引用します。

'use strict';
exports.handler = (event, context, callback) => {
    const response = event.Records[0].cf.response;
    const headers = response.headers;
    const customHeaderName = 'X-Amz-Meta-Last-Modified';
    const headerNameToBeChanged = 'Last-Modified';

    if (headers[customHeaderName]) {
        headers[headerNameToBeChanged] = headers[customHeaderName];
    }
    callback(null, response);
};
  • 3 - 4行: イベントから渡されたレスポンスヘッダを変数に格納
  • 5 - 6行: 対象のヘッダを変数に格納
  • 8 - 10行: レスポンスヘッダの中に X-Amz-Meta-Last-Modified が含まれていた場合は Last-Modified をその値に変更
  • 11行: レスポンスを返却

まず Last-Modifed ヘッダについてこちらのサイトが非常に分かりやすかったため引用します。

Last-ModifiedヘッダはレスポンスヘッダのひとつでApacheやNginxなどのWEBサーバー側で適切な設定をすることによって、ブラウザ側にコンテンツの最終更新時刻を送信することができます。

ブラウザ側は、このコンテンツの最終更新時刻を覚えておき次回リクエストした際にリクエストヘッダの中に含めて送信します。サーバー側のコンテンツに変更がなければサーバーはステータスコード304という「コンテンツ未更新」ステータスコードを送ります。 ブラウザが304を受け取ると自分のブラウザにキャッシュされたコンテンツを表示させるため、オリジンサーバーもしくはCDNキャッシュサーバー側でコンテンツを配信することはなく、配信負荷が軽減されるというものです。

つまり Last-Modified で指定された日時より新しいコンテンツの場合はデータを再取得するということですね。それに対して X-Amz-Meta-* とはS3オブジェクトで指定可能なカスタムヘッダの識別子です。 Last-Modified の値をこの値で書き換えることによって、いつコンテンツが更新されたのかという情報を細かく制御できそうですね。

cloudfront-ab-test

このブループリントにはリクエストヘッダを解析して特定のクッキーに基づきアクセス先URIを変更してA/Bテストを実施するLambda関数が含まれています。ブループリントからコードを引用します。

'use strict';

exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const headers = request.headers;

    if (request.uri !== '/experiment-pixel.jpg') {
        // do not process if this is not an A-B test request
        callback(null, request);
    } else {
        const experimentCookieName = 'X-Experiment-Name=';

        const groupA = 'A';
        const groupB = 'B';

        const groupAObject = '/experiment-group/control-pixel.jpg';
        const groupBObject = '/experiment-group/treatment-pixel.jpg';

        /*
         * Lambda at the Edge headers are array objects.
         * Client may send multiple Cookie headers, i.e.:
         * > GET /viewerRes/test HTTP/1.1
         * > User-Agent: curl/7.18.1 (x86_64-unknown-linux-gnu) libcurl/7.18.1 OpenSSL/1.0.1u zlib/1.2.3
         * > Cookie: First=1; Second=2
         * > Cookie: ClientCode=abc
         * > Host: example.com
         * You can access the first Cookie line by
         *    headers["Cookie"][0]
         * and the second by
         *    headers["Cookie"][1]
         *   headers["Cookie"][0] will return "First=1; Second=2", cookie tokens are not parsed
         * separately.
         */
        let modifiedUri = false;
        if (headers.Cookie) {
            for (let i = 0; i < headers.Cookie.length; i++) {
                const experimentIndex = headers.Cookie[i].indexOf(experimentCookieName);
                console.error(experimentIndex);
                if (experimentIndex >= 0) {
                    if (headers.Cookie[i][experimentIndex + experimentCookieName.length] === groupA) {
                        request.uri = groupAObject;
                        modifiedUri = true;
                    } else if (headers.Cookie[i][experimentIndex + experimentCookieName.length] === groupB) {
                        request.uri = groupBObject;
                        modifiedUri = true;
                    }
                }
            }
        }

        /*
         * If this is the first time the viewer is
         * requesting this or we have not found
         * the experiment cookies,
         * randomly distribute viewers between objects.
         */
        if (!modifiedUri) {
            if (Math.random() < 0.75) {
                // 75% of the viewers go to group A.
                request.uri = groupAObject;
            } else {
                request.uri = groupBObject;
            }
        }

        callback(null, request);
    }
};

以下の内容を実施していることがコードから読み取れます。

  • 4 - 5行: でイベントから渡されたリクエストヘッダを変数に格納
  • 7 - 9行: A/Bテスト対象でない場合はリクエストの書き換えを行わない
  • 11 - 17行: それぞれ以下の内容です
    • A/Bテストの対象クライントを識別するクッキーを変数に格納
    • グループ毎にリクエストの書き換えるURIを変数に格納
  • 35 - 36行: ヘッダにクッキーが存在する場合、forで回して全てのクッキーを対象にする
  • 37 - 49行: クッキーの配列からA/Bテスト用クッキーが見つかった場合、それぞれのグループ用URIに書き換え
  • 57 - 64行: リクエストが初めての場合またはA/Bテスト用クッキーが見つからない場合はランダムにURIを書き換える
  • 66行: 書き換えたリクエストを返却

100行にも満たないコードで簡易的なA/Bテストが実施できそうですね。すごい。

まとめ

いかがだったでしょうか。

ブループリントを元にLambda@Edgeのコードをご紹介しました。基本的には今までのLambdaと同じように書けるようですね。個人的にはPython対応を心待ちにしています!引き続きLambda@Edgeのエントリを書いていきたいと思います。

本エントリがみなさんの参考になれば幸いです。