Next.jsアプリをVercelからGCPに移行した話

5 min read読了の目安(約5100字

ZennではフロントエンドにNext.jsを使っています。もともとはVercelで動かしていたのですが、2021年3月にGCPに移行しました。今回は移行を決めた理由や、具体的な構成、移行作業などについて書きたいと思います。

なぜ移行したのか

Next.jsのデプロイ先としてVercelは圧倒的に優れています。ISRImage OptimizationといったNext.jsの強力な機能をサーバー側の追加設定なしで使用できますし、CDNでの静的ファイルのキャッシュなども特に意識しなくてもいい感じにやってくれます。

Vercel以外にデプロイするとなると、Next.jsの一部の機能がうまく動かなかったり、パフォーマンス・チューニングを自分で頑張る必要があったりと自分で面倒を見なければならない部分が多くなります。

しかし、Zennのケースでは以下のような理由からVercelからGCPに移行することにしました。

理由1. コストを抑えるため

最も大きな理由はランニングコストです。Vercelの料金プランは2021年3月時点では3種類しかありません。

  • Hobby... $0
  • Pro... $20/月
  • Enterprise... 料金は相談

https://zenn.dev/lollipop_onl/articles/eoz-vercel-pricing-2020

ZennではProプランを利用していたのですが、Bandwidth 〜1TB/月の制限に少しずつ近づいており、数ヶ月のうちにEnterpriseプランに変更するか、他のサービスへ移行するか判断する必要がありました。

Enterpriseプランだと数千ドル/月かかるようで、現時点でこれだけのコストをかけるのはためらわれます。また、サービスが成長していけばさらに料金が上がることが予想されます。

一方でGCPなら、当面は数百ドル/月で運用できそうだと分かりました。Vercelにはお世話になっているのでEnterpriseプランにしてお金を突っ込みたい気持ちはあったのですが、余裕のあるお金の使い方ができるフェーズではないので、低コストが運用できるGCPが良いだろうと考えました。

理由2. ランタイムの制限が不安だったため

VercelにはAWS Lambdaが使われており、Next.jsのSSRやISRなどの処理もLambda上で実行されます。そのため、間接的にLambdaの制限の影響を受けることになります。

特に不安を感じていたのがPayloadの制限で、Vercelではリクエスト・レスポンスのbodyが5MBを超えると413: FUNCTION_PAYLOAD_TOO_LARGEエラーが発生します。画像などを処理しない限りまず問題にはならないのですが、以前起きた障害ではこの制限に苦しむことになったので、できれば対処しておきたいという気持ちがありました。

理由3. フロントとバックエンドをGAEに統一すると色々と捗るため

ZennのバックエンドはGoogle App Engine(GAE)で動いています。フロントエンドもGAEに共通化すれば、設定や管理が楽になります。

CI/CDがやりやすい


これまでのデプロイの流れ

GAEに共通化することでCloud Buildを使ったCI/CDがやりやすくなり、設定の見通しもよくなります。

ただし、Vercelでは3〜4分ほどでデプロイが完了するのに対し、GAEではその2倍近く時間がかかることになりました。

ステージング環境を作るのが楽に

インテグレーション環境やステージング環境を作るうえで、GAEとVercelを連携するのが結構面倒でした。GAEに共通化すれば設定が簡潔になり、IaC(インフラ構成のコード化)も可能になります。
また、GAEだとIdentity-Aware Proxy(IAP)という仕組みによるアクセス制御をほぼ無料で使用できます。

※ Vercelにもサイトにパスワードをかける機能がありますが、使用するには $150/月 かかります。

共通のドメインを使える

バックエンドにサブドメイン(api.zenn.dev)を使っていましたが、フロントエンドもGCPに移行すれば共通のドメイン(zenn.dev/api/*)を使うことができるようになります。

理由4. 障害に巻き込まれる可能性を抑えるため

これまではGAE、Vercel、Lambda のいずれかに障害が起きるとサイトごと落ちてしまう構成になっていました。例えば、2021年2月にはVercelの障害AWS東京リージョンの詳細の影響を受けています。依存先が減るのは心理的にも楽です。

理由5. GAE東京リージョンにカスタムドメインをあてると遅くなる問題を回避

GAE東京リージョンではカスタムドメインを使うとレイテンシが増加します。

https://zenn.dev/catnose99/articles/56f523d39cca43

個人的に試した結果、カスタムドメインをあてるとだいたい50〜150msほどレスポンスが遅くなることが分かりました(エッジキャッシュにのっている静的ファイルは問題ないようです)。

この問題はGCPのロードバランサを配置し、ロードバランサに対してカスタムドメインをあてることで解消します。詳しい構成はのちほど紹介します。


以上の理由からVercelからGCPへ移行することを決めました。Vercel自体に不満があったからではなく、総合的に見てGCPで運用した方が良いだろうと判断した形です。今後自分がNext.jsで新しくアプリを作るときには、よっぽどの理由がない限りVercelを選ぶと思います。

新しい構成

フロントエンドとバックエンドの両方をGAEを動かし、その前にロードバランサ(Cloud Load Balancing)を配置してます。フロントエンドに関してはCloud CDNを有効にして、部分的にファイルをキャッシュをしています。

※ Cloud CDN は Cloud Load Balancing と合わせて使うことになります。

Cloud Load Balancingにzenn.devドメインをあてており、URLマップ機能を使ってリクエストされたパスによってルーティングするサービスを振り分けるようにしています。これにより、GAE東京リージョンのカスタムドメイン遅い問題が解消され、APIレスポンスが体感で分かるレベルで速くなりました。

このあたりのGAEやCloud CDN / Load Balancingの設定については別の記事にまとめました。

ISRは廃止

Next.jsのISRで動的コンテンツをキャッシュするときの戦略にも書きましたが、ZennではもともとNext.jsのISRを多用していました。

しかし、Next.jsのISRをVercel以外で実現するのは現状ではとても難しいため、Next.jsデフォルトのISRの使用はやめることにしました。

幸いにもCloud CDNはISRに近い挙動を実現できるstale-while-revalidate(SWR)というキャッシュコントロールに対応しているので、今後は投稿一覧ページなどを中心に活用していきたいと思っています。

↓ 参考

https://zenn.dev/catnose99/articles/0b601c1f62019b

なお、記事や本の詳細ページなどでは、キャッシュの設定を誤ったときのリスクの大きさからキャッシュ自体を廃止することにしました。(ソースコードの見通しが良くなった!)

移行手順

移行作業は、単純にToDoを作って一つずつ消化していくことにしました。ざっくりと以下のようなことを実施しました。

  1. ISRの実装を取り除き、新環境で動くような実装に書き換え
  2. 開発環境でGAE、CDN、ロードバランサ、証明書などの設定を行い、問題なく動くことを確認
  3. 開発環境のDNS設定でVercelからGCPへの切替を実施
  4. 開発環境での負荷テストを通してインスタンス数などのオートスケール設定を調整
  5. 開発環境と同じようにGCPでステージング環境(後の本番環境)をつくる
  6. メンテナンス予告(ツイート
  7. (移行当日)DNS設定でステージング環境にzenn.devドメインを向ける

準備をはじめてから移行完了まで、だいたい2週間くらいかかりました。

意識したポイント

スムーズに移行を行うために意識したポイントはこんな感じです。

  • 稼働中の本番環境にあらかじめ反映しても問題のない変更はデプロイしておき、なるべく移行時の変更箇所を小さくする(問題が発生したときに原因究明をしやすくするため)
  • 開発環境で事前に移行を行い、本番環境でも全く同じ手順を実施するようにした
  • 頭が回っていなくてもミスしないように当日の手順書を超詳細に作っておいた(何も考えなくてもその手順に従えば移行が完了するようにした)

結果的に、ダウンタイムがほとんど発生することなく移行が完了しました。これでインフラ周りの心配事をひとつ解消できました。ようやくサービス自体の改善に着手できそうで嬉しいです。

この記事に贈られたバッジ