Next.jsのサーバーサイドレンダリング (SSR) と、同じEKSクラスター内で稼働するバックエンド(Rails)のpod間の通信でハマってしまったので、備忘録で解決策とハマったポイントを書いてみます。
課題:Next.jsのSSRからRails APIへのリクエストがうまくいかない
Next.jsで構築されたフロントエンドと、Railsで構築されたバックエンドが同じEKSクラスター内にあり、それぞれが独立したPodとして稼働している状態でした。
フロントエンド: Next.js (Pages Router)
バックエンド: Ruby on Rails
このような環境で、Next.jsのSSR (getServerSideProps
) 内でRailsのAPIを叩いて、データを取得しようとしたところ、APIリクエストがなぜか失敗してしまうという問題に直面しました。
// pages/index.js (例)
export async function getServerSideProps() {
// このfetchがうまくいかない...
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/v1/posts`);
const posts = await res.json();
return { props: { posts } };
}
ローカル環境では.envファイルにNEXT_PUBLIC_BASE_URL='http://localhost:0000'
と指定してDockerで接続していたため問題なく動いていたのですが、EKS環境にデプロイするとfetchが出来ずエラーとなってしまいました。
原因:SSRとクライアントサイド通信の違い
Next.jsのSSRは処理がクライアントではなくサーバーサイドで実行されるということは理解していましたが、APIをリクエストする際の通信の違いをきちんと理解できていなかった為、かなりハマってしまいました。
-
クライアントサイドでの通信:
- ブラウザからRails API Podに対して、外部のロードバランサーやIngressを経由してHTTPSリクエストが送信される
-
fetch
のURLは、外部からアクセス可能なドメイン名やIPアドレスになる
-
SSR (Node.js) での通信:
- SSRはサーバーサイド (Node.js) で実行されるため、Next.jsのpodからRailsのPodへ直接リクエストを送信する
- これはクラスター内部の通信であり、外部のドメイン名やIPアドレスは利用できない
SSR内のfetch
でlocalhost
や外部のドメインを指定しても、Next.jsのpodから見ると通信先が正しくないため、リクエストが失敗してしまっていたようでした。
ポイント1:SSRのfetch
は絶対パスにする
SSRでfetch
を使用する場合、相対パスではなく、完全なURL (絶対パス) を指定する必要がありました。
環境変数で登録していたNEXT_PUBLIC_BASE_URL
は、空文字の場合相対パスとなりますので、クライアントサイドの場合は自動で現在のオリジンを利用してリクエストを送信します。
が、SSRはサーバーサイドで実行されるので、絶対パスを指定する必要があるそうです。
ポイント2:クラスター内部通信にはService名を利用する
前述した通り、今回のパターンでは外部のドメイン名やIPアドレスは利用できないので、podのservice名を指定する必要がありました。
解決策:Service名を利用した内部通信
解決策としては、以下の2つの設定を行うことで、Next.js SSRからRailsのAPIへの通信ができるようになりました。
1. Railsの設定ファイルでService名を許可する
もしconfig.hostsの設定がある場合は、ここにpodのservice名を追記します。
これにより、Railsはクラスター内部からのリクエストを受け付けるようになります。
# config/environments/production.rb (例)
Rails.application.configure do
# ...
# RailsのService名を指定
config.hosts << "rails-api-service-name"
# ...
end
config.hosts.clear
の設定がある場合は、すべてのリクエストを受け付ける状態(非推奨)なので、この設定は不要です。
2. Next.js SSRのfetch
URLを変更する
SSR内のfetch
のURLを、RailsのpodのService名とポート番号を使った内部通信用の形式に変更します。
// pages/index.js (例)
export async function getServerSideProps() {
const serviceName = process.env.SERVICE_NAME; // http://rails-api-service-name:3000
// Service名とポート番号を指定して内部通信
const res = await fetch(`${serviceName}/api/v1/posts'`);
const posts = await res.json();
return { props: { posts } };
}
この修正で、バックエンドのAPIを叩けるようになりました!
まとめ
ローカルでは発生しないエラーでしたのでデバッグが少し大変だったのと、
SSR側でエラーの内容をconsole.logで出力してみたところ 今回の原因とは全く違う部分(エラーログ出力時のエラー)が出たのもあって、原因特定まで時間を要してしまいましたが、理解を深めることができてよかったです!