こんにちは、rightgo09 です。今回は CloudFront Functions についてまとめてみます。
目次
前置き:CloudFront Functions とは
2021 年 5 月
- Amazon CloudFront が軽量エッジコンピューティング機能である CloudFront Functions を発表
- CloudFront Functions の導入 – 任意の規模において低レイテンシーでコードをエッジで実行 | Amazon Web Services ブログ
CloudFront Functions とは、Amazon CloudFront (CDN) のエッジコンピューティングです。
元々提供されていた Lambda@Edge との違いは、Lambda@Edge がリージョナルエッジで実行されるのに対して、CloudFront Functions は更にユーザに近いエッジロケーションで実行されるところです。
速度が求められる Web 業界において、ユーザに近いところで処理できるのはとても魅力的な選択肢ですが、その代わり制約も大きく、例えば、この実行は 1 ミリ秒未満で終える必要があります。1 ミリ秒を超えてしまうとその時点で CloudFront が 503 エラーをエンドユーザに返して終わるため、実行に 1 ミリ秒を超えないことは特に重要となります。
他の主な制約たち
- リソース設定(CPU, メモリ)はできない
- 環境変数は使えない
- 使用可能言語が JavaScript 一択(機能制限もあり)
- HTTP のボディ部分(リクエスト・レスポンスともに)には関与できない
制約の範囲外を実現したい場合は、素直に Lamda@Edge を使うのが良さそうです。
CloudFront Functions のコードを見る
使用可能言語は JavaScript のみとなっています。現在、ES5.1 が基準となっており、const
や let
も使用できず、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 について基本的な使い方を学びました。たくさんの制限がありますが、使い方次第ではかなり便利で最速な機能であると理解しました。
以上です。