メンテモが Vercel を剥がすまで (移行作業編)
この記事は↑の記事の後編です。 前編からだいぶ日が空いてしまいましたが、今回はメンテモのWebアプリケーションがVercelからCloud Runに移行するまでの実際の作業を紹介します。
はじめまして。 @itometeam です。メンテモで業務委託として開発全般のお手伝いをしています。
メンテモのWebアプリケーションはフロントエンドにNext.jsを使っています。 元々は例に漏れずVercelを使っていましたが、スケールするにつれてどうしてもボトルネックになる部分が増えてきたため別の環境に移すことを検討し始めました。
もちろんVercelはNext.jsのデプロイ先として今後も一番の選択肢としてあり続けると思います。 Webサーバをクラウド上に構築する上で意識するべきことをほとんどおまかせでやってくれますし、プレビューURLの自動生成など開発体験においても優れています。 メンテモがVercelの利用をやめた理由は前編のとおりですが、この記事はVercelに文句をつけるものではなく、あくまで同様の対応が必要になった場合の参考として読んでもらえればと思います。
移行先の選択
移行先の選択肢はたくさんありますが、メンテモではもともとWebアプリケーション以外がGCPに載っていたのでWebサーバーの移行先もGCPに統一することにしました。
Vercelは動作環境とCDN、ビルド環境など様々な要素を含むため、同様のことをするためにはGCPのサービスをいくつか組み合わせる必要があります。
Vercelからの移行を完了させるには最低でも以下の機能の代替を探す必要がありました。
ちなみにVercelは各Pull Requestごとに自動でプレビュー環境を作ってくれますが、これは今回の移行では見送りました。
サーバーをどこに載せるか
実際にNext.jsのサーバーを載せるインフラについてはCloud Runを選んでいます。GAEも選択肢としてあがりましたが、マシンスペックの選択肢が多いことや実行環境に縛りがないこと、スケーリングの自由度が高いことを勘案してCloud Runに決めました。
特に自動スケーリングが早く、なおかつ普段は最小限のインスタンスしか起動しないCloud Runの特徴はメンテモのような初期のWebサービスにマッチしているように思います。
ZennやメルカリShopなどメンテモより規模の大きいプロダクトでもNext.js on Cloud Runが採用されていることも決め手の一つになりました。
ビルドをどこでやるか
VercelではGitHub連携をしたら勝手にデプロイフローを組んでくれて、特定のブランチにプッシュするだけで自動デプロイしてくれます。
メンテモではこの代わりにGitHub Actions上でコンテナのビルド、アップロード、Cloud Runのリビジョンの更新までをやっていました。
しかしGitHub Actionsはジョブの並列数に限りがあり、またデプロイだけで10数分かかっていたのもあって頻繁にワークフローが詰まるようになってしました。
GitHub Actions上では他にもテストやLintなどの各種チェックも走らせているため、CIの待ち時間によって開発体験が悪化するのを防ぐために現在ではデプロイのワークフローをCloud Buildに移行しています。
CDNどうするか
Vercelでは多くの静的アセットが自動的にCDNにキャッシュされますが、載せ替えに伴ってこれらも代わりのものを用意する必要がありました。
またメンテモでは多くのページでNext.jsのISRを使っていましたが、Cloud Runへの載せ替えの際にこれらをすべてSSRに変更しました。
代わりにstale-while-revalidateヘッダーを使ってCDN側で同様のキャッシュ戦略を行おうと考えていたため、移行先のCDNはそれに対応しているものに限定して考えると、Cloud CDNとFastlyが候補として残りました。
Fastlyはキャッシュのインスタントパージが魅力的で最後まで悩みましたが、ほとんどのインフラがGCP上に載っているためできるだけそこに寄せたかったこと、 メンテモのビジネスの都合上インスタントパージできないことがクリティカルになりづらそうだったことなどからCloud CDNを選びました。
環境変数どうするか
Vercelではコンソールに環境変数を打ち込むと勝手に暗号化してくれて、Next.jsのビルドフローに合わせて適切にそれらを利用してくれます。
また、vercel cliの vercel env pull
コマンドで、設定されている環境変数をローカルの開発環境に簡単に持ってくることもできます。
Next.jsはビルド時、サーバーのランタイム、クライアントのランタイムでそれぞれ環境変数にアクセスすることができ、
環境変数の渡し方も .env
に書く方法、実行するマシンの環境変数として渡す方法、 next.config.js
の env
フィールドに埋め込む方法があってそれぞれに微妙に挙動も異なります。
さらに適当にこれらを扱うと本来サーバーからしかアクセスできないはずだった機密情報が簡単にクライアント側のコードに埋め込まれてしまうため、Vercelをやめて他の環境に移る際に一番気をつける必要があるところだと思います。
メンテモではサーバーで使う環境変数はすべてGCPのSecret Managerに格納しています。
デプロイを行うCloud Buildと実行環境のCloud Runにはそれぞれ起動時にSecret Managerから値を取得し、ローカルの開発環境ではgcloudコマンドを使って値を取得しています。
最終的にメンテモのインフラ構成は以上のようなものになりました。Vercelを使えばこれらを全部おまかせできると思うと改めてVercelの便利さがわかりますね。
移行手順
インフラ構成が決まってしまえばあとは、それをTerraformに起こしていくだけです。
今回はインフラの大規模な更新だったのもあったため、先に新しいインフラをすべてGCP上に作って社内でテストをした上で最後にDNSの切り替えをするという流れで向き先を変更しました。
移行してよかったこと
コストは安くなった
当初のモチベーションであったインフラのコストカットは無事達成できました。
VercelのEnterprise版とくらべて圧倒的に安くなっているのはもちろんのこと、Proプランとくらべてもかなり安くなりました。 CDNのおかげでCloud Runは最初の無料枠に収まっているためまだ請求が発生してもいません。
Terraformで管理できる範囲が増えた
インフラのほとんどすべてがGCP上に移行できたため、Terraformで管理できる範囲が大幅に増えました。
Vercelからも最近になって公式のTerraform providerが出ましたが、移行を決めたときにはまだなかったのでVercelだけコンソールをいじる必要があり、特に環境変数の管理などに不満を感じていました。
GCPに移行してからはほぼ全てがTerraformで管理できているので、ステージング環境と本番環境の乖離などが起こりづらくなっています。
デプロイフローの選択肢が増えた
Vercelはデプロイへのトリガーが基本的に指定したブランチへのpushしかないですが、コンテナベースのインフラになったことでデプロイフローにかなり自由が効くようになりました。
Cloud Runはリビジョンを切り替えることで即座にトラフィックを新しいバージョンに流すことができるので、現在のメンテモではmainへのpushごとにDockerイメージを作成してアップロードしておいてリリースはリビジョンを切り替えるだけというフローになっています。
これによってバグが起こったときに環境が戻すのが容易になりました。
移行のデメリット
ISRは厳しい
Next.jsのISRはSSRしたページを一定期間キャッシュしておくことでWebサーバーのリクエストを高速化する機能です。 静的サイト生成機能の付加機能として説明されることが多いですが、どちらかというとSSR+stale-while-revalidateキャッシュという理解が近いと思います。
キャッシュ切れしたあとのリクエストに関しても新しいSSRが終わるまでは古いキャッシュを返すので、1回目のSSRが終わってしまえば実質キャッシュヒット率が100%になるというとても便利な機能ですが、 Cloud Runではキャッシュがインスタンスごとに分散してしまうのでうまく動きません。
AWS上ではISRをサーバーレス環境でもうまく動かせる serverless-nextjs というのがあるみたいですが、これはS3でキャッシュを共有して更新の同期をSQSのFIFOキューで行うという設計になっているらしく、これを別環境で再現しようとするくらいならシンプルにSSR+CDNでのstale-while-revalidateキャッシュでいいんじゃないかなと思っています。
やっぱりVercelの開発者体験はよかった
なんども言うけどVercelはよくできてる。