緊急性の高いプロジェクトこそ、冷静に状況を把握して計画を立案
ともかくなんとかしないと! というテンションのプロジェクトでしたが、冷静に考えると実際にはDB負荷はそこまで大きくないな、という仮説を立てました。
ワクチン接種は人間がボトルネックになるシステムです。1000回、2000回と接種する人はいません。仮に国内の居住者がもれなく2回ずつ接種したとしても、これから半年で最大3億回のワクチン接種が国内で行われて、それ以上は多くならないです。このうち、もし仮に100%の予約受付をSTORES 予約 で受注したとして3億行、10%でも3000万行、1%で300万行のDBエントリが新たに半年間で挿入されるわけですが、それはそこまでの高負荷だろうか? というと、DBに詳しいSREの方などですと明確に「違う」と言えるのではないでしょうか。そのくらいの行数を毎日といった頻度で処理しているDBは世の中には数多あるかと思います。
つまるところ、アクセスが殺到してシステムが落ちる、というとき、落ちているのはおそらくDBではないです。前段のWebサーバーです。ここだけ救えば良いのだろうという目星をつけました。
解決策の調査と決定
冒頭記載の通り、STORES 予約 はワクチン接種予約専用に作られたシステムというわけではありません。他にもさまざまな業種の予約業務に利用されており、ワクチン接種予約にアクセスが殺到しているときに、サイトの他の部分には影響を及ぼさないようにする必要がありました。すべてのコンテンツが「https://coubic.com」上でサブドメインも切られずに提供されており、URLは急に変えることはできません。特に重要なのは管理画面で、お客さまが大量にアクセスしているからといってマーチャントまで締め出されてしまうとマーチャントの業務に支障が出てしまいます。
そこで、サイトの一部だけを選択的にレートリミットできるようなソリューションを探すことにしました。検索エンジンで「Web Rate Limit Waiting Room」などと検索した結果、使えそうだなと思ったのは以下の3つです(すべて英文)。
おおむねの傾向として上から下に向かって細かな制御ができるようになる(しないといけない)印象です。今回はこのうち、一番簡単に使えそうなCloudflareを選択しました。
Cloudflareを選んだ理由としては、ひとつは正直なところ先方からの提案があったからというのも大きいです。ですが、そのほかにもTerraformで設定が書けるというのも大きなアドバンテージに映りました。
STORES 予約 ではインフラ構築の多くがTerraformで達成されています。自分たちがすでに利用しているテクノロジーからの大きな乖離がないのは大事です。なぜなら、この新しいソリューションを選択することがどのくらい大変で、どのような運用負荷がかかるかを容易に想像することができるからです(※)。
※ 5月の時点では実はterraform-provider-cloudflareのcloudflare_waiting_roomのリソースはプルリクエストが出たばかりでマージされていませんでした。当初、プルリクエストされている内容を手元でビルドする必要があって一手間必要でしたが、これは6月頃にめでたくマージされ、今は特別な手順を必要とせず動くようになりました。
構成の方針
上記の通り「既存のサービスを止めない」がトッププライオリティです。また、チームの他のメンバーが通常の開発を通常のスプリントで実施しており、これを止めてサイト最適化に注力させるのも今後の計画に影響があると考え、できれば既存のサービス「の開発」も止めずに済めばいいと考えました。そこでおおむね以下のような方針で構成していくことにしました。
- まず既存のサービスサイトである「https://coubic.com」には既存のサービスがこれまで通りデプロイされ運用されている状態をキープします。
- その隣にワクチン接種予約をハンドリングする専用のサイトとして「https://stores-reserve.com」を作成し、Cloudflareでサーブします。
- 普段から「https://coubic.com」のトラフィックを受けているALBで、ワクチン接種予約サイトかどうかをListener Ruleで判断し、必要に応じて「https://stores-reserve.com」にALBのレイヤーでリダイレクトすることにします。
- https://stores-reserve.comの側ではCloudflare Waiting Roomが待ち受けており、それぞれのマーチャント(≒接種会場)ごとに個別のレートリミットを実施します。Cloudflare Waiting Roomでは個々のユーザーからのアクセスに固有のCookieを割り振って、どのユーザーがどの順序でサイトに訪問してきたかを管理します。発行済みCookieの数が多すぎて、アクセスが殺到していると判断される時には、ここでHTMLをレンダリングして終了です。
- サイトに余裕がある時にはCloudflareからALBにリクエストを戻します。リダイレクトがループにならないように注意しながら、実際のサイトへリクエストを投げます。
実際の構成
このような構成にすることで、既存の管理画面などへのアクセス経路には一切変更を加えることなく、アクセスが殺到することが予想されるワクチン予約の予約画面のみを保護することができます。既存のサイトの側はこれまで通りに開発してこれまで通りにデプロイすることが可能になっており、すでに並行して走っている他の開発案件にも影響を与えません。
構成する中で見えてきた、追加で対策が必要だった点
以上の流れでおおむねうまくいきましたが、何点かは追加で対策が必要でした。
まず、CORS対応が追加で必要でした。今回、これまでのドメインと別のドメインを新たに作成しましたが、それらの間でjavascriptなどを共有する必要があります。ではAccess-Control-Allow-Origin: *でいいのではないかというと、そうもいきません。今回は上記の通り、待合室(Cloudflare Waiting Room)がCookieを使っているというのが肝です。Cookie付きのリクエストを発行するにはAccess-Control-Allow-Origin: *ではいけないので、きちんとドメインを明示する必要があります。これは、STORES 予約 の本体アプリを変更してproxyのドメインを明示的に許可するようにも書けるのでしょうが、今回はここに75行ほどのちいさなCloudflare Workersを使いました。
Cloudflare Workersを使用
本体側にも少しだけ変更を入れました。例えばSTORES 予約 にはFacebookログイン機能があるのですが、これを使うとリダイレクト先に「coubic.com」が登録されているので「coubic.com」の側にアクセスしてしまいます。これを避けるため、今回はワクチン接種予約にはFacebookログインは使わないだろうと割り切り、導線を非表示にするといった対応もしました。
