ReactからNext.jsに移行したらマルチサーバーが原因で詰んだ話

フロントエンド

はじめに

Next.jsを運用する際はVercelやNetlifyを使うのが一般的な気がします。
しかし、プロダクトの都合上、ECS( マルチコンテナ )を用いてデプロイすることがありました。
その時に詰んだ内容と対策についてです。誰かの助けになると幸いです。

まずは当時の状況に軽く触れておきますので、読むのが面倒な方は本編まで飛ばしてください。

なんで自前サーバーを使ったか

Next.jsを使う前はReactで実装したアプリをECSで公開していました。
React から Next.jsへの膨大な移行作業のなか、DNSの切り替えという手間を惜しんで、そのままECSでNext.jsアプリをデプロイする流れとなりました。

デプロイ方法

ECSでコンテナを立ち上げる際にnext buildし、next startをするという方法を取ってました。

発生したエラー等

ヘルスチェックのタイムアウト

事象

コンテナ立ち上げ時にALBで実施しているヘルスチェックが失敗して、コンテナ立ち上げが失敗していました。
コンテナは落ちると自動で再起動するようになっていたので、ヘルスチェック失敗 -> コンテナ停止 -> コンテナ再起動をループしてEC2のCPU使用率がかなり高くなっていました。
( ECSはFargateではなくEC2を使ってました。)

原因

Next.jsのビルドが結構負荷高めで、処理に時間がかかっており、ヘルスチェックの時間に間に合わないのが原因でした。
コンテナ数が少ない環境(develop環境とか)では負荷に耐え、ヘルスチェックに間に合ってましたが、
コンテナ数が多い環境でのデプロイは、負荷が高すぎてヘルスチェックに間に合わなくなってました。
( コンテナごとにビルドしてたのでコンテナ数が増えると負荷が比例して高くなってました。)

対策

考えられる対策は2つありました。

  1. dockerイメージのビルド時にNext.jsもビルドする。
    これなら全体としてのNext.jsビルドの回数は1回なのでコンテナ数によって負荷が高くなることは無くなります。
    また、コンテナ立ち上げ時にNext.jsビルドすることもないので、立ち上げ負荷を減らせます。
  2. Next.jsのビルドをできるだけ軽くする。

1が全体的なパフォーマンスが上がるのですが、環境変数をビルド時に注入してあげるのが手間だったので2を選びました。
具体的にはnext.config.jsで以下のように設定しました。

module.exports = {
  // swcMinify: false,
  typescript: {
    ignoreBuildErrors: true,
  },
  eslint: {
    ignoreDuringBuilds: true,
  },
};

内容はこんな感じです。
2行目でSWCというビルドツールをONにして、4行目でtypescriptの型チェックをスキップ、7行目でeslintをスキップするようにしています。

SWCについてはNext.jsのバージョン11だか12だかでデフォルトでONになってますが、なぜかOFFにしてたので、該当箇所を削除してSWCを使うようにしました。

typescriptとeslintは、そもそもデプロイ時ではなく、開発時にチェックするもので、CIのテストでクリアしていることを担保していたのでスキップしても良いという判断です。

SWCによるパフォーマンス向上とtypescript, eslintスキップによる時間削減によりヘルスチェックに間に合うようになりました。
たぶんSWCの力がかなりでかいです。

handleHardNavigation Invariant: attempted to hard navigate to the same URL

事象

本番デプロイ後に掲題のエラーが発生し、ページが表示されない状態になりました。

エラーメッセージ自体は「何度も同じURLにアクセスするなよ。」というもののようです。
発生した画面はIDaaSを用いたOAuthのログイン画面で、IDaaSでの処理後にアプリにリダイレクトする画面で発生していました。

原因

IDaaSからのリダイレクト時に以下の順序で処理が発生し掲題エラーの発生に至ったのではと考えています。

  1. IDaaSからアプリURLへリダイレクト
  2. Next.jsサーバーにjsファイルをGETしにいく
  3. そんなjsファイルはサーバーにないよと言われる( 404 )
  4. ブラウザがもう一度jsファイルをGETしてみようとする
  5. エラー発生

マルチコンテナで運用し、かつ、各コンテナごとにNext.jsをビルドしていたので、ファイル名のハッシュ部にコンテナ間で差異が出ているのが原因かと予想しました。
React時代から各コンテナでビルドを実施していたので、この処理の流れはあったもののエラーにならず、当たりコンテナが引けるまでループしていたのだと思います。
Next.jsはそれをエラーとして教えてくれたんですね。

対策

next.config.jsに以下の設定を追加しました。

module.exports = {
  generateBuildId: async () => {
    return "any_fixed_character_string";
  },
}

これによりビルド毎に生成されるハッシュ値を固定化することができ、どのコンテナでも同じ名前のjsファイルを返すようになりました。

一応公式のドキュメントにも記載ありました。

This can cause problems in multi-server deployments when next build is run on every server. 

next buildを使ってサーバー動かす時はマルチサーバーだと問題ありかもねって書いてあるから、たぶんこれが原因だろくらいの理解で実装してみましたが、まさにこれが当たりでした。

一応dockerイメージのビルド時にnext buildするという対策も取れましたが、こちらも環境変数の問題から却下しました。

追記

ハッシュ値を固定化しデプロイごとにファイル名が変わらないようにした影響で、jsファイルがキャッシュされ、古いファイルを使い続けるようになってしまいました。

対策としては、CirclCIやgithub actionsなどでCIごとにハッシュ値が使えると思いますので、それをビルドIDとして指定することにしました。
CI上でDockerイメージのビルドを行なっていたのでARGとしてわたし、環境変数に格納、next.config.jsから環境変数を呼び出すという処理を行いました。

さいごに

以上がReactからNext.jsに移行した際に詰んだ事象と解決策でした。
一番いいのはVercelやNetlifyを利用することだと思いますが、プロダクトの状況によっては自前でホスティングすることになると思います。
そんな方の参考になれば幸いです。

タイトルとURLをコピーしました