SPA(Single Page Application)が主流だったフロントエンド開発トレンドがSSR(Server-side rendering)とエッジコンピューティングに向けた最適化が進むなど、多様化しています。開発手法自体は JavaScript / TypeScript をベースとしたフレームワークを利用することが多いと思いますが、AWSを利用したサーバーレスの文脈では、開発トレンドが SSR に変わっていくところに関して、少し悩みが出てくることもあるのではないでしょうか。
サーバーレスの定義を広げるとECS Fargate や App Runner のようなマネージドサービスを利用すればさほど悩むことはないと思いますが、いずれも稼働時間単位の課金構造になるので、pay-as-you-go モデルに慣れ親しんだサーバーレスユーザーにとっては固定費が気になってしまいます。できれば、AWS Lambda にデプロイして、実行単位と実行時間分のみ課金される方が心地よく、固定費に悩まされることがなくなります。
この記事はそのような方々を対象に、Next.js や Remix のようなウェブアプリケーションの最新バージョンを簡単に AWS Lambda にデプロイするアーキテクチャと構築方法についてお伝えします。
SSRがしたいだけなら Amplify Hosting で良いのでは?
アプリケーションが提供するコンテンツの性質や機能によって変わってきますが、最もお手軽な方法は Amplify Hosting を利用することだと考えます。全体構成やユースケースが複雑ではなく、アプリの利用目的がはっきりしているのであれば、Amplify Hosting を利用することも良い選択肢です。ただし、以下2点、考慮するポイントがあります。
①通常の AWS Lambda を使用する場合のように、こと細かい環境設定・融通の聞くアーキテクチャ構成・CD/CDのツールと構成の自由度を確保したい場合
②ネットワーク構成(VPCなど)を適用したい場合
Amplify Hosting でも現時点では Next.js のみならず Express のように一般的なサーバーアプリケーションもデプロイ可能になっています。しかし、開発が進むにつれて様々な付帯環境によって発生する要求や仕様変更に柔軟に対応するためには、少しアーキテクチャの抽象化度合いを下げて、以下のような構成を取ることがおすすめです。
構築したいアーキテクチャ構成
この構成自体は、早い段階から AWS Lambda へのデプロイをサポートしている Nuxt3 記事の構成と変わりません。ポイントは、大きく以下2点です。
- Nuxt3 のように AWS Lambda 向けのアダプターを個別にサポートしていないフレームワークでもデプロイして実行させることが可能
- Edge Computing をするほどでもない、汎用的なウェブアプリを固定費ゼロで構成したい場合の選択肢
EC2/ECS/App Runner など、サーバーホスティングして利用できるタイプのウェブアプリケーションであれば、この後紹介する aws-lambda-web-adapter を利用することで、アプリの改修を行うことなくお手軽に AWS Lambda 上にデプロイしてホスティングすることが可能です。
aws-lambda-web-adapter について
SSRタイプのウェブフレームワークの場合、AWS Lambda で実行させるためには、Lambda のイベントにマッピングされたHTTPリクエストペイロードをウェブフレームワークのエントリーポイントに合わせて変換を行う処理が必要です。aws-lambda-web-adapter は、これらの処理を個別のライブラリごとに行わなくても実行できるように、Lambda Extensions を利用してアプリケーションサーバーを立ち上げ、AWS Lambda を挟んで Function URL や API Gateway を入り口として HTTP リクエスト・レスポンスをハンドリングしてくれます。このような仕組みは、以下2点の特徴を持っています。
- ECS Fargate などで利用できる Docker イメージを、ほぼそのまま(Dockerfile に概ね1~数行追加するだけで)AWS Lambda で利用可能
- Docker を利用できない or したくない場合、アプリバンドルを zip で固めて、aws-lambda-web-adapter の Lambda Layer を利用して実行可能
aws-lambda-web-adapter の詳細については、こちらの資料も合わせて確認してください。
また、Lambda response streaming を利用することも可能です。以下の記事も合わせてチェックしてみてください。
以降は上記のような内容を実現するにあたって、 Docker を利用できない、または、したくない方向けに、 Docker に触れなくても aws-lambda-web-adapter を利用する方法をご紹介します。
Next.js v14 のデプロイ方法
昨年(2023年)何かと話題になった Next.js v14 ですが、v13 まではAWS Lambda 向けのビルドバンドルを生成してくれる nextjs-lambda というライブラリが便利でした。しかし v14 に対応できず、今後も継続的にLambda を利用していくためには aws-lambda-web-adapter の使用がおすすめです。
まずは、create-next-app を使って v14 のプロジェクトを作成します。その後、 next.config.mjs
を開いて以下の項目を追記します。
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone", // ビルドパッケージに含めるファイルをまとめる
distDir: "dist" // ビルドしたアセットを dist ディレクトリに出力
};
export default nextConfig;
npm run build
を実行すると dist
ディレクトリが生成されて、ビルドパッケージに含まれるファイルがまとめられます。 dist/standalone/
に以下のような内容で run.sh
を追加して実行権限を与えておきます。
#!/bin/bash
[ ! -d '/tmp/cache' ] && mkdir -p /tmp/cache
HOSTNAME=0.0.0.0 exec node server.js
以下のように zip コマンドを使って dist/standalone/
以下のファイルを固めます。このファイルを Lambda にアップロードすることになります。
zip -r deploy_package.zip ./
ここから Node.js v20 ランタイムで Lambda を作成し、Function URL を有効にしておきます。メモリーサイズは 256MB 以上にします。その後、環境変数に以下のように値を登録しておきます。
AWS_LAMBDA_EXEC_WRAPPER=/opt/bootstrap
AWS_LWA_ENABLE_COMPRESSION=true
PORT=3000
RUST_LOG=info
続けて、aws-lambda-web-adapter の Layer を指定します。以下のARN を直接コンソールなどで指定して登録することが可能です。
※参考:「Lambda functions packaged as Zip package for AWS managed runtimes」
# x86_64
arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:20
# arm64
arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerArm64:20
その後、Function URL を実行して文言が表示されていれば一旦成功です。静的アセットに関してはパッケージに含まれていないため、現時点では画像やスタイルが適用されていない状態になります。後述する「CloudFront の設定と静的ファイルの振り分け」の段落にて設定を行いますので、まずは S3 バケットを作成し、以下のように静的ファイルをアップロードしておきます。
aws s3 cp public/ s3://{web-app-assets-bucket}/ --recursive
aws s3 cp static/ s3://{web-app-assets-bucket}/_next/static/ --recursive
この記事では触れていないですが、AWS SAM(Serverless Application Model) を利用して Next.js v14 をデプロイするサンプルも用意されているので、こちらも合わせてご参考ください。
Remix v2 のデプロイ方法
Next.js v14 リリースに際して注目されているもう一つの React SSR ウェブフレームワークが Remix です。Next.js に関しては aws-lambda-web-adapter にもサンプルが掲載されている反面、Remix に関しては情報が少ないため、もし手掛かりが見つからず断念されている方がいれば、是非一度試してみていただければと思います。やり方は前述の Next.js とほぼ同じで、Lambda にアップロードするハンドル zip の内容とディレクトリ構成が異なる程度です。
まずは、create-remix を使って remix プロジェクトを作成( npx create-remix@latest {project_name}
)します。その後、プロジェクトルートに以下のように run.sh
ファイルを作成、実行権限を与えておきます。
#!/bin/bash
[ ! -d '/tmp/cache' ] && mkdir -p /tmp/cache
HOSTNAME=0.0.0.0 HOME=/tmp exec npm run start # 最後の行は Next.js とは異なるため要注意
次に、ビルドコマンド( npm run build
)を実行します。完了するとプロジェクトルートおよび public/
にそれぞれ build
ディレクトリが作成されます。
-
build/
:app/entry.server.tsx
をメインとしてサーバー側で実行するバンドル -
public/build/
: クライアント側で実行する JS 、画像など静的アセットのバンドル
その状態で、プロジェクトルートから zip ファイルを作成します。zip を作成する前に、 node_module を事前に削除した上で、 npm install --production
と実行しておくことでパッケージサイズを減らすことができます。
zip -r deploy_package.zip ./
ここから Node.js v20 ランタイムで Lambda を作成し、メモリサイズは 256MB 以上、Function URL を有効にしておきます。その後、環境変数に以下のように値を登録しておきます(※ Next.js と同じです)
AWS_LAMBDA_EXEC_WRAPPER=/opt/bootstrap
AWS_LWA_ENABLE_COMPRESSION=true
PORT=3000
RUST_LOG=info
続けて、aws-lambda-web-adapter の Layer を指定します。以下のARN を直接コンソールなどで指定して登録することが可能です。
※参考:「Lambda functions packaged as Zip package for AWS managed runtimes」
# x86_64
arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:20
# arm64
arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerArm64:20
Function URL を実行して文言が表示されていれば一旦成功です。 一方で、Next.js 同様、public 以下のアセットは S3 にアップロードしておきます。静的アセットに関してはzipパッケージから抜いておいても問題ありません。後述する「CloudFront の設定と静的ファイルの振り分け」の段落にて設定を行いますので、まずは S3 バケットを作成し、以下のようにファイルをアップロードしておきます。
aws s3 cp public/ s3://{web-app-assets-bucket}/ --recursive
近日中、aws-lambda-web-adapter にて SAM を使った Remix デプロイのサンプルも更新予定ですので、合わせてチェックしてみてください。
CloudFront の設定と静的ファイルの振り分け
Next.js や Remix のようなウェブフレームワークをビルドするとサーバーで実行するためのバンドルJSが生成されます。その他に、css、js、画像ファイルなどクライアント側で読み込むための静的なアセットも合わせてホスティングする必要がありますが、Lambda のパッケージにそれらを含めてしまうより、S3 などを利用してうまく振り分けることで最適化を行うことができます。
Next.js 、Remix であれば public/
, static/
が対象になります。そのほか、適宜画像などを public/img
などに配置して振り分ける構成も良いです。まずは以下のように別々の CloudFront オリジンとして登録しておきます。
その後、behaviors で path pattern を指定して振り分けます。静的ファイル向けの Origin となる S3 には OAI(Origin Access Identity) / OAC (Origin Access Control) を指定して CloudFront を経由しないアクセスを制限しておきます。
Lambda にデプロイする zip を作成する際は public
以下を抜いてしまい、サーバー側では含まれないように調節することも可能です。こうすることで、Lambda から出ていくデータ伝送量を抑え、静的リソースについては CloudFront のキャッシュを利用やすくなります。同じような考え方で、サーバー側では node_modules 以下を Lambda Layer にしてパッケージサイズを小さくするといった工夫も可能ですので、自由度高くデプロイ構成を最適化することが可能です。
Next.js v14 の場合
以下のように表示できれば構築完了です。
Remix v2 の場合
以下のように表示できれば構築完了です。
※Next.js と違い、create-remix でプロジェクトを作成した直後は画像ファイルが入っていないので、適宜 public/
以下に画像をおいて表示を確認してみてください。例えば、 public/img/
に画像を配置して、 public/
を一気に S3 に同期すると楽に運用できます。
Lambda Function URL への直アクセス対応
CloudFront と連携することは良いとしても、Lambda Function で作成した URL への直アクセスが public のままでは不安を抱くこともあるかと思います。その時に、CloudFront の Origin Request にて Lambda@Edge を起動し、IAM 認証(SigV4)付きで Lambda Function URL を呼ぶように設定することが可能です。Lambda では IAM 認証つきで Function URL を作成し、こちらの記事を参考にLambda@Edge で SigV4 を実装することで、直URLアクセスを防ぐことが可能です。
おわりに
いかがだったでしょうか。aws-lambda-web-adapter を利用すれば、 Next.js / Remix に限らず、また JavaScript / TypeScript フレームワークに限定せずとも様々な言語・ランタイムのウェブアプリケーションフレームワークを AWS Lambda で起動させることが可能です。「サーバーレス化を進めるためには既存アプリケーションの改修が不可欠」という認識があったのも事実ですが、今回ご紹介した仕組みにより多少なりともその負担を和らげることができるのではないでしょうか。今回紹介した方法は諸事情により Docker の利用が難しい場合でも構築可能ですので、ぜひ一度お試しいただければと思います。
この記事に関する内容を含め、AWSのシステム構成全般において気になる点やサポートが必要な場合は、お気軽にお問い合わせください。