脱SSRによる6割料金削減の裏側
仕事をしていて記録を残すとき、結果だけ残したくなることがよくあります。記録を残すというのはそれ自体労力がかかるものなので、一見すると重要な記録である結果に集中したくなるためです。
しかし、後に続くものたちにとって結果はさほど重要ではありません。彼らは新たな結果のためにまた別の過程をたどらなければいけないためです。 読む人にとっては結果よりも過程こそが重要ということを、ここ数年間仕事をするうえで感じることが非常によくありました。
先日公開したWebフロントエンドで脱SSRして料金を6割節約した話はいわゆる結果の部分です。
ここからは基本的に過程の話を重点的に振り返っていきます。 この文章が会社の現在、未来の同僚だけでなく、すべての大規模更新を企図する人たちの一助となれば幸いです。
また、筆者としてもかなり骨が折れる取り組みだったので、読んでいただけるとありがたいです。
何をしたの?
SSR(サーバーサイドレンダリング)で配信されていたWebフロントエンドを静的配信に乗せ換えてCSR(クライアントサイドレンダリング)に一本化し、インフラをゼロから再構築しました。
移行前夜
ORICALは複数のスポーツチームや芸能事務所(弊社ではパートナーと呼んでいます)と連携してサービスを展開しており、最初の開発は2019年の夏でした。当時はアルバイトを含めても全体で10人程度の会社であったのが今は社員だけで50人近くを数えるようになり、2020年6月に1つのサービスをリリースしたORICALは、現在20弱のパートナー様との協業によりそれぞれのサービスを展開するまでに成長しました。
それに従い、「Elastic BeanstalkでのSSRで手軽にインフラを構成できる」という利点は次第に影を潜め、「手軽な分費用がかさむ」という表裏一体の欠点が顕在化していくことになります。
まず最初に手が入ったのは画像転送料についてでした。この時の取り組みはCloudflare導入でCloudFront転送料金を9割カットした話に詳しく述べられています。この取り組みは2024年の初頭に実を結び、見事に成功しました。
画像転送がひと段落すると、次の料金上の最大の負担はSSRでのサーバー料金になります。そんなわけで脱SSRをやりませんか?という機運が5月ごろに高まりましたが、まずその時点でいくつか問題がありました。
他の大規模更改案件との優先度付け
既に5年たっているコードベースなだけあって、いろいろなところにガタが来ています。5年前に設計した時点で参考にしたベストプラクティスは今ではもう違いますし、このままでは新規開発者がキャッチアップするのが大変です。
細かい改修であれば通常の開発を並行して行えますが、影響が広範にわたる大規模更改を実施する体制がありませんでした。また、他にもいくつかこのような大規模更改を行いたいという企画がいくつかありました。
そういった案件との中での優先度付けを行うこと、そして、そもそも難しいことが予想されるタスクに取り組むという覚悟を決めるという段階が最初にありました。今回の場合、決定に至ったのが7月初頭です。 今回脱SSRが選ばれた理由は基本的には「料金上のインパクトが一番大きい」ためですが、ビルド時間の短縮や他の非機能要件、例えばスケーラビリティの改善など、ユーザーへの利点も大きかったためです。
移行準備
概念実証
脱SSRを行うにあたり最初に一番問題だったのは「そもそもどうやったら問題なく進行できるか?」というのを社内の誰も知らなかったことです。これはORICALが大規模であり、それの根本的な変更を行うにあたって基本的に影響範囲が分からなかったためです。そのため、影響範囲の調査から始まります。 Nuxt.jsの脱SSRは、あるようでなかなか事例が見つかりませんでした。そのため、どうなるかは実際に自分たちで試してみないとわかりません。しかし、やることはそこまで複雑ではありません。 Nuxt.jsの設定を静的配信に書き換え、SSRと同時に新しい構成でも動かせるようにします。
最初の概念実証の実装は7月でしたが、これのおかげで当初一番不確実性が高かった「そもそもどうやって移行するのか?」「どこに問題があり、どのくらい時間がかかりそうか?」というのを大まかに見積もることができました。
次は大まかな見積もりをもとに計画案を作っていきます。
移行計画
インフラを静的配信ベースに移行する際にはいろいろなやり方があり、さまざまなトレードオフが存在します。 脱SSRでは主に以下の「試験環境での実現」「品質保証」「移行」「廃止」の4段階を踏む必要がありますが、それぞれについて3つずつ選択肢を用意し、議論を行いました。
様々な選択肢はどれも結果的に静的配信のインフラを実現しますが、移行工数や移行完了後のメンテナンスコスト、ランニングコスト、危険度、ユーザー体験の間に複雑なトレードオフが存在します。
これらのトレードオフを念頭に、私たちは最も移行工数を減らしたうえで、なるべくランニングコストを下げる方法を取ることになりました。
これは、新機能のリリースが控えており、webフロントエンドチームのリソースがひっ迫することが予想されたためです。
試験環境での実現 + 品質保証
さて、webフロントエンドチームは基本的に9, 10月にリリースされる新機能開発にかかりきりになっているため、作業を2人で進めていきます。 この作業は私 冬鏡 と 岩田で分担して行いました。
大まかには、脱SSRを行う上でそのままでは実現できない機能であるOGPの代替システムの実現を冬鏡が、インフラのTerraform化を岩田が担当しています。
OGPシステムについては、webフロントエンドではシェアシステムとして実現されていますが、ちょうど新機能開発に合わせてWeb Share APIを用いた実装に切り替えるという話が上がっていたため、既存の実装を全面的に切り替えるようにしました。
さらに、品質保証については、QAチームに隅から隅まで確認いただき、上がってきたリグレッションをいろいろ修正したりもしました。本当に頭が上がりません。
インフラのTerraform化については、当初の計画にはありませんでしたが、タイミングが非常に良いため、ついでに行うことになりました。これによって、気軽に検証環境を立てられるようになりました。 この作業はほぼ8月いっぱいかかりました。ここまではほぼ計画通りです。
シェアシステムのバグ
8月末になって、シェアシステムについて、Web Share APIへの移行は無事に終了し、あとは受け入れテストを回すだけの段になって、Androidでは想定通りにブラウザのAPIが機能しないことが判明しました。
具体的には、AndroidのGoogle Chromeでは、テキストと画像を同時にWeb Share APIでシェアしようとすると画像しかシェアされないという状況です。数年前の動作の記録は発見できたので、ここ最近埋め込まれたバグであると考えられますが、これによって、脱SSRのweb側の作業は頓挫してしまいました。
stackoverflowで指摘されていたり
最近は以下でも記事になっていたりします。
また、そもそも意図通りにWeb Share APIが想定通りに動いていたとして、このような比較的大きな挙動の変化について、連携先のパートナー様の同意が得られるのかという問題が浮上してきます。これは、基本的に仕様変更はパートナーと合意して行っているため、ある程度大きな変更をするときは事前に承認を得なければいけないためです。
この際に一番議論を引き起こしたのは、「では、どのような仕様なら受け入れられるのか?」という問題です。基本的にサービスの仕様のほとんどは文書化されていましたが、シェアシステムについては今まで更新がなかったため文書化が漏れており、要件を決めなおすところから行う必要がありました。
結局、1週間ほどてんやわんやした後、Web Share APIについてはフィーチャーフラグでいったん封印したのち、新たに静的配信インフラでも動くようなOGPシステムを開発することになりました。 この実装は設計から行い、10月の初頭までかかりました。
メンテナンスへの対応
インフラの対応は主に 岩田 が行っていました。インフラ側での主なむずかしさの一つは、手軽さとセキュリティが求められる開発環境と、最適なパフォーマンスとコストが求められる本番環境の要求の微妙な差異でしたが、見事にTerraformでハンドリングしていただきました。
最初のInfrastructure as Codeの実装は基本的に想定通り動作したものの、ここでも10月になって暗黙の仕様が牙を剥くことになります。それはメンテナンス時の挙動です。
SSR時代、メンテナンスはApplication Load Balancerのリスナールールをスイッチし、メンテナンス専用の画面にルーティングすることで実現していましたが、静的配信インフラでの本番環境ではそもそもロードバランサーを噛ませないため、根本的に別の仕組みでハンドリングを行うことになります。また、CloudFrontの更新にはやや時間がかかるので、できればインフラ構成を変更しないで何かの設定を書き換えることで動的に対応されるようにします。
最初に検討したのはメンテナンス時の表示に用いるアセットを事前にビルドしておきCloudFront Functionsでリライトを行うというものでした。しかし、CloudFront Distributionのカスタムエラーレスポンスの設定も変えなければいけないことが判明します。そのため、メンテナンス時に変なURLにアクセスすると中途半端に通常運用時のデータが見えてしまいます。しかも、カスタムエラーレスポンスはDistributionとステータスコードにつき1種類しか設定できず、カジュアルに変更することは望ましくありません。
結局、いろいろ悩んだ末に、メンテナンス時に表示する静的ファイルについて事前にビルドしておき、CloudFront KeyValue Storeを見て動的にスイッチを行い、CloudFront FunctionsでHTML文字列を直接返却するというやや強引な実装に落ち着きました。
デプロイと改善サイクル
10月に入ってからは、本番環境を想定したデプロイをステージング環境に行い、問題があったら修正するというサイクルをずっと回していました。
興味深いことに、実装では遭遇しなかった問題が疑似本番環境デプロイではたくさん出てきます。メンテナンスへの対応もそうですが、例えばCORSヘッダーを適切に設定できていなかったり、うまく疎通してくれなかったり、いろいろありました。
特に、旧システムの更新が必要な時に、焦りすぎて操作を間違えてしまい、本番環境のデグレを引き起こしたときは結構凹みました。
また、ここにきて、ついでに進めることを決めたTerraform化が極めて強力な効力を発生させ始めました。インフラがコード化されたことにより、試行錯誤によって得られた知見がコードを通して共有され、高速な改善を可能にしたためです。
移行と廃止
諸々の問題をつぶし終わったあとの移行は比較的シンプルでした。
まず、旧インフラと両立するように移行後のインフラをTerraformを用いてデプロイします。そのうえで、メンテナンス中にRoute 53の向き先を移行後のインフラに変え、いくつかのフィーチャーフラグを設定しなおすだけです。
移行のために事前に手順をすべてコマンド化し、当日臨んだところ、1つのコマンド以外はすべて想定通りに動いたので大変安心したのを覚えています。
そして、このパートナーで1週間稼働させて特に目立った問題がおこらなかったことから、残りのパートナーも移行し、10月末に旧インフラを廃止して現在に至ります。
諸々終わってからの感想と反省
当初の想定を超えてかなり大変だった印象があります。
当初は1ヵ月で終わると思っていたのが3ヵ月かかったのもそうですが、想定しないトラブル続きでした。最初に概念実証して不確実性を劇的に削れたのは大きかったですが、全体的にそのあとの詰めの甘さというのが露呈した気がします。
3ヵ月もかかってしまったもう一つの要因として、webチームと独立して基本的に2人で作業したというのもある気がしていて、副作用としてwebチームへの説明が不十分になってしまったという反省点もあります。
一方、とても良かった点として、今回の移行の影の立役者としてTerraformは一気に社内に定着したのはとても大きかったと思います。 これからインフラを最適化していくにあたって、必ず大きな助けになっていくでしょう。
15万行クラスのフロントエンドのインフラを3ヵ月で載せ替えられたのはなかなか頑張ったんじゃないでしょうか。
まだまだ大規模更改案件はいくつか残っていますが、これらの反省点を活かしつつ、取り組めると良いとチームとして考えています。