はじめに
皆さんこんにちは、アソビューでエンジニアをしている李です。
アソビューでは、レジャー施設向けのDX支援SaaS「ウラカタチケット」を提供しています。施設運営の効率化からエンドユーザーのチケット購入体験までを支えるプラットフォームです。
最近、このウラカタチケットで「まとめて購入」機能をリリースし、新しい購入体験の提供を開始しました。
この機能は、購入処理の中で複数のAPIを呼び出したり、複数プランの情報を参照したりする必要があるため、従来のフローよりバックエンドに負荷がかかりやすい構造になっています。
この構造のままアクセスが集中すると、すべてのリクエストをそのまま受け入れてはバックエンドが捌ききれなくなる恐れがあります。
そのため、システムのキャパシティに合わせて流量を適切にコントロールする必要があり、その手段として「入場制限」の仕組みを導入しました。
入場制限画面
なお、今回のロジックはゼロから作ったものではなく、社内の別プロジェクトで運用実績のある仕組みをベースにしています。
その上で、「まとめて購入」特有の要件に合わせて必要な部分だけを手直ししました。
実績のある土台を使うことで、設計の迷いを減らしつつ、差分に集中できたのも大きかったです。
この記事では、全体構成と実装上の工夫、そして進める中で得られた学びをまとめます。
システム構成
本機能は全リクエストの入口となるため、低レイテンシと高可用性を最優先に、構成要素は必要最小限に絞りました。エッジでの処理を重くしないことでパフォーマンスを確保しつつ、運用面での不確実性も増やさないことを狙っています。
- CloudFront(viewer-request Lambda@Edge)
- リクエスト受信直後に入場可否を判定
- DynamoDB
- ユーザーの入場状態(admitted / waiting)を TTL 付きで管理
リクエストの流れは次の通りです。
Client → CloudFront → Lambda@Edge → DynamoDB → Origin(API)
CloudFront のエッジで判定を行うことで、 バックエンドに到達する前の段階でリクエストを制御でき、 アクセス集中時でもシステム全体への影響を抑えることを狙いました。
入場制限ロジックの考え方
ロジック自体は、複雑なものではありません。 リクエストを受け取った時点で、そのユーザーが「今、入場可能かどうか」を確認します。
- トークンを発行・保持し、正規性を前提に判定する(JWT/署名+期限)
- 現在の呼び出し状況を取得し、トークン内の番号と照合する
- 順番前は待機ページへ(ポーリングで更新)
- 順番到達で画面へ進める(期限切れは再発行)
入場可能番号は DynamoDB に保存しています。最後の更新時間も保存されているので、定期的に更新されています。
重要なのは、「待機/通過」の判定に使う情報がクライアント側で偽造されないことです。今回の実装では、判定に必要な情報を JWT として持たせ、署名検証を通ったものだけを受け付けるようにしています。細かい仕様は伏せますが、「正規のフローで発行された情報だけを前提に判定する」ためのガードとして入れました。
Lambda@Edge は同期処理のみが可能なため、外部アクセスや処理量は極力抑える必要があります。今回は既存ロジックの設計を踏襲しつつ、エッジで無理のない処理に収まるよう意識しました。
実装中に直面した CORS の問題
実装を進める中で、少し意外だったのが CORS の問題でした。
対象APIに返していたレスポンスが、ブラウザ上では「ネットワークエラー」として扱われてしまったのです。
原因は、Lambda@Edge が返すカスタムレスポンスには CORS ヘッダーが自動では付与されない点にありました。
エッジで完結する処理であっても、 最終的に結果を受け取るのはブラウザです。 そこで、レスポンス生成時に CORS ヘッダーを明示的に付与し、 OPTIONS リクエストも適切に処理するよう修正しました。
この対応によって、待機画面は安定して表示されるようになり、「エッジで制御しているからこそ、フロントとの接点も丁寧に見る必要がある」という学びを得ました。
負荷試験による安定性の検証と調整
本番に入れる前に、まずやっておきたかったのが「想定されるアクセス負荷の集中に対して、システムが安定して稼働し続けられるか」を自分たちの手で確かめることでした。
もうひとつ目的があって、入場制限の“ちょうどいい閾値”を探すことです。バックエンドが踏み抜かないように守りつつ、できるだけ多くのユーザーがスムーズに画面へ進めるラインを見つけたい――そのためにチームで Gatling を回し、一定規模の同時アクセスを作って挙動を確認しました。
見たかったのは大きく3つです。
- 待機判定が意図通りに動作し、対象リクエストが確実に待機レスポンスへ誘導されること。
- 通過させた分の負荷がバックエンドに偏ってかからないこと。
- 待機→再試行の流れがユーザー体験として破綻しないこと。
これらが崩れると、制限の強弱を調整する以前に仕組みとして成り立たないので、最優先で確認しました。
もう一つ気にしていたのが DynamoDB 側です。入場状態の読み書きが集中したときにどこかがボトルネックにならないか、偏りが出てスロットリングが起きないか、消費容量の増え方が荒ぶらないか――このあたりもセットで見ています。
結果としては、エッジで受け止めることでバックエンドへの負荷集中を抑えつつ、応答も安定していることを確認できました。
さらに、制限の強さを調整しながら「守る」と「通す」のバランスを現実的なラインに寄せられたのも収穫でした。既存ロジックを土台にしていた分、挙動の見立てが立てやすく、検証と調整を手早く回せたのも良かった点だと思っています。
最後に
今回の取り組みを通して強く感じたのは、「既存資産をどう活かすか」という視点の大切さです。すでに実績のある仕組みを土台にすることで、設計の方向性に迷うことが減り、「まとめて購入」特有の要件といった本当に向き合うべき部分に集中することができました。
新しい仕組みを一から作ることだけが前進ではありません。これまで積み重ねてきたものを理解し、必要な形に少しずつ進化させていく。今後も、そうした姿勢を大切にしながら、より安心して利用できる購入体験を支えていきたいと考えています。
PR
アソビューではより良いプロダクトを素早く世の中に届けられるよう、様々な挑戦を続けています。 私達と一緒に働くエンジニアを募集していますので、興味のある方はぜひお気軽にエントリーください!