Next.js × ECS(Fargate)で実現するSSRデプロイの裏側
はじめまして。
アルサーガパートナーズ株式会社でサーバーサイドエンジニアをしております、yokoiと申します。
今回はとあるPJにてNext.jsを使用したSSR構成を採用することがあったため、SSRデプロイに関するノウハウをまとめるため、筆を取らせていただきました。
はじめに
今回の記事では主にSSRデプロイに際してハマった部分に焦点を当てて解説していきたいと思います。
そのため注意事項になりますが、下記の項目は割愛させていただきますのでご容赦ください🙇♂️
- PJの詳しいインフラ構成
- SSR構成を採用するメリット・デメリット
- デプロイに関連するコードの全文及び実装方法
- LaravelやNext.js、AWSの各サービスなどに対する環境構築方法
また以下のような知識・経験を前提としております。
- ある程度のマルチレポでの開発知見(フロント・サーバー分離)
- CICDを活用した開発経験
- Next.jsの基礎知識
- AWS(ECS,VPC,Route53,SSM)の基礎知識
- Laravelの基礎知識
それでは項目ごとに解説していきます。
Nodeコンテナにおける環境変数周りの注意点
Next.jsで環境変数を読むためにはビルド時に.env.production
ファイルが参照できるようになっている必要があります。
.env.production
を作成したあとdocker/build-push-action@v4
を使用して、イメージのビルドおよびプッシュを行います。
NodeコンテナのDockerfileでは以下のようにして、Next.jsのビルドを行います。
RUN npm install && \
npm run build
CMD ["npm", "run", "start"]
SSRデプロイにて不具合につながる可能性のある環境変数
環境変数の取り扱いによってはSSRデプロイが上手くいかなくなってしまいます。
今回私たちは下記のような情報を環境変数として管理していました。(※あくまで一例になります)
サーバー側(Laravel)
- CORS設定のためのフロントURL。
- SSR外からのアクセスをする際の送信先ドメイン(送信元ではない点に注意。つまりAPIサーバーのURL)
- SSRでコンテナ間通信をする際の送信先ドメイン
- Cookieを共有するためのドメインを指定するための設定
フロント側(Next)
- APIサーバーのURL
- APIサーバーのコンテナ間通信用URL
APIサーバーのroute定義
APIサーバーのrouteの設定に関しても注意しなければなりません。
ドメイン間通信とコンテナ間通信の両方を受け入れる必要があるからです。
RouteServiceProviderにて下記のように記述します。
protected function mapApiV1BackendRoutes(): void
{
$routes = function () {
Route::prefix('api/admin/v1')
->as('api.admin.')
->middleware('api')
->group(base_path('routes/backend/api.php'));
};
// ドメイン間通信用のドメインでのアクセスの許可
Route::domain(config('domain.app_domain'))
->group($routes);
// コンテナ間通信用のドメインでのアクセスの許可
Route::domain(config('domain.nginx_domain'))
->group($routes);
}
大事なのはRoute::domain($domain)->group($routes)
というメソッド。
このメソッドは引数で渡されたドメインに対してrouteをアタッチするというものになります。
ドメイン間通信とコンテナ間通信の両方で同じrouteをアタッチすることでSSR内外どちらでもrouteにアクセスできるようになります。
(configにて.envのAPP_DOMAIN
とNGINX_DOMAIN
を参照しております。)
ECSのサービスが無限再起動を繰り返す
SSRデプロイをする際にエラーが吐かれることもなく、ECSが再起動を繰り返す現象が発生することがあります。
こちらが発生する原因としてコンテナ間通信がうまくいっていないということが考えられます。
その場合通信がいつまで経っても確立できず、ヘルスチェックに落ちてしまうためコンテナが何度も立ち上がってしまいます。
こういった現象が発生した際には(暫定的な対処になってしまいますが。。。)下記手順を試してみてください。
- 再起動を繰り返すECSサービスの詳細ページに遷移します。
- 右上の「サービスを更新」を押下してください。
- 「必要なタスク」を0にします。
セキュリティグループ
上記までの設定をきちんと行った上でデプロイをして、一見うまく動作しているように見える時があります。
ただアクセスしてみると504 Gateway Timeout
が発生するという現象が発生しました。
このような時はセキュリティグループのインバウンドルールを見直す必要があります。
完全に余談ですが、社内のベテランエンジニアの方から「504エラーが発生しているときはセキュリティグループの設定に問題がある場合が多い」との意見をいただきました。今後気をつけようと思います。
セキュリティグループには許可するグループにセキュリティグループそのものを設定することができます。そして自分自身のセキュリティグループを許可しないとうまく動作しない時があるようです。
コンテナ間通信
さていよいよ本記事の本題であるコンテナ間通信について解説いたします。
SSRでは一般的なドメイン間通信ではなくコンテナ間通信を行う必要がございます。
ECSにてnodeサービスとappサービスを連携させるためにはService Discovery
という機能を使用する必要がございます。
- APIサーバー側のECSサービスの詳細ページに遷移します。
- 右上の「サービスを更新」を押下してください。
- 「サービス検出を使用」にチェックを入れます。
- 「既存の名前空間」から名前空間を選択します。(この時点で必要な名前空間がない場合はAWS Cloud Mapで作成します。)
- AWS Cloud Mapにて名前空間を作成します。
「名前空間名」は任意の名前を入力します。
「名前空間の説明」はオプションなのでスキップしても大丈夫です。
「インスタンスの検出」は「API呼び出しとVPCのDNSクエリ」を選択。
「VPC」に任意のVPCを選択します。
「名前空間の作成」を押下して名前空間を作成します。
- 「名前空間」の作成に成功すると「サービス」を作成できるため作成します。
- 「サービス」の作成に成功した後ECSの詳細に戻り、「既存のサービスの検出サービスを選択」を選択し、先ほど作成したサービスを選択します。
- 全ての設定が完了したら「更新」を押下して設定を反映させます。
-
Service Discovery
にて名前解決したドメインを環境変数に設定するとコンテナ間通信が実現できます。
Service Discovery
に関する詳しい情報は公式のドキュメントを参照してみてください。
認証時のCookie共有のための設定
今回のPJでは認証時にトークンをCookieにセットする方針で実装していました。
SSRデプロイでの問題とはあまり関係がないのですが、おまけとして解説いたします。
Cookieを使用した認証を成功させるには、SSR用のnodeコンテナとAPIサーバーの役割をするappコンテナでCookieを共有できるようにする必要があります。
LaravelではSESSION_DOMAIN
という環境変数に設定したドメインとCookieを共有できるようになります。
そのため共有したいドメインをこの環境変数にセットします。
この環境変数に何も指定しない場合はホスト(LaravelでCookieを設定した場合はLaravel)にのみ送信できるという設定になります。
ただしdomainを指定した場合にはサブドメインにも環境変数を共有することが可能になってしまうのでセキュリティ的にはあまりよろしくないよう。。。。。
まとめ
今回はPJにてSSRデプロイを行う際にハマってしまった事象とその対応をまとめさせていただきました。
想定外のトラブルが続き、非常に時間がかかってしまいました。新しい技術の導入は慎重に行わなければいけませんね。
Discussion