モバイルゲームのコンテキストで考える Lambda@Edge


[AWS & Game Advent Calendar 2020](https://qiita.com/advent-calendar/2020/aws-game) の 6 日目の記事です。

## Lambda@Edge 使ってますか

> Lambda@Edge （以下 L@E）を使用すると、サーバー管理を何も行わなくても、ウェブアプリケーションをグローバルに分散させ、パフォーマンスを向上させることができます。L@E は、Amazon CloudFront コンテンツ配信ネットワーク (CDN) によって生成されたイベントに対応してコードを実行します。コードを AWS Lambda にアップロードするだけで自動的にコードの実行やスケーリングが行われ、エンドユーザーに最も近い AWS ロケーションでの高可用性が実現します。

https://aws.amazon.com/jp/lambda/edge/ より引用

Amazon CloudFront （以下 CloudFront）のイベントをトリガーとインプットにして AWS Lambda （以下 Lambda）の Lambda Function を実行することができる機能です。

* https://aws.amazon.com/jp/cloudfront/
* https://aws.amazon.com/jp/lambda/

モバイルゲームでは通常、画像、動画、音声などの静的なアセットファイルのモバイル端末への配信に CDN がよく使われます。インストール後のアセット初回 DL で大量/大容量のファイルが端末に DL されるようになっておりゲームのリリース直後に激しいネットワーク通信が発生するケースがありますが、そういったときも CloudFront などの CDN を使っていれば安心です。 [^cf-quota] L@E を活用すると、ファイル DL というこの単純（だがスケーラブル）な機能にロジックを持たせたり、さらにそれに応じてリクエストやレスポンスの内容を書き換える、といったことがある程度可能になります。例えば、[こちら](https://aws.amazon.com/blogs/networking-and-content-delivery/adding-http-security-headers-using-lambdaedge-and-amazon-cloudfront/)の例（注：英語）では、レスポンスヘッダにセキュリティ関連のヘッダを動的に追加することでセキュリティを高める、といった使い方が紹介されています。

## モバイルゲームのコンテキストで考える Lambda@Edge

この記事では、**モバイルゲームのよくあるユースケースや要件に沿って L@E の利用シーンをいくつか列挙するかたちで紹介**します。アーキテクチャや実装の詳細は割愛します。参考 URL をリンクしているものもあるので詳細はそちらを参照してください。


### １．プレイヤーのプロフィール画像のサムネを動的に生成したい

え、**サムネなんて、アップロードされたら自動で生成されるように [Amazon S3 の Event](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html#with-s3-example-configure-event-source) を使っているよって？**確かにそれで問題ありませんが、仕様というのは時間とともに変わるものです。必要なサムネのバリエーションが増えたりというのはよくあります。その際に既に存在する元画像全てに対して画像の変換を改めて実施するでももちろん問題ありません。実際にはもう何年も休眠しているプレイヤーの画像に対しては画像の変換と作成は不要かもしれません。L@E による動的な画像生成では、実際に使われる（アクセスのある）画像に対してのみ最初のリクエストで生成しておき以降はそのファイルが参照される、という柔軟な仕組みが可能です。[^generate-thumbnail]

![Slide131-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/51170fad-ecc6-1d81-e435-7c0c8585853a.png)

L@E は、上図の(1)〜(4)に対応したポイントのいずれかを実行タイミングとして指定します。 (1):`Viewer request`, (2):`Origin request`, (3):`Origin response`, (4):`Viewer response` 。詳細は [Lambda 関数をトリガーできる CloudFront イベント - Amazon CloudFront](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html) のドキュメントをどうぞ。

1. クライアント端末から画像への HTTP リクエスト
2. キャッシュがない場合、CloudFront のエッジがオリジンの S3 Bucket へリクエスト
3. 該当のサムネ画像がオリジンにない場合、ステータスコード 400 となるがそれを条件にサムネの生成と S3 Bucket への Put を実行する。以降のアクセスでは、そのサムネに対するオリジンリクエストでは生成されたこのファイルが使用される。
4. クライアント端末へ画像がレスポンスされるとともにキャッシュされる。

参考

* [Amazon CloudFront & Lambda@Edge で画像をリサイズする | Amazon Web Services ブログ](https://aws.amazon.com/jp/blogs/news/resizing-images-with-amazon-cloudfront-lambdaedge-aws-cdn-blog/)[^thumbnail-post]


### ２．バージョンが古いアプリにも対応するようにレスポンスするファイルを動的に変更したい

アプリをバージョンアップする際は後方互換性を意識したり、アプリ起動時にバージョンアップの強制をフックしたりなどはよく行われる実装かと思います。ただ、それだけではどうしても対応ができないこともあります。そうした際にクライアントアプリ側のバージョン情報に応じて自動的に一部ファイルを切り替えるようなことが可能です。例えば設定ファイルのようなものを対象とすれば、古いクライアントアプリにおいてのみ接続先やふるまいを変えるようなことを構成できます。

![Slide132-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/0d2b9b7e-082f-c1e7-ae87-e775bf2a3f65.png)

1. クライアント端末から画像への HTTP リクエスト。クエリストリングや HTTP ヘッダにアプリのバージョン情報が入っている。その値に応じてリクエストする URL パスを書き換える。
2. キャッシュがない場合、CloudFront のエッジがオリジンの S3 Bucket へリクエスト。Viewer Request で URL パスを書き換えている場合はこの Origin Request の URL パスも書き換わっている。
3. 書き換えられた URL パスに応じた、そのアプリ・バージョンに合わせたファイルをレスポンス。
4. 同じくアプリ・バージョンに合わせたファイルをクライアントにレスポンス。

参考

* [Lambda@Edge を使ったゲームプレイヤーへのカスタムコンテンツの配信 | Amazon Web Services ブログ](https://aws.amazon.com/jp/blogs/news/how-to-deliver-custom-game-content-to-players-using-lambdaedge/)


### ３．プレイヤー個別のアセットファイルを管理する

Application Server がプレイヤーに応じて異なるデータを返す場合に事前の認証・認可を必要とする構成は一般的です。静的なアセットにおいても同様のことをしたいときはないでしょうか？例えば、

* ある有料アイテムを課金したプレイヤー、あるいは特別なクエストを攻略したプレイヤー、など条件を満たしたプレイヤーにのみ見せても良い画像や音声のファイルがある。
* 各プレイヤーが独自にカスマイズやデコレーションした「名刺」や「プレミアムカード」のような画像を作成・DL できる機能があるが、毎回画像生成したくはない。かといってノーガードで CDN から配信し悪意のあるユーザが閲覧できてしまう可能性があるのでは困る。

といったようなユースケースです。こういうケースでは静的ファイルに対する認証・認可を L@E で構成すると便利です。

![Slide133-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/2f7fcc25-6dae-557d-6c37-13e05de0ce92.png)


* 事前ステップ： クライアントは Amazon Cognito などで事前に認証（ログインなど）を行い、Json Web Token (JWT) などの認証トークンを予め受け取っておく。

1. クライアント端末からファイルへの HTTP リクエスト。 JWT トークンを Authorization ヘッダや Cookie などに含めておき、 L@E がその正当性を検証し、リクエストされた URL のファイルをレスポンスしてもよいかを判定する。検証に失敗する場合は、キャッシュの有無に関わらず即座に 401 unauthorized をクライアントへレスポンスする。L@E ではレスポンスボディの動的な生成にも対応しています。
2. Viewer request で適切に認証・認可されており、かつオリジンが CloudFront の該当ディストリビューション以外からは参照できないように適切に構成されているなら、ここでの特別な処理は不要です。

参考

* [Authorization@Edge – How to Use Lambda@Edge and JSON Web Tokens to Enhance Web Application Security | Networking & Content Delivery](https://aws.amazon.com/blogs/networking-and-content-delivery/authorizationedge-how-to-use-lambdaedge-and-json-web-tokens-to-enhance-web-application-security/) （英語）
* [Guest post: How Space Ape Games delivers secure WebApps using AWS | AWS Game Tech Blog](https://aws.amazon.com/blogs/gametech/guest-post-how-space-ape-games-delivers-secure-webapps-using-aws/) （英語）


### ４．来週のイベント用の追加キャラの画像を UL しておきたいが、イベント開始前にすっぱ抜かれて公開されると困る

将来のイベント用のアセットを早めに用意しておくと、URL の規則性から見つけられてしまうことがあります。例えば既存のカードの URL が

* .../event/1/card/1.png
* .../event/1/card/2.png
* .../event/1/card/3.png
* ...
* .../event/2/card/1.png
* .../event/2/card/2.png
* .../event/2/card/3.png
* ...

のような Database の サロゲートキーや PK に依存している設計だと、まだ存在しない自然数のうち小さいものを狙えばいいのでけっこう単純です。あるいは以下のようなカード名になっていたり。

* .../card/Knight.png
* .../card/Lancer.png
* .../card/Witch.png
* ...

連番よりは推測しづらいですが、意味のある文字列ではあるのでそのゲームをよく遊んでいる人なら結局はあたりがつけやすかったりします。このような課題に対してよくある既存の解決方法と懸念は以下などです。

1. イベント直前にファイル一式を UL する。  
→ 直前作業は事故のもとですね。できれば早めに作業しておきたいところでしょう。
2. 各データのレコードそれぞれに、**推測困難なランダム文字列**などを登録しておきそれを URL パスに使用する。  
→ 生成と登録の手間はまだしも、対応するファイルのアップロードが面倒なうえに、オリジン上のファイルパスやファイル名からどれが何のファイルなのかがぱっと見では内部の人にも分かりません。

L@E を使うと上記2つのアプローチをどちらもより一歩改善するアプローチが可能です。

1.については、ファイルの UL 自体は事前に終えておきますが、該当の URL 範囲への Origin Request についてはイベント開始日時（の少し前）までは 404 Not Found を返すように L@E を実装します。該当のアセットの URL が `../event/3/*` のように正規表現やワイルドカードなどをつかって　L@E のコード上で指定しやすい場合により使いやすい方法です。
イベント開始後の適当なタイミングで、L@E 上の該当ロジックは削除するデプロイをしてもいいですし、さらに次のイベント用に同様のロジックを追加するときに前のイベントのロジックを削除してもよいです。前者のほうがより L@E の利用料金は抑えられます。

![Slide134-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/e7a57d48-4e24-8afa-7d67-1ab0b3d83a3d.png)

2.については、推測困難な文字列としてランダムな文字列を使用する代わりに、もとのデータを特定する値（PKやカード名など）をもとに変換や暗号化をすることで得られる文字列を使用します。その変換ロジックや共通鍵を L@E 上に持たせておき Origin Request で元の値に戻すように実装します。そうすることで、エンドユーザから見える URL は推測困難ながら、オリジン上では今までと同様に可読性のある URL を維持することが可能です。[^url-obfuscation]

![Slide135-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/810ef3a3-9c60-baf0-a214-7108a99bf4aa.png)

参考

* [Leveraging external data in Lambda@Edge | Networking & Content Delivery](https://aws.amazon.com/blogs/networking-and-content-delivery/leveraging-external-data-in-lambdaedge/)


## まとめ

以上です。もっと色々ありそうですね。あったら教えて下さい。




[^cf-quota]: [CloudFront のクォータ](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-general)のドキュメントの確認と必要に応じた「クォータ引き上げのリクエスト」はお忘れなく。

[^thumbnail-post]: この記事では、(1) Viewer request にもう１つ別の L@E を使用して、クライアントからの自由な画像サイズリクエストを対応可能なサイズバリエーションに落とし込むということが行われています。

[^generate-thumbnail]: また、ここではサムネを例にしていますが、プレイヤーがアップロードする画像の加工全般にあてはまるユースケースです。

[^url-obfuscation]: 共通鍵による暗号化・復号を含めて変換ロジックをエンドユーザが解明してしまう可能性は０ではありません。それを懸念する場合は、やはりランダムな文字列を使用し、それらと元の値のマッピングを DynamoDB などに保持しておき L@E から参照する、という方法も考えられます。 L@E はネットワーク経由で外部のリソースにアクセスすることも可能なのです。