ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

CloudFront Functions の使い方を学ぶ

こんにちは、rightgo09 です。今回は CloudFront Functions についてまとめてみます。

目次

前置き:CloudFront Functions とは

2021 年 5 月

CloudFront Functions とは、Amazon CloudFront (CDN) のエッジコンピューティングです。

元々提供されていた Lambda@Edge との違いは、Lambda@Edge がリージョナルエッジで実行されるのに対して、CloudFront Functions は更にユーザに近いエッジロケーションで実行されるところです。

速度が求められる Web 業界において、ユーザに近いところで処理できるのはとても魅力的な選択肢ですが、その代わり制約も大きく、例えば、この実行は 1 ミリ秒未満で終える必要があります。1 ミリ秒を超えてしまうとその時点で CloudFront が 503 エラーをエンドユーザに返して終わるため、実行に 1 ミリ秒を超えないことは特に重要となります。

1 ミリ秒以上かかったので 503 エラーが返ってきたとき

他の主な制約たち

  • リソース設定(CPU, メモリ)はできない
  • 環境変数は使えない
  • 使用可能言語が JavaScript 一択(機能制限もあり)
  • HTTP のボディ部分(リクエスト・レスポンスともに)には関与できない

制約の範囲外を実現したい場合は、素直に Lamda@Edge を使うのが良さそうです。

CloudFront Functions のコードを見る

使用可能言語は JavaScript のみとなっています。現在、ES5.1 が基準となっており、constlet も使用できず、Lambda で Node.js ランタイムを選択して実装するのとは全く異なるため、かなり注意が必要です。

エントリーポイントは handler() という関数です。この関数は必ず定義されている必要があります。また、1 ファイルで完結している必要があるため、ライブラリを読み込むことはできません。

CloudFront Functions 関数作成時のスケルトン

function handler(event) {
    // NOTE: This example function is for a viewer request event trigger. 
    // Choose viewer request for event trigger when you associate this function with a distribution. 
    var response = {
        statusCode: 200,
        statusDescription: 'OK',
        headers: {
            'cloudfront-functions': { value: 'generated-by-CloudFront-Functions' }
        }
    };
    return response;
}

この例では「ビューワーリクエスト」(下図参照)としての設置が想定されており、エンドユーザからのアクセスを処理してその場で直接レスポンスを返却する例となっています。後続のサーバたちに処理は伝播しないため、実質 200 OK を返すダミーのようなスケルトンとなっています。

CloudFront のビヘイビアに関数を設定し、アクセスしてみると、以下のようになります。

$ curl -i https://xxxxxxxxxxxx.cloudfront.net/
HTTP/2 200
server: CloudFront
cloudfront-functions: generated-by-CloudFront-Functions    ← ここが上記の成果
x-cache: FunctionGeneratedResponse from cloudfront
content-length: 0
....
(ボディはない)

どんな使い方ができる?

CloudFront Functions には 2 つの設置の仕方があります。

1 つは前述の「ビューワーリクエスト」で、エンドユーザからのアクセスを処理します。 もう 1 つは「ビューワーレスポンス」で、後続のサーバたち(エッジロケーション含む)が作成したレスポンスに対して処理をします。

両方に設置したときの図

ビューワーリクエストに設置した場合

  • リクエスト用の構造を return すると、後続のサーバたちに処理したリクエストを伝播する
  • レスポンス用の構造を return すると、そのままエンドユーザへのレスポンスとなる
  • リクエストの HTTP ボディ部分に触れることはできない

ビューワーレスポンスに設置した場合

  • エンドユーザに送信しようとしているレスポンスを変更できる
    • ただし、ステータスコード(200 OK とか)を変更することはできない
  • レスポンスの HTTP ボディ部分に触れることはできない

ともにボディに触れることができないので、大掛かりな変更はできません(そもそも 1ms しか時間がないので、大きめのデータを相手にしている余裕はない)。

AWS 公式サンプルの確認

セキュリティヘッダの問答無用の追加 Github

(ビューワーレスポンスとして設置)

function handler(event) {
    var response = event.response;
    var headers = response.headers;

    // Set HTTP security headers
    // Since JavaScript doesn't allow for hyphens in variable names, we use the dict["key"] notation 
    headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; 
    headers['content-security-policy'] = { value: "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; frame-ancestors 'none'"}; 
    headers['x-content-type-options'] = { value: 'nosniff'}; 
    headers['x-frame-options'] = {value: 'DENY'}; 
    headers['x-xss-protection'] = {value: '1; mode=block'};
    headers['referrer-policy'] = {value: 'same-origin'};
    
    // Return the response to viewers 
    return response;
}

セキュリティ関連の HTTP ヘッダを追加しています。

ちなみに、正にこの例に関しては 2021 年 11 月から提供されたレスポンスヘッダーポリシー(Managed-SecurityHeadersPolicy の適用)を使用する方がお手軽で用途にもあっているので、わざわざ CloudFront Functions で行う必要はなくなったようです。

URL に index.html をつける Github

(ビューワーリクエストとして設置)

function handler(event) {
    var request = event.request;
    var uri = request.uri;
    
    // Check whether the URI is missing a file name.
    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    } 
    // Check whether the URI is missing a file extension.
    else if (!uri.includes('.')) {
        request.uri += '/index.html';
    }

    return request;
}

//books などで終わる URL の場合、/index.html/books/index.html という URL にしてしまう関数の例です。//index.html が同じ内容のとき、もし CloudFront のキャッシュキーに URL パスを使用している場合は、何もしないままだと //index.html は別々のキャッシュとして保持されてしまい、効率が悪いので、片方に寄せて統一しているようです。

国判定でリダイレクトする Github

(ビューワーリクエストとして設置)

function handler(event) {
    var request = event.request;
    var headers = request.headers;
    var host = request.headers.host.value;
    var country = 'DE' // Choose a country code
    var newurl = `https://${host}/de/index.html` // Change the redirect URL to your choice 
  
    if (headers['cloudfront-viewer-country']) {
        var countryCode = headers['cloudfront-viewer-country'].value;
        if (countryCode === country) {
            var response = {
                statusCode: 302,
                statusDescription: 'Found',
                headers:
                    { "location": { "value": newurl } }
                }

            return response;
        }
    }
    return request;
}

CloudFront では CloudFront Functions よりも手前で HTTP リクエストヘッダに追加で情報を差し込むことができ、cloudfront-viewer-country もその一つです。

ref. CloudFront HTTP ヘッダーの追加 - Amazon CloudFront

上記の cloudfront-viewer-country は、エンドユーザがアクセスしてきた国の情報が使えます(これは IP アドレスを基にした判定とのこと)。ドイツからのアクセスと判定されたときは、de を含めた URL にリダイレクトさせて、そうでなければ後ろに伝播という例でした。

やってみた

IP アドレス制御

(ビューワーリクエストとして設置)

function handler(event) {
    var ip = event.viewer.ip;
    console.log(ip);

    if (ip === "203.0.113.111") { // 許可する IP アドレス。環境変数が使えないので直書き。
        return event.request;
    } else {
        return {
            statusCode: 404,
            statusDescription: 'Not Found',
        };
    }
}

ちなみに console.log() の結果は CloudWatch Logs に出力されますが、「米国東部 (バージニア北部) us-east-1」リージョンにロググループが作成されることを覚えておく必要があります。プログラム自体は全世界に散在していますが、リクエストがあったリージョンごとにログが作成されるのではなく、us-east-1 に集約してくれています。

Google クローラボットのときに目印をつける

CloudFront Functions だけでなく後続に Lambda@Edge も設置し、そこでレンダリングするために、事前に目印をつけます。

(ビューワーリクエストとして設置)

function handler(event) {
    var request = event.request;
    var ua = request.headers["user-agent"].value;

    // 簡単判定
    if (ua.includes("Google")) {
        request.headers["x-google-bot"] = {value: "true"};
    } else {
        request.headers["x-google-bot"] = {value: "false"};
    }

    return request;
}

Lambda@Edge 内でやることの一部を CloudFront Functions に移動しただけとも言えます。

まとめ

CloudFront Functions について基本的な使い方を学びました。たくさんの制限がありますが、使い方次第ではかなり便利で最速な機能であると理解しました。

以上です。

unifa-e.com