Amazon Web Services ブログ

Lambda@Edge デザインベストプラクティス

Lambda@Edge をより活用いただくために Lambda@Edge ベストプラクティスシリーズと題したブログを連載します。この中ではいくつかのユースケースを用いて Lambda@Edge をどのように利用すればよいか、CI/CD パイプラインにどのように組み込むべきか、ビジネスニーズに応える形で組み込まれていることを担保するためにはどのように考えればよいか等について取り上げます。

記念すべき初回は Lambda@Edge のデザインベストプラクティスについて取り上げます。いくつか一般的なユースケースをもとに関数をどのタイミングで実行するのが良いのか、それはどのような観点で選択されるべきかということについてパフォーマンス及びコスト最適化の観点から推奨構成について説明していきたいと思います。

Lambda@Edge について

Lambda@Edge を使うことで、サーバーのプロビジョニングや管理を行うことなく、Node.js の関数を AWS ロケーション全体でグローバルに実行できます。これにより多様なコンテンツやパーソナライズされたコンテンツの提供を低レイテンシで行うことが可能となります。コンテンツを加工する関数は実現する内容に応じて異なるタイミングで実行できます。ビューワー(クライアント) がコンテンツのリクエストを出した時点で関数を実行することもできますし、CloudFront がオリジンに対してリクエストを送るタイミングで実行することもできます。Node.js のコードを Lambda@Edge にアップロードした時点から必要なものをグローバルロケーションにコピーし、必要なスケールで実行できるように展開します。したがって、Lambda@Edge の関数はユーザーの近くにある場所で実行されます。世界中のどこで関数が実行されようと全体の実行時間が課金の対象となります。

一般的なユースケース

世界中の多くのお客様が多様なユースケースで様々なソリューションとして利用されています。そのような中で Lambda@Edge を利用するメリットは大きく以下の 4 つのカテゴリに分類されます。

パフォーマンス: Lambda@Edge を使用する最大の利点の 1 つは、コンテンツがオリジンから返されたときにキャッシュされる可能性を高めたり、キャッシュに既に存在するコンテンツを利用されやすくすることで、キャッシュヒット率を向上させることです。キャッシュヒット率の向上はキャッシュミスによるオリジンアクセスを減らすことにつながるため、アプリケーション、Web サイトのパフォーマンスの向上に大きく寄与します。Lambda@Edge を使うことによりキャッシュヒット率のが向上する可能性が高い例は以下のとおりです。

  • レスポンス時における Cache Control ヘッダーを付与もしくは変更
  • オリジンからの 3xx レスポンスに対してリダイレクト処理を実装し、ビューワーレスポンスのレイテンシを低減
  • クエリ文字列や User-Agent の並び替えやグルーピングによるリクエストのバリエーション(キャッシュキー) の集約
  • リクエストヘッダーや Cookie 、クエリ文字列をもとにした異なるオリジンへの誘導によるレイテンシの低減

動的コンテンツの生成: リクエスト、レスポンスそれぞれに含まれる値をもとにコンテンツを動的に生成することができます。良く利用される例は以下のとおりです。

  • リクエストに含まれるパラメーターをもとにした画像のリサイズ
  • Mustache のようなロジックフリーなテンプレートを利用した際のレンダリング
  • A/B テスト
  • 有効期間が過ぎたコンテンツなどに対する 301/302 リダイレクトレスポンス

セキュリティ: Lambda@Edge をカスタム認可/認証に利用することもできます。一般的なユースケースは以下のとおりです。

  • リクエストに Token などの署名を付与によるカスタムオリジンにおけるアクセスコントロールへの対応
  • JWT/MD5/SHA トークンハッシュを利用してクライアントからのリクエストに対してトークン認証機能を提供
  • ボット検出機能と連携
  • HSTS(Hypertext Strict Transport Security) や CSP(Contents Security Policy) などのヘッダーの付与

オリジン依存性の排除: いくつかのケースにおいてオリジンサーバーはリクエストやレスポンスに対して追加の処理が必要な場合があります。このような実装をオリジン側に行うのではなく Lambda@Edge で実行させることでよりシームレスにアプリケーションを動作させることができるようになります。よく利用されるケースは以下の通りです。

  • Pretty URL の作成
  • オリジンへのリクエストにおける認証認可の管理
  • オリジンのディレクトリ構成に合致するようにするためのURLの変更
  • カスタムロードバランシングロジックやフェイルオーバーの実装

トリガーの選択

Lambda@Edge 関数では次の 4 つの CloudFront のイベントをトリガーに設定できます。

ビューワー オリジン
リクエスト 全てのリクエストに対して CloudFront のキャッシュを確認する前に実行 キャッシュミスの時にオリジンへリクエストを送る前に実行
レスポンス 全てのリクエストに対してオリジンかキャッシュからレスポンスが応答された後に実行 キャッシュミスの時にオリジンからレスポンスが応答された後に実行

こちらのドキュメントで利用ケースから Lambda@Edge 関数のトリガーを選ぶことができます。加えて、次の確認項目は Lambda@Edge 関数でどのトリガーを利用するか決める際に役立ちます。

  • キャッシュミスの時に関数を実行したいか? → オリジンのトリガーを選択
  • 全てのリクエストに対して関数を実行したいか?→ ビューワーのトリガーを選択
  • キャッシュキー(URL 、Cookie、ヘッダー、クエリ文字列) を変更したいか?→ ビューワーのリクエストをトリガーに選択
  • 結果をキャッシュせずにレスポンスを変更したいか?→ ビューワーのレスポンスをトリガーに選択
  • 動的にオリジンを選択したいか?→ オリジンのリクエストをトリガーに選択
  • オリジンの URL を書き換えたいか?→ ビューワーのリクエストをトリガーに選択
  • キャッシュされないレスポンスを生成したいか?→ ビューワーのリクエストをトリガーに選択
  • キャッシュされる前にレスポンスを変更したいか?→ オリジンのレスポンスをトリガーに選択
  • キャッシュされるレスポンスを生成したいか?→ オリジンのリクエストをトリガーに選択

コスト効率の最適化

Lambda@Edge は次の 2 つの項目に基づいて課金されます(料金は本ブログが投稿された時点のものとなります)。

  1. リクエスト数: 100 万リクエストにつき $0.60 です。
  2. 関数の所要時間: GB- 秒につき $0.00005001 です。例えば、Lambda@Edge 関数に 128MB を割り当てた場合、128MB- 秒毎に $0.00000625125 が所要時間として課金されます。なお Lambda@Edge 関数は 50 ミリ秒単位で切り上げられます。

最新の価格と Lambda@Edge の課金例は料金ページをご確認ください。Lambda@Edge 関数の費用を削減するためには次の項目に従って、必要な時にだけ関数を実行するようにして、またリソースの割り当てを最適化してください。

最初はこ関数呼び出しの最適化です。Lambda@Edge のトリガーとして関数の実行が必要最低限となる CloudFront の動作を選択します。例えば、ビューワーの認可に Lambda@Edge が用いられるこちらのソリューションでは、プライベートコンテンツに対してのみ Lambda@Edge 関数が実行されます。このユースケースでは、CloudFront のキャッシュ動作でパスのパターンを “private/*” と指定することで、オリジンのプライベートコンテンツを識別しています。

次に適切なトリガーを選択することです。Lambda@Edge のロジックは、オリジンかビューワーのトリガーを使って実装することができます。例えば、ビューワーの HTTP レスポンスのヘッダーに HSTS を追加することができます。この例では、Lambda@Edge の呼び出しを最適化し、かつ CloudFront のキャッシュを活用するために、ビューワートリガーではなくオリジントリガーを選択するべきです。

最後に、関数のリソース割り当てを最適化することです。ビューワートリガーのリソース割り当ては 128MB に制限されていますが、オリジントリガーでは 3008MB までリソースを割り当てられます。また関数の実行時間を最適化するために必要であるため、メモリサイズを増やすと利用できる CPU リソースが同等に増加されます。処理内容と予算にあわせて、これらの要素のバランスを考慮してベストなメモリサイズを選択する必要があります。

性能の最適化

Lambda@Edge 関数は CloudFront のグローバルネットワークを用いて全世界の AWS ロケーションで実行されます。ビューワーのリクエストがエッジロケーションに届くと、リクエストはエッジで処理され、Lambda@Edge はビューワーに近い AWS ロケーションで関数を実行します。

CloudFront ディストリビューションで Lambda@Edge を利用している時と利用していない時を比較すると、ビューワーが感じるレイテンシーは異なります。この差は、CloudFront ディストリビューションの設定、トリガータイプ、エッジロケーション、関数のコード、アプリケーションのロジックなどを含んだ、いくつかの要素で決まります。いくつか例をあげます。

  • バージニア北部リージョン(us-east-1) にバックエンドとして Lambda 関数にアクセスする APIGateway がある構成を考えます。グローバルなビューワーであり、アプリケーションは 260 ms (この値は 160 ms がバージニア北部リージョンへのファーストバイトレイテンシー(FBL) であり、それに加えてオリジンサーバーへの FBL として 100 ms がかかる) で動的に 3xx リダイレクト応答を生成します。リダイレクトの処理を Lambda@Edge 関数で実施すると、平均アプリケーション FBL は 110 ms (CloudFront の FBL が 80 ms、加えてビューワーのリクエストをトリガーとした Lambda@Edge の呼び出しに 30 ms) まで減少します。
  • CloudFront でキャッシュヒット率が 95% の静的な html ファイルにおいて、Lambda@Edge が HTTP セキュリティヘッダーを追加する処理を実行することを考えます。Lambda@Edge がキャッシュミスの時にだけ実行されるように設定されている場合、平均 FBL は 0.5 ms (オリジンのレスポンスをトリガーとして関数を呼び出す 10 ms の 5 % の時間) 増加します。

Lambda@Edge のサービスチームは性能の継続的な改良を進めており、上記で記載したレイテンシーは今後改善されていきます。

様々なユースケースで、Lambda@Edge の実装を最適化することでビューワーのエクスペリエンスを改善することができます。これを実現するためには Lambda@Edge 関数の実行時間を減らし、サービスで求められる機能が Lambda@Edge のスケーリングの範囲内で実行できていることを確認してください。

まずは下記に従って Lambda@Edge 関数の実行時間を削減しましょう。

  • 性能をよくするために関数のコードを最適化します。例えば、関数の呼び出し時の変数とオブジェクトの再初期化を制限するために実行コンテキストを再利用するようにします。これは Lambda@Edge 関数のコードがローカルに、検索、格納、参照される外部のコンフィグがある場合に特に重要です。そのような場合は代わりに静的初期化、コンストラクタ、グローバル変数や静的変数、シングルトンが利用できないか検討してください。
  • 関数実行時の外部ネットワークを最適化します。例えば、TCP 通信の Keep-Alive を有効にして、以前の関数呼び出しで確立したコネクション(HTTP やデータベースへの通信など) を再利用します。可能であればそれに加えて、ネットワークレイテンシーを減らすために Lambda@Edge 関数と同じリージョンのリソースへ通信してください。これを実現する一つの方法としては DynamoDB Global Tables を用います。他の推奨方法としては関数が必要なリソースにだけ外部へのリクエストを実行するようにします。例えば、Aurora や S3 Select ではクエリフィルターを利用できます。またリクエストする外部の変数が、頻繁には変わらない、ビューワーごとに変わらない、すぐに変更を反映する必要がない、ものであれば、コード内の定数を利用し、変数が変更された時にだけ関数をアップデートすることを検討してください。
  • パッケージのデプロイを最適化します。例えば、自分たちで作成できるようなシンプルな関数は外部パッケージに依存しないようにしてください。外部リソースを使う必要があるときは、軽量なパッケージを選択するようにしてください。加えて、デプロイパッケージを軽量化するために minify や browserfy のようなツールを使ってください。

次に、Lambda@Edge の関数スケーリングの制限に注意しましょう。そして必要に応じて上限緩和申請をリクエストするようにしてください。

関数で必要な上限をどのように見積もるか説明するために、次の例を考えてみます。静的な画像ファイルを CloudFront で 5000 RPS(1 秒あたりのリクエスト数)で配信し、キャッシュヒット率が 90% とします。またオリジンのレスポンスをトリガーとして HTTP セキュリティヘッダーを追加する Lambda@Edge 関数を実行しているとします。

Lambda@Edge は 10% × 5000 RPS = 500 RPS で呼び出されます。この関数はシンプルであるため、平均実行時間が 1 ms と推定できます。定常状態の同時実行数を計算するために 10 ms(オリジンのレスポンスをトリガーとして関数を呼び出す時間) を平均実行時間に追加し、計算した RPS をその値に掛けます。今回の例だと、500 RPS × (10 ms + 1 ms) より同時実行が 55 となります。1 アカウントあたりの同時実行数の制限が 1000 であるため問題がないとわかります。

Lambda@Edge 関数が数秒間で数百の同時実行でバーストする必要がある場合、他にも考慮しなくてはいけない点があります。急なバーストでは Lambda@Edge は同時実行数を決められた量だけすぐに増加させます。もし急な負荷の増加の対応として不十分な場合、Lambda@Edge はアカウントの上限に到達するか、関数の同時実行数が増加した負荷を正常に処理するのに十分な数になるまで 1 分あたり同時実行数を 500 づつ増やし続けます。この自動スケーリングにより急なトラフィックの増加が発生した最初の 1 分は Lambda@Edge 関数の同時実行が決められた量に制限されます。さらに、スケールアウト時のコールドスタートは、シンプルな関数でも関数の実行時間が一桁増えてしまいます。

まとめ

CloudFront と Lambda@Edge を DynamoDB Global Tables のような他の AWS サービスと共に利用することで、ユースケースにあった高性能な分散型サーバーレス Web アプリケーションを作り始めることができます。このブログではいくつかの一般的な Lambda@Edge のユースケース、そしてコストを確認しながら Lambda@Edge の性能を改良するためのベストプラクティスをお伝えしました。本シリーズの次のブログでは Lambda@Edge 関数を、簡単に開発してテストする方法、そして Lambda@Edge を CI/CD パイプラインに組み込む方法を説明していきたいと思います。

翻訳は SA 三上が担当しました。原文はこちらです。