re:Invent 2024: AppsFlyer社が語るCloudFrontのエッジコンピューティング活用法
はじめに
海外の様々な講演を日本語記事に書き起こすことで、隠れた良質な情報をもっと身近なものに。そんなコンセプトで進める本企画で今回取り上げるプレゼンテーションはこちら!
📖 AWS re:Invent 2024 - Practical applications of edge compute in Amazon CloudFront (CDN401)
この動画では、Amazon CloudFrontにおけるエッジコンピューティングソリューションについて、CloudFront FunctionsとLambda@Edgeの2つのオプションの特徴と使い分けを解説しています。特に、AppsFlyerのVice President of R&DであるDanny Tourgemanによる、1日2,200億インプレッションを処理する実際のユースケースが紹介され、Client HintsへのUser Agent対応やWAFとCloud Functionの連携によるオリジン負荷軽減など、具体的な実装方法と成果が共有されています。また、CloudFront FunctionsのCompute Utilizationの制限やLambda@Edgeのスケーリング制限など、各機能の制約事項と、それらを踏まえたコード最適化のベストプラクティスについても詳しく説明されています。
※ 画像をクリックすると、動画中の該当シーンに遷移します。
re:Invent 2024関連の書き起こし記事については、こちらのSpreadsheet に情報をまとめています。合わせてご確認ください!
本編
Amazon CloudFrontにおけるエッジコンピューティングの概要
皆さん、こんにちは。AWS のソリューションアーキテクトの Kamil Bogacz です。本日は、Amazon CloudFront におけるエッジコンピューティングソリューションについて、概念的な理解から実践レベルまでご案内できることを大変嬉しく思います。これは確かに大胆な発言かもしれません。私たちは皆、これが多くの練習、実験、そして実践的な経験を必要とする道のりであることを知っています。しかし、実証済みのユースケースを確認し、ベストプラクティスを詳しく見て、そしてそれらの微妙な違いや依存関係を詳しく見ていくことで、その道を進むための適切なメンタルモデルを構築する助けになると確信しています。
そのため、私は他の2人の実践者にも同席してもらっています。私の右側には、Amazon CloudFront のプリンシパルプロダクトマネージャーである David Brown と、これらのエッジコンピューティングサービスを広範に活用しているお客様の代表として、AppsFlyer の Vice President of R&D である Danny Tourgeman がいます。本題に入る前に、一つ明確にしておきたいことがあります。私たちが話すエッジコンピューティングとは何かということです。コンテンツカタログでエッジと検索しても、おそらく異なる技術や業界に関連する多くのセッションが見つかるでしょう。私たちが注目するエッジとは、Amazon CloudFront をコアサービスとする AWS のグローバルエッジインフラストラクチャを指し、そのインフラストラクチャ上で独自のコードを実行して、Webアプリケーションのコンテンツ配信をカスタマイズできるエッジコンピューティングのオプションを提供します。
さて、私たちが話すエッジの種類が明確になったところで、アジェンダの概要をお話しします。まず、利用可能なエッジコンピューティングのオプションを振り返り、それらが CloudFront のワークフローにどのように適合するかを重点的に見ていきます。これは非常に重要です。なぜなら、CloudFront が全体のコンテキストを設定し、すべての依存関係の源となるからです。私の経験では、これらをしっかりと理解してはじめて、自身のアーキテクチャにおけるエッジコンピューティングソリューションの実装について、十分な情報に基づいた決定を下すことができます。次に、エッジでコードを実行することでどのような問題を解決できるのか、そしてそれが最適な選択となる理由を示す、実証済みのユースケースをご紹介します。最後に、David からパフォーマンスの向上、スケーリング、そしてサービス制限のより良い管理に役立つベストプラクティスについてご説明します。
CloudFrontのインフラストラクチャとエッジコンピューティングオプション
先ほど、エッジという用語は、さまざまなユースケースや業界に関連する多くの異なる技術と結びつけられる可能性があると述べました。しかし、これらすべての技術において、真に特徴的なのは基盤となるプラットフォームです。なぜなら、そのプラットフォームが、デプロイメントモデル、利用可能な容量、そして使用できる機能を決定するからです。本セッションのトピックに関して言えば、それは Amazon CloudFront となります。これは AWS のコンテンツデリバリーネットワークであり、グローバルな規模であらゆる業界のお客様の Web アプリケーション配信を改善しています。この規模の要因は、レイテンシーを削減し、ワークロードにより高い回復力を追加するために特別に構築された AWS エッジネットワークに起因しています。
これを可能にしているのは、絶えず成長を続けるエッジロケーションのフットプリントです。現在、700以上のエッジロケーションがあり、そこには CloudFront ホストが配置され、ローカルなビューワーネットワークへのトラフィックを処理しています。これらすべてのエッジロケーションは、AWS バックボーンネットワークを通じて AWS でホストされているオリジンと非常に良好に接続されており、AWS リージョンからエッジロケーションまでの安定した高帯域幅で冗長性のある接続を確保し、インターネットレベルの不安定性から隔離されています。ISP ネットワークへの CloudFront の到達範囲をさらに深く拡張する最初のコンポーネントは、1100以上の組み込み POP で構成されています。これらは ISP ネットワークの深部に設置された CloudFront の小規模なデプロイメントで、コンテンツをターゲットとするビューワーにさらに近づけています。物理的なトポロジーから論理的なトポロジービューに戻ると、CloudFront は設計上、階層的な構造を持っていることも知っておく必要があります。これは、エッジインフラストラクチャの成長規模とアプリケーションオリジンサーバーにかかる負荷のバランスを取るためです。参考までに、過去2年間で、私たちは400から700のロケーションまで成長しました。これは間違いなく良いことです。新しい場所に新しいポイントオブプレゼンスを設置する際、特にローカルでより多くのトラフィックを処理し、レイテンシーを削減するために、インフラストラクチャを成長させたいと考えているからです。
しかし、このような成長に伴う副作用として、ロケーション数の増加によってオリジンへの集約負荷も増大してしまいます。この課題に対処するため、プラットフォームでは13の Regional Edge Cache を導入し、これらがすべての Edge ロケーションの親キャッシュとして機能しています。これらの役割は、近隣の Edge ロケーションからの負荷を吸収し、オリジンへの負荷を最小限に抑えることです。なぜなら、結局のところ、コンテンツはお客様のサーバーから取得する必要があるからです。
この導入部分は、Edge Computing を生み出した核心的なアイディアにつながります。つまり、これらすべてのロケーションで利用可能なコンピュートリソースの一部を、お客様のロジックやコードの実行に活用してはどうか、ということです。私たちは長年にわたってこのアイディアを追求してきました。そして現在では、Edge コンピューティングのオプションとして、バランスの取れたポートフォリオをご用意しています。
2つのオプションがあります。700以上のEdgeロケーションというインフラストラクチャの観点から見ると、CloudFront Functionsの形式でコードを実行できます。これにより、ビューワーにできるだけ近い場所で、短時間で軽量な関数を実行することが可能です。この機能を補完するものとして、関数をKeyValueStoreと関連付けることもできます。これによってアプリケーションデータをアプリケーションロジックから切り離しつつ、そのアプリケーションデータを同じEdgeロケーションにプッシュして、コード自体から即座にアクセスできるようになります。
もう1つのオプションは、Regional Edge Cacheロケーションのレベルで利用できるLambda@Edgeです。これは基本的にLambdaサービスのクラウド拡張版です。AWSのサーバーレスコンピューティングサービスであるLambdaを使用したことがある方なら、コードの開発や運用面で同様の開発者体験が期待できます。Lambda@Edgeの特徴的な点は、13のRegional Edge Cacheロケーションすべてにコードを自動的にレプリケートすることです。CloudFront Functionsとは対照的に、Lambda@Edgeはより複雑なロジック、より長い実行時間、サードパーティAPIへのネットワークコール、さらにサードパーティライブラリの追加もサポートしています。
CloudFrontのイベントトリガーとエッジコンピューティングの選択
ここでEdge Computeを導入したことで、私たちは主にイベントベースの実行パラダイムを扱うことになります。これは、関数が何らかのイベントトリガーに応答して呼び出されるモデルです。CloudFrontのコンテキストでは、すべてのイベントトリガーは、ビューワーとCloudFront上のアプリケーション間の単一のトランザクションにおける、リクエストとレスポンスのフローに厳密に関連付けられています。より具体的には、そのリクエスト-レスポンスフローの各ステップを表すプラットフォーム定義のイベントトリガーが存在します。
リクエストパスには、CloudFrontがViewerからリクエストを受け取った後の最初のステップに相当するViewer Requestがあります。重要なのは、このステップがキャッシュキーの計算よりも前に発生するということです。そのパスの反対側には、Origin Requestがあり、これはリクエストがOriginに転送される直前にコードを実行します。つまり、このトリガーはキャッシュミスが発生した後にのみ実行されるということです。レスポンスパスに切り替えると、予想通り順序が逆になります。Originからレスポンスを受け取った直後にレスポンスを処理するOrigin Responseロジックがあり、その後、レスポンスがViewerに送信される直前にトリガーされるViewer Responseがあります。 ご想像の通り、これらのイベントトリガーは、それぞれに適用可能なユースケースの種類を大きく決定づけます。これらのイベントトリガーについて、知っておくべき細かな注意点がいくつかあります。Origin向けのトリガーはLambda@Edgeでのみ利用可能であることを覚えておいてください。Viewer向けのトリガーでは、CloudFront FunctionsとLambda@Edgeのどちらかを使用できますが、両方を混在させることはできません。
もしソリューションで両方のレスポンストリガーを使用する必要がある場合は、一つのオプションに統一して進める必要があります。
これまでに学んだことを考慮すると、いくつかの重要な決定を行う必要があります。イベントトリガーを選択し、CloudFront FunctionsとLambda@Edgeのどちらを使うかを決定する必要があります。これらの決定は、ソリューションのパフォーマンス、スケール、コストプロファイルに大きな影響を与えます。私がいつもお勧めするのは、まず両方のオプションのサービス制限を確認することです。仕様を細かく調べることは設計プロセスの中で最も面白い部分ではないかもしれませんが、これらの詳細は各オプションが最も適しているユースケースの種類を如実に物語っていると考えています。制限については後ほど詳しく説明しますので、ここでは一旦置いておきましょう。
制限は考慮すべき点の一つに過ぎず、二つ目の、おそらくさらに重要な点は、関数がどのようなアクションを実行できるかということです。ここで言うのは、コード自体で何ができるかということではなく、関数の出力がCloudFrontの次のステップをどのように決定するかということです。これは関数のライフサイクルに関係してくるので、もう少し詳しく見ていきましょう。
関数が呼び出されると、パスの方向に応じてRequestまたはResponseイベントが入力として受け取られます。Requestパスの場合、これは基本的にヘッダー、クエリ文字列、Cookieなど、リクエストのすべてのプロパティを詳細に示すイベントオブジェクトです。Lambda@Edgeでは、リクエストのボディにもアクセスできます。 Responseイベントでは、ステータスコードやヘッダーなどのレスポンス属性を確認できます。あまり明確ではないかもしれませんが、Responseイベントでは、同じトランザクションのRequestの詳細にもアクセスできます。これは、ユースケースが元のレスポンスだけでなく、そのレスポンスにつながったリクエストにも依存する場合に非常に便利です。
一般的な関数のライフサイクルでは、入力を受け取り、コードが処理を実行し、そして CloudFront がリクエストやレスポンスを進めるために、関数として何かを返す必要があります。関数が生成できる出力には2つのタイプがあります。1つ目は、リクエストまたはレスポンスイベントを返すことです。CloudFront がこれを受け取ると、通常の標準的なワークフローに従って処理を進めます。このオプションが興味深いのは、イベントレスポンスやリクエストオブジェクトに加えられた変更が、基となるオブジェクトに反映されることに気づいた時です。これは CloudFront のEdge computeの主要な機能の1つで、リクエストとレスポンスをその場で修正することができます。 さらに興味深いのは、リクエストパスでは、リクエストイベントに Origin オブジェクトが含まれており、CloudFront の設定に従ってリクエストがどの Origin にルーティングされるべきかが示されていることです。同じように、ドメイン名を上書きすることでルーティング先を動的に変更でき、関数で指定した任意の Origin にリクエストを送信することができます。
2つ目のオプションは、レスポンスやリターンイベントを返す代わりに、関数内でカスタムレスポンスを生成することです。これにより、CloudFront は対応する HTTP レスポンスを生成し、直接ビューワーに送り返します。適用されるトリガーによって、その影響は異なります。リクエストパスでは、基となるリクエストは終了し、それ以上処理されません - つまり、確実に Origin には到達しません。Origin 向けのトリガーでそのレスポンスを返す場合、そのレスポンスはキャッシュの対象となります。これは非常に有用な特性です。なぜなら、関数からレスポンスを一度だけ生成してキャッシュに保存し、後続のリクエストで再利用できるからです。
AppsFlyerのユースケース:User AgentとOrigin選択の最適化
Edge functionsの機能と CloudFront との連携について、より深い理解を得たことで、 Edge functions で解決できる適切な問題を特定できる立場になりました。私は、これらのユースケースをすべて収集し、特定のイベントトリガーごとにグループ化しました。これらは、ほとんどの場合、目的に適していることが証明されています。
リクエストパスから始めると、多くのユースケースで Viewer request が唯一の賢明な選択肢であることがよくあります。これは、多くのユースケースで最も重要なのが、リクエストがキャッシュに到達する前にリクエストを前処理する機能だからです。例えばアクセス制御の場合、厳密な実行順序が必要です。未認証のリクエストがある場合、そのリクエストがキャッシュに到達する前にブロックする必要があります。この順序を守ることができなければ、保護されたコンテンツをキャッシュに置くことすらできません。
次のいくつかのユースケースでは、前のスライドで説明したように、特にキャッシュスキームの一部である属性を動的に変更できることが最も重要です。関数からキャッシュを制御する機能により、レスポンスのより効果的なキャッシングを実現できます。また、オーディエンスをセグメント化し、特定のグループ向けに最適化されたオブジェクトのバージョンに透過的に誘導することもできます。レスポンスレンダリングは、Viewer requestを終了させて独自のレスポンスを生成するすべてのユースケースを指します。要件によっては、特にそのレスポンスをキャッシュに保存して他のユーザーが頻繁に再利用できる場合、Origin requestよりも Viewer request の方が適している場合があります。
最後のユースケースは、Origin Routingです。これは、CloudFrontが提供するパスパターンベースのネイティブな機能を超えて、より高度なOrigin選択パターンを定義する機能です。このユースケースは以前はOrigin Routingに限定されていましたが、最近の発表をご存知の方は、CloudFront Functionsを使用してOriginをその場で変更することも可能になったことをご存じでしょう。レスポンスパスでは、多くのユースケースがOriginの不足しているロジックを補完することに関連しています。サードパーティのCMSを使用している場合、ステータスコードやヘッダーの返し方を完全にコントロールできないことがあります。そのため、このパスでの一般的なアクションは、Cache-Controlヘッダーの追加やセキュリティヘッダーの追加などの修正です。 非常に多くの場合、レスポンスパスの機能はリクエストパスのロジックを補完するものです。例えば、バリアント選択ユースケースでセッションの永続性を示すには、多くの場合、セッションの永続性を確保するCookieの設定が必要になります。
では、これらを実践に移して、シンプルなシナリオから始めてみましょう。 ニュースサイトを運営しているとします。アーキテクチャはできる限りシンプルです。サードパーティのCMSから記事を公開し、CloudFrontが配信とパフォーマンスの管理を担当しています。Originサイドでは、画像リクエストパラメータに基づいて、フォーマットと解像度を最適化する画像変換エンドポイントもホストしています。
ここで、年次計画プロセスの最中で、来年取り組みたい主要な施策を設定したとしましょう。 まず、画像変換エンドポイントを悪用する悪意のある行為者への対策です。彼らはキャッシュを攪乱させており、ランダム化された入力でリクエストを大量に送信することでキャッシュミスを増加させ、Originへの負荷を高め、最終的にサービスの中断を引き起こしています。2番目の施策はビジネスサイドからの要望で、サブスクリプションベースのサービスを導入することです。これは技術的には、ペイウォールの設定を意味します。3番目も同じくビジネスサイドからの要望で、プロダクトチームが実験を定義して開始できるシンプルなインターフェースを通じて、A/Bテストなどの実験をより良く実行できるようにすることです。
4番目にして最後の要件は、SEOランクの改善です。私たちは、ランクが低い原因の一部が、使用しているCMSによって決められた生のURL形式にあることを知っていました。検索エンジンは説明的なURLを高く評価するため、ユーザーフレンドリーなURLを導入する必要があります。
これらの要件に一つずつ取り組んでいきましょう。 まず、不正なリクエストから画像変換エンドポイントを保護することから始めます。この攻撃ベクトルについてより慎重に考えると、攻撃者はキャッシュを構成する属性に対する保護がないことを悪用していることがわかります。これにより、彼らは効果的にキャッシュをバイパスしてOriginに直接アクセスすることができます。この場合の対策は明確です - キャッシュの正規化を適用する必要があり、これは単純で自己完結的なロジックであるため、Viewer Request CloudFront Functionsを使用することになります。関数のコードでは、 様々な画面サイズの解像度入力に典型的な離散値をすべてリストアップする必要があります。通常とは異なる値のリクエストが来た場合、関数は そのリストの中から最も近い値に置き換えることができます。結果として、可能なキャッシュキーのバリエーションの数を制限し、キャッシュ攪乱の問題に対処することができます。
次に残りの要件を見ていきましょう。Paywallの追加は、基本的にアクセス制御手法の追加を意味します。これは顧客から非常によく要望される機能で、通常はToken-basedの認証を実装することを選択されます。このアプローチでは、認可されたクライアントが暗号で署名されたTokenを保持していることを前提としています。私たちの関数は門番の役割を果たし、そのTokenを検証し、署名が正しいことを確認し、視聴者のリクエストが認可された国やASNから来ているかなど、Tokenに含まれるアクセス条件が満たされているかを確認する必要があります。
Tokenizationは優れたソリューションですが、完璧ではありません。Tokenは漏洩する可能性があるためです。この対策として、CloudFrontのアクセスログの異常値分析を実行し、どのTokenとそれに関連するアカウントが侵害されているかを特定することができます。これらのアカウントをKey-Value Storeに保存された失効リストに追加し、誰かが同じTokenを再利用しようとした場合、関数は関連するアカウントがそのリストに載っているかを確認し、そのようなリクエストをブロックします。
SEOクローラーのことも忘れてはいけません。Token-basedのアクセス制御を導入したことで、SEOクローラーは自身を認証してTokenを提示することができなくなります。ここでAWS WAFのBot Control機能が役立ちます。Bot Control機能はSEOボットのトラフィックを識別し、なりすましトラフィックでないことを確認し、適切な秘密ヘッダーでそのトラフィックにマークを付けます。これを関数で検知することができます。そして、SEOクローラーからの検証済みトラフィックであることを確認できれば、検証ステップをスキップする例外処理を行うことができます。
URLからのページバリアントの実装に関して、これはKey-Value Storeの完璧なユースケースとなります。プロダクトチームがページ設定をプッシュするためのシンプルなインターフェースとして機能するのです。ページリクエストが来た時、関数はその設定を読み取り、ユーザーフレンドリーなURLをCMSが理解できる元のURLにすぐに変換する方法を把握します。A/Bテストやバリアントテストが設定されている場合、関数はサイコロを振るように、どのバリアントのページを視聴者に提供するかを決定します。Viewer Request関数がどのバリアントを提供するかを決定する際、一貫性のために視聴者をそのバージョンに留めておきたいと考えます。視聴者が戻るボタンを押したりページをリロードしたりする際に、常にバージョン間を行き来させたくはありません。
この問題に対処するために、Viewer Responseのロジックを拡張し、セッションの固定化を実装する必要があります。最も簡単な方法は、Cookieを追加することです。Viewer Response関数では、視聴者が固定すべきページのバージョンを決定する特定の値をCookieとして設定します。前のスライドで説明したように、Viewer Request関数はその特定のCookieを探し、Cookieが存在する場合は再度サイコロを振るのではなく、視聴者を直接ターゲットバージョンのページに誘導します。
Viewer Response Functionが、サイコロを振った最初の結果をどのように知るのか疑問に思われるかもしれません。Viewer Response Eventのトリガーには、同じトランザクションに関連するリクエストの詳細も含まれているということを思い出してください。Viewer Responseの観点からは、Viewer Request Functionを実行した結果を確認することができ、これを活用しているのです。この仮想的ではありますが現実的なシナリオを通じて、AppsFlyerの実際のユースケースと、最も重要な具体的な成果について理解を深める良いウォームアップになったと思います。
AppsFlyerのエッジコンピューティング活用事例と得られた効果
まず初めに、この重要なトピックについてAppsFlyerのユースケースを発表する機会を与えてくださった皆様とDavidに感謝申し上げます。正直に申し上げますと、これが私にとって初めてのイベント登壇となります。私はDannyと申しまして、AppsFlyerで複数の開発グループを率いているエンジニアリングリーダーです。これらのグループは、すべての受信データの処理、エンドユーザーへのレスポンス、メインビジネスコアのアトリビューション、そして様々なチャネル(パートナーや顧客)へのデータ送信を担当していますが、主にそのデータを基にプロダクトを構築するダウンストリームのコンシューマーへのデータ提供を行っています。
AppsFlyerについてよくご存じない方のために、簡単に説明させていただきます。私たちは、様々なチャネルで実施されているマーケティングキャンペーンの効果を測定するデジタルプラットフォームです。チャネルと言いますと、Meta、Google、Amazon、TikTokなど、皆さんご存知のプラットフォームが該当します。私たちは、ユーザーがアプリとどのように関わっているかを実質的に測定し、その後のアクション - アプリのインストール、アンインストール、あるいはアプリオーナーがROIを向上させるために測定したい重要なインアップアクションなど - を追跡します。皆さんのスマートフォンを見ていただくと、多くのアプリがありますが、その中でAppsFlyer SDKは一度ならず組み込まれている可能性が高いですね。これは私たちのフットプリントを示しています。世界中の数十億台のデバイスで利用されているわけですが、Edge Computingについて話す上で、このデータの耐障害性とAWSがこのデータを受け取る方法に対する期待は非常に重要な点です。
残りの時間で覚えていただきたいキーポイントをお話しさせていただきます。まず第一に、Edge Computingの強みと、AppsFlyerがそれをどのように活用しているかということです。第二に、これが私たちのアーキテクチャからデカップルされているという事実です。ちなみに、昨年6月にCDNから別のクラウドプロバイダーに受信データの90-95%を約1.5ヶ月かけて移行しました。基本的にアーキテクチャは何も変更せず、ビジネスをサポートできるEdge Computingの機能を活用しました。
私たちがこれを行っているのは、AWSが素晴らしいからではなく、ビジネスに価値をもたらすからです。ビジネスに価値がもたらされれば、お客様は満足し、アプリケーションも満足し、私も満足します。これから紹介するユースケースを通じて、これら3つのポイントをサポートしていきたいと思います。
AppsFlyer について語る際、Productionは避けて通れない言葉であり、その意味するところは非常に大きいものです。数字を強調したいと思います。なぜなら、コンテキストを理解する上で非常に重要だからです。システムコンポーネント全体を見ると、そのデータ量の大きさが分かります。1日あたり約2,200億のインプレッションがあります。インプレッションとは表示回数のことで、クリックやイベントよりも若干少なめです。これは約1秒あたり250-260万リクエストに相当します。これは活発な実稼働環境であり、AppsFlyer においてProductionは重要な考慮事項となっています。これらの数字を挙げることが重要なのは、私たちがエッジで運用しているからです。
それでは、ハイレベルアーキテクチャの詳細を見ていきましょう。データは様々なプラットフォームから到着します。デバイスや、様々な種類のデバイス、TV、コンソール、そして業界で使用されているAmazon Route 53を実行しているサーバーからデータが届きます。CDNプロバイダーからクラウドに移行する際、ほとんどの配信システムにはAWS WAFが付属しています。2番目のユースケースでは、セキュリティ目的だけでなく、ビジネスの観点からどのように活用しているかをお見せします。
そこからデータはApplication Load Balancerに移動し、インスタンスへと進みます。ここで言うインスタンスとは、データを処理する最初のマイクロサービス層であるWebハンドラーのことで、ユーザーをストアにリダイレクトし、メインビジネスを処理する他のコンポーネントへ下流に送る際のロジックがすべて含まれています。最初のユースケースでは、User Agentに焦点を当てます。User Agentの意味、Client Hintsへの対応方法、GoogleがUser Agentについて導入したこと、そしてプライバシーの懸念にどう対応しているかを説明します。
2番目のユースケースでは、AWS WAFとCloudFront Functionの連携、そしてオリジン選択に関するビジネスをどのように活用しているかについて説明します。これはUser Agentの取得方法だけでなく、このデータを送信している主体を特定する方法も含みます。モバイルデバイスかもしれませんし、モバイルデバイスの代わりに動作しているサーバーかもしれません。どのようなインタラクションも、様々なブラウザーやソーシャルアプリから到着する可能性があり、それぞれに応じた異なる処理ロジックが必要です。最終的には、これらのインタラクションからUser Agentを取得する必要があります。
GoogleがClient Hintsを導入するまでは、従来の方法でクリック時にUser Agentを受け取り、インプレッション時に確認して取得していました。User Agentは私たちにとって2つの重要な理由があります:適切なストアにユーザーを誘導すること、そしてアトリビューションロジックでクレジットの付与先とユーザーの来訪元を判断することです。GoogleがClient Hintsを導入した現実では、User Agentは完全には利用できなくなるか、部分的にしか利用できなくなります。ビジネスにとって重要な情報であるため、不足しているUser Agent情報の提供をクライアントに明示的に要求する必要があります。
たとえば、Googleがクライアントヒントを導入する前のバージョンでは、緑色で表示されているすべてのデータ、つまり私たちのビジネスにとって重要なAndroidデバイス、OSバージョン、デバイスモデル、Chromeバージョンなどの情報を受け取ることができました。現在では、これらの情報は部分的にしか取得できず、データを補完するために明示的なリクエストが必要になっています。この課題に対する単純なアプローチと、CloudFront Functionを活用した解決方法についてご説明します。
この図を見れば一目瞭然です。モバイルでクリックが発生すると、CloudFront、Application Load Balancer、そしてインスタンスへと到達します。Googleがクライアントヒントを導入する前と異なり、現在ではUser Agentを直接取得することができません。そのため、これを取得して理解するためのロジックを追加する必要があります。まず、ビジネスにとって重要なUser Agent情報のうち、どの情報が不足しているかを特定します。不足している情報を特定したら、それをクライアントに送り返します。クライアントはそのリクエストを受け取り、私たちの運用に不可欠な拡張されたUser Agentで応答します。実際のところ、このラウンドトリップが大きな問題となっています。
その後、リダイレクトを行います。Webサイトでは、User Agentが不足していることを識別し、クライアントとサーバー間のすべてのやり取りを管理するロジックを追加する必要があります。これは課題に対する解決策ではありますが、管理が必要で、かつラウンドトリップが問題となるため、理想的とは言えません。
そこで、すべてのロジックをエッジに移行することにしました。 やり取り自体は依然として必要ですが、エッジ配信に基づいているため、ユーザーにより近い場所で処理が行われます。クリックが発生すると、Webベンダーに転送する代わりに、Functionが不足しているUser Agentを識別し、User Agentを補完するようクライアントにリクエストを生成します。補完された情報を受け取ると、それを私たちのサーバーに転送します。実質的に、サーバーは最初から拡張されたUser Agentのみを受け取るため、Googleがクライアントヒントを導入する前と同じように動作します。これによって、ラウンドトリップの問題とサーバー管理のロジックの問題が解決されました。
AppsFlyerにとってのメリットをまとめますと、まず、レイテンシーが300〜500ミリ秒削減され、約30%もの大幅な改善が実現しました。ユーザーはフローを中断することなく、素早くストアに到達できるようになりました。ユーザーが満足し、お客様も満足し、私たちも満足しています。エンジニアリングの観点からは、ロジックを完全に分離してエッジに移行することができました。リダイレクトフローで必要だった複雑さを追加する必要がなくなりました。
2番目のユースケースは、User Agentの取得方法だけでなく、オリジンの選択方法に関するものです。リクエストの送信元を特定することは、私たちにとって非常に重要です。クライアントからUser Agentを取得できますが、 同じUser Agentを持つクライアントの代わりにサーバーから取得することもできます。オリジンの特定が重要な理由は、オリジンを特定できないと、実際には不要なサーバーにリダイレクトを行ってしまうからです。さらに、リダイレクトはより多くのコンピューティングリソースを必要とするため、より多くの処理能力が必要になります。
サーバーなのか、同じUser Agentを持つクライアントなのかを識別するために、「リダイレクトは不要、サーバーサイドでクライアントではない」ということを明示的に示すクエリパラメータを有効にしています。しかし、これはオプションであり、すべてのマネタイゼーションネットワークやパートナーにこれを強制することは現実的ではありません。つまり、コントロールは私たちの手中にはなく、パートナーがリダイレクトを必要とするかどうかの申告次第なのです。 1分間に5,000万件のリクエストを処理するには、約160個のKubernetes Podsが必要です。実際には不要なリダイレクトを行っているため、パートナー次第でより多くのコンピューティングリソースが必要になってしまいます。
以前と同様に、データは様々なデバイスからRoute 53を通じて到着します。しかし、クエリパラメータに依存する代わりに、サーバーとクライアント用に定義したレートルールを追加しています。リクエストをカウントし、レートに基づいてクライアントかサーバーかのタグを付けています。そのデータはALBに書き込まれ、ALBではヘッダーを読み取って関連するインスタンスに転送するルールを追加しました。ルールに基づいてS2SとC2Sの両方に対応する複数のALBがあり、どのタイプのインスタンスにルーティングする必要があるかが分かります。これで問題は解決しましたが、同じWAF、同じルール、同じレートルール、同じカウント、同じタグ付けを使用してさらに改善することができます。
しかし、さらに良いアプローチは、ALBに移行してルールの複雑さを増やす代わりに、Cloud Functionを使用することです。Cloud Functionがヘッダーを読み取り、それに基づいて適切なTLBを選択します。今回は、異なる種類のフリートインスタンス用に異なるTLBを用意し、ALBにルールを追加することなく同じ問題を解決しています。ヘッダーを読み取り、Cloud Functionで行ったタグ付けに基づいてALBにルーティングし、適切なインスタンスに到達します。
AppsFlyerにとってのメリットは何でしょうか?オリジンの負荷を軽減できました。 これらのコンポーネントはもう必要ありません。なぜなら、サーバーとそうでないものを区別できるようになったからです。リダイレクトは非常にコンピューティング集約的です。実際、精度を除けば、サーバーには200 OK、リダイレクトには301を返すようになりました。以前は160個必要だった5,000万件の処理に、現在は95個で済むようになり、約40%削減されました。もちろん、コンピューティングリソースが少なくなれば、お客様も喜び、コストも削減でき、クラウドコンピューティングのコストも節約できます。
Lambda@EdgeとCloudFront Functionsのスケーリングと制限
これが2つのユースケースです。1つ目はClient Hintsで、主にレイテンシーを削減し、エンジニアリングを簡素化してくれるUser Agentについてです。2つ目はWAFとCloud Functionの連携により、オリジンの負荷を軽減してコストを削減できた例です。以上です。ありがとうございます。Danny。皆さん、聞こえていますか?はい、大丈夫ですね。
素晴らしい事例でしたね。私がこの事例で特に気に入っているのは、エッジで利用可能な様々な機能を活用してロジックをエッジに移行するという創造的なアプローチです。そして、彼らがそれを実施している規模は本当に驚くべきものです。自己紹介させていただきますと、私はDavidで、CloudFrontチームのPrincipal Product Managerを務めています。Edge Computingの活用を検討されているお客様から最もよく聞かれる質問の1つは、自社のワークロードに対応できるスケールがあるかどうかということです。Dannyの事例が示すように、私たちは恐らくお客様のニーズに対応できるスケールを持っています。
どのようなプラットフォームでも制限は必ずありますので、その制限を理解しておく必要があります。制限を理解すればするほど、お客様のニーズに対応できるかどうかを判断しやすくなります。これらの制限の例と考慮すべき点について見ていきましょう。まずはLambda at Edgeから始めます。Lambda at Edgeのスケーリングに関して考慮すべき制限は主に2つあります。1つ目は同時実行数に関するもので、Lambdaをご利用の方はおそらく馴染みがあると思います。これは任意の時点でLambdaが関数を実行するために利用可能なインスタンス数のことです。同時実行数の上限はリージョンごとに設定されており、そのリージョンで実行するすべての関数がこの制限にカウントされます。
同じリージョンでCloudFrontやLambda Edge、そしてLambda関数を実行している場合、これらは全てそのリージョンの同時実行制限にカウントされます。また、あまり知られていませんがLambda Edgeに大きな影響を与えるもう1つの制限として、1秒あたりのリクエスト数、つまりトランザクション数があります。これはリージョンの同時実行制限の10倍になります。つまり、あるリージョンで同時実行制限が1,000の場合、そのリージョンのLambda関数で処理できるのは1秒あたり10,000リクエストということになります。
フロントエンドアプリケーションの開発者として、Webアプリケーションに入ってくる1秒あたりのリクエスト数はご存知かもしれません。しかし、それが実際にLambdaの同時実行数とどのように関係し、どうすれば1秒あたりのトランザクション制限を超えないようにできるのでしょうか?計算式自体はシンプルですが、本当に理解しておく必要があるのは、関数自体の平均実行時間です。例として、あるリージョンでの最大同時実行数が1、つまりコンピューティングに利用できるLambdaインスタンスが1つだけだとします。トランザクション制限がその10倍だとすると、1秒あたり10リクエストを処理できることになりますが、Lambdaは1つのインスタンスで一度に1つの関数しか実行できません。そのため、1つのインスタンスで1秒あたり10リクエストを処理するには、関数の実行時間が100ミリ秒以下である必要があります。
リクエスト毎秒(RPS)と同時実行数の間の変換における魔法の数字は100ミリ秒です。関数の平均実行時間が100ミリ秒未満の場合、RPSを10で割るだけで必要な同時実行数が分かります。これによってLambdaの同時実行数制限と1秒あたりのトランザクション制限を超えないようにできます。関数の実行時間が100ミリ秒を超える場合は、トランザクション毎秒の制限に到達することはありません。その場合は、RPSに実行時間(秒単位)を掛けることで、必要な同時実行数を算出できます。
例を見てみましょう。 CloudFront distributionで、Origin Request関数を使用していて、1秒あたり50,000リクエストがCloudFrontに到達している状況を想定します。Origin Request関数の場合、キャッシュミスが発生した時のみ実行されます。キャッシュヒット率が92%(これはかなり良好な数値です)だとすると、リクエストの8%のみがLambdaで実行されることになり、これは約4,000 RPSに相当します。平均実行時間が200ミリ秒で、これは100ミリ秒の閾値を超えています。必要な同時実行数を計算するには、4,000に0.2秒を掛けて、800という数値が得られます。
もう一つの例を見てみましょう。 Viewer Request関数の場合、すべてのリクエストで実行されるため、キャッシュヒット率は関係ありません。同じように1秒あたり50,000リクエストがアプリケーションに到達すると仮定します。この場合、関数の実行時間は50ミリ秒で、100ミリ秒の閾値を下回っています。したがって、50,000を10で割るだけで、このアプリケーションをサポートするために必要な同時実行数は5,000となります。
CloudFront Functionsに目を向けると、同時実行数やスケーリングの制限はありません - どのようなトラフィック量でもスケールします。ただし、CloudFront Functionsには関数の実行可能時間に制限があります。Compute Utilizationというメトリクスが提供されており、これは関数の実行に許可された総時間に対する実際の実行時間の割合を示します。最大許容時間があり、それを超えると関数がスロットリングされる可能性があります。 ここにCompute Utilizationを時系列でプロットしたグラフがあり、このグラフのどこかに最大許容時間が示されています。多くの関数実行がこの最大許容時間以内に収まっていることが分かります。しかし、Compute Utilizationが100%まで急上昇し、 最大許容時間を超えたケースもいくつか見られます。
スロットリングを行うかどうかを判断するために、CloudFrontは関数実行の単純移動平均を計算します。 直近50回の関数呼び出しを取得し、単純移動平均を計算します。この単純移動平均が最大許容時間を下回っている場合、関数はスロットリングされません。この例では、最大許容時間を超える関数実行が何度かありましたが、単純移動平均がその範囲内に収まっていたため、それらの関数は実際にはスロットリングされることなく正常に実行されました。これにより、アプリケーションに影響を与えることなく、散発的な実行時間のスパイクを許容することができます。
エッジコンピューティングのベストプラクティスとパフォーマンス最適化
関数には時間制限がありますが、その制限の中で最大限の効果を得るにはどうすればよいでしょうか?制限内に収めるために、関数をどのように書けばよいのでしょうか?いくつかのベストプラクティスをご紹介します。まず1つ目は、関数とLambda@Edgeの間のスタイルの違いです。Lambda@Edgeではグローバル変数は素晴らしい機能で、積極的に使用すべきです。一方、CloudFront Functionsではグローバル変数は機能しません。グローバル変数とは、イベントハンドラーのコンテキスト外で設定される変数のことです。関数インスタンスを再利用する際に、すでに保存した状態や他の変数を後続の呼び出しで再利用したい場合にグローバル変数を使用します。しかし、Functionsの場合はそれが機能しません - CloudFront Functionを呼び出すたびに、新しいインスタンスが生成されます。
つまり、異なる呼び出し間で状態を保持することはできません。コード内のグローバル変数の属性を取り除くことで、コードのスペースを節約し、最適化することができます。
2つ目のベストプラクティスは、正規表現に関するものです。正規表現は非常に強力で、文字列マッチングに優れた方法です。しかし、関数内では、多くの場合かなりシンプルな文字列マッチングを行っています。ここでは、入力されるホストヘッダーが特定のドメイン名と一致するかどうかを確認する例を示しています。JavaScriptには、このincludesステートメントのように、自動的にそれを行う多くの組み込み関数があります。ネイティブのJavaScript関数を使用する方が、正規表現を使用するよりもパフォーマンスが向上します。正規表現は使用できますが、関数内での使用方法とタイミングは慎重に選択する必要があります。
KeyValueStoreを使用する場合、これは重要なポイントです。多くの人がやっているのを見かけるのは、大量のデータを1つのキーバリューペアに詰め込み、関数内で単一の読み取りを行ってそのデータを取得することです。その後、その大きな設定データの塊から必要なデータを取り出すためのJSON解析ロジックを書く必要があります。KeyValueStoreの仕組みとしては、提供されたデータセット全体を関数サンドボックス自体のメモリに取り込みます。KVSで読み取りを行う場合、基本的にはメモリから読み取るだけなので、非常に高速です。多くの場合、すべてを1つのキーバリューストアに詰め込んで複雑な解析を書くよりも、KVSに対して複数回の読み取りを行ってデータを取得する方が効率的です。したがって、KeyValueStoreを使用する場合は、関数内での解析を減らせるようにデータ構造を設計することで、より良いパフォーマンスと低いCompute Utilizationを実現できます。
Lambda@Edgeについて話しましょう。Lambda@Edge関数を書く際、私がいつも最初に人々に伝えることは、異なるメモリ設定で試してみることです。ほとんどの場合、最低メモリ設定である128メガバイトで十分です。しかし、時には少し余分に支払うことで、関数からはるかに良いパフォーマンスを得られることがあります。これは、DynamoDBを呼び出し、そのデータを関数内で加工する例です。関数のメモリ割り当てを128メガバイトから256メガバイトに増やすことで、パフォーマンスが約40%向上することがわかります。コスト面では約20%の増加が発生します。これらのトレードオフを考える必要があります - 40%のパフォーマンス向上は20%のコスト増に見合うでしょうか?場合によりけりです。しかし、関数を計画してLambda@Edgeの活用を検討する際は、必ず異なるメモリ割り当てを確認してください。それによって価格とパフォーマンスの比率が変わる可能性があります。
Lambda@Edgeを利用されているお客様からよく耳にする問題の1つが、コールドスタートに関するものです。Lambdaは、以前の呼び出しで使用したインスタンスを再利用することに関しては非常に効率的です。しかし、時にはスケールアウトが必要になり、その際にコールドスタートが問題となることがあります。この問題を軽減する1つの方法として、異なるイベントトリガーで実行される複数の関数を1つの関数にまとめるという方法があります。 CloudFrontは、Lambda@Edge関数に渡すイベントオブジェクトの中で、実際にどのイベントトリガーが実行されているかを教えてくれます。関数内でロジックを設定し、Viewer Requestの場合はこのロジックを実行し、Origin Request関数の場合は別のロジックを実行するといった具合に制御できます。すべてを1つの関数にまとめることの利点は、単一の関数に対する呼び出しが増えることです。1つの関数への呼び出しが増えると、すでにウォームアップされたインスタンスのプールが大きくなり、複数の関数に分散させる場合と比べてインスタンスが再利用される可能性が高くなります。関数が十分小さく1つにまとめられる場合、これはコールドスタートを減らすための賢い方法です。
最後に、外部ネットワーク呼び出しについて触れたいと思います。多くの方がLambda@Edgeを使用して、データベースやS3、あるいは任意のHTTPエンドポイントなど、様々なソースへのネットワーク呼び出しを行っています。これらのライブラリの多くには、接続を再利用する機能が備わっています。Lambdaはウォームインスタンスの再利用が得意で、ウォームインスタンスを再利用する場合、その接続がすでに確立されており、関数に接続の再利用を指示する限り、その接続を利用できる可能性が高いのです。 これは、HTTPライブラリで接続を維持する時間を指定するオプションの例です。これを使用することで、リクエストごとにTCP接続を確立するオーバーヘッドを削減できます。
セッションのまとめと結論
では、まとめに入りましょう。今日は、CloudFront FunctionsとLambda@Edgeの様々なユースケースと、それぞれをどのような場合に使用すべきかについて説明することから始めました。また、AppsFlyerの皆様から、エッジコンピューティングソリューションを創造的に活用し、非常に大規模なビジネス課題を解決する素晴らしい事例をご紹介いただきました。両プラットフォームの制限についても説明し、アプリケーションを適切にスケールさせるためにそれらの制限にどう対応するかについて話し合いました。最後に、プラットフォームから最高のパフォーマンスを引き出すためのコード最適化のベストプラクティスについて説明しました。本日は、お時間をいただき、ありがとうございました。私とKamilを代表して、このセッションをお楽しみいただけたことを願っています。re:Inventの残りのセッションもお楽しみください。ありがとうございました。
※ こちらの記事は Amazon Bedrock を利用することで全て自動で作成しています。
※ 生成AI記事によるインターネット汚染の懸念を踏まえ、本記事ではセッション動画を情報量をほぼ変化させずに文字と画像に変換することで、できるだけオリジナルコンテンツそのものの価値を維持しつつ、多言語でのAccessibilityやGooglabilityを高められればと考えています。
Discussion