Serverless Operations, inc

>_cd /blog/id_nwg365t1vv

title

フロントエンド SSR 環境構築の悩みを解決!最新 React Router v7 と Next.js 15 のサーバーレス環境構築方法

summary

フロントエンドの環境構築に関する悩みがある方を対象に、AWS Lambda を利用したサーバーレスなホスティング環境の構成と構築方法について詳細な手順を交えて紹介します。

クラウドが広く使われるようになったとはいえ、その構成の複雑さ管理の手間などに悩まされている方は多いのではないでしょうか。この記事では、可能な限り構築しやすく、AWS マネージドサービスを活用することでインフラ運用工数を大幅に削減できることを目指した構成をご紹介します。

以前、同様の構成をこちらの記事(https://serverless.co.jp/blog/g30vzpio0ww/)にて紹介しておりますが、この記事では Docker を利用するやり方になります。Docker を使わない場合に比べ、構築の手間が少なく、よりデバッグしやすくなっております。Docker を利用したことがない or 利用できない環境でも対応できる方法を含めておりますので、是非一度目を通してみてください。

こういう方におすすめ

この記事は、以下のような方におすすめです。

  • フロントエンドを AWS にデプロイしたい
  • ECS など VPC を含むネットワーク構築に負担を感じる
  • インフラ運用工数を可能な限りかけたくない
  • 固定費を可能な限り抑えたい
  • S3・DynamoDB・Cognito など、AWS 各サービスと手軽に連携したい
  • 初期ローンチはサーバーレスで構築して、後日サーバーフルな環境への移行を検討したい

前提条件

  • ビルド環境では Docker の利用が必要
  • AWSの基礎知識(IAM, AWS CLI, その他今回の構成で利用する各AWSサービス)

※ Docker 利用に関しては、docker build , tag , push のみ利用します。 docker コマンドが使える環境であることを確認してください。Docker に関する深い知識は必要ありません。

※ ローカル環境での docker の利用が難しい場合、GitHub Actions など CI/CD 環境で行うか、こちらの記事(https://serverless.co.jp/blog/o9t08eelff0a/)のように開発またはビルド用の EC2 インスタンスを立てて作業を行うことで解決することが可能です。記事は Python がメインになっていますが、Node.js 環境の説明も含まれています。Amazon Linux 2023 の EC2 環境では、以下のコマンドで手軽に Docker を入れることが可能です。

sudo dnf update
sudo dnf install -y docker
sudo systemctl start docker
sudo systemctl enable docker

アーキテクチャ概要

シンプルな構成ですが、主なポイントは以下になります。

  • AWS Lambda 上に、ビルド済みの React Router v7 または Next.js 15 アプリケーションをデプロイ(Lambda Web Adapterを使用)
  • CloudFront 経由でアクセスさせることでキャッシュとカスタムドメインが適用できる
  • Function URLs へのアクセスには IAM 認証を適用し、CloudFront 経由でしかアクセスできないようにする
  • IAM 認証の解決には、Origin Access Control (GET) と、SigV4 (POST/PUT) 署名をする Lambda@Edge 関数を作成・利用する
  • 静的ファイルは別途 S3 に振り分けるようにし(※必須ではない)、コンテナ再デプロイなしでも静的ファイルのみ追加・更新ができるようにしたり、Lambda パッケージの最適化が可能

Lambda Web Adapter とは?

SSR タイプのウェブフレームワークの場合、AWS Lambda で実行させるためには、Lambda のイベントにマッピングされた HTTP リクエストペイロードをウェブフレームワークのエントリーポイントに合わせて変換を行う処理が必要です。 aws-lambda-web-adapter は、これらの処理を個別のライブラリごとに行わなくても実行できるように、Lambda Extensions を利用して内部的にアプリケーションサーバーを立ち上げ、AWS Lambda から直接 Function URL を入り口とした HTTP リクエスト・レスポンスをハンドリングできるようにしてくれます。ECS Fargate などで利用できる Docker イメージを、ほぼそのまま(Dockerfile に概ね1~数行追加するだけで)AWS Lambda で利用可能になりますので、アプリケーションの実装に今回のようなサーバーレス構成のための特別な改修等のお手入れをする必要がないことが特徴です。

Lambda Web Adapter の詳細については、こちらの資料も合わせて確認してください。

以降は、上記のような内容を実現するにあたって、最新ウェブフレームワークである React Router v7 と Next.js 15 をデプロイする具体的な方法を紹介していきます。

(共通)事前に必要なAWSリソースを作成する

デプロイに際して、まずは以下のリソースを作成しておきます。

ECR リポジトリ作成

フロントエンドのコンテナイメージをプッシュする ECR(Elastic Container Registry)のリポジトリを作成します。AWS マネジメントコンソールにログインして、検索窓から「ECR」を入力し、以下の画面から新規リポジトリを作成します。

検証用ではリポジトリ名のみを入力し、他の設定はそのまま作成に進みます。イメージタグの上書き可否や暗号化に関しては案件に合わせて適宜設定してください。

作成したリポジトリの名前で検索して、リポジトリ名をクリックします。

プッシュコマンドの確認ボタンを押して、コマンド一覧が表示されることを確認します。ここに表示されているコマンドは後ほどデプロイ作業時に利用しますが、まずはコマンド表示ができる場所だけ押さえておきましょう。ゼロベースでこれらのコマンドを考えたり、編集する必要はありません。

静的ファイルをアップロードする S3 バケットを作成(※必須ではありません)

静的ファイルは CloudFront でキャッシュできるため、あえて Lambda パッケージと分離して振り分ける必要性は基本的にありませんが、実務においては PDF などの静的ファイルを大量に扱ったり、署名付き Cookie を利用するといった場面で最適化を行いたくなることがある場合において有効です。また Lambda パッケージを可能な限りコンパクトに保ちたい等の場合でも、以下のように作成しておきます。

AWS マネジメントコンソールの検索窓から S3 を入力し、バケット作成ボタンを押します。

バケット作成画面から、バケット名を入力します。バケット名はグローバルでユニークな名前にする必要があります。他の設定はデフォルトのままバケットを作成します。

作成が完了したら、以下のようにバケットが見つかることを確認しておきます。

Lambda@Edge の作成

CloudFront -> Lambda Function URL へのアクセス時に、Lambda Function URL のIAM 認証を解決する必要があります。GET リクエストに関しては CloudFront 標準の OAC (Origin Access Control) で解決することが可能ですが、POST/PUTなど Request Body が含まれるリクエストに関しては OAC に加えてコンテントペイロードの署名を含む( x-amz-content-sha256 )必要があります。

以下、AWS Lambda のコンソールから新規の関数を作成します。注意点として、Lambda@Edge は us-east-1 リージョンで作成する必要があるため、他のリージョンで作成していた場合はリージョンを切り替えてください。

関数名を入力し、ランタイムは Node.js 22、アーキテクチャは x86 になっていることを確認して、関数を作成します。

コードを以下のように編集します。


export const handler = async event => {

  const request = event.Records[0].cf.request

  if (!request.body?.data) {
    return request
  }

  const payload = request.body.data
  const decoded = Buffer.from(payload, 'base64').toString('utf-8')
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(decoded))
  const byteArray = Array.from(new Uint8Array(digest))

  let hash = ''
  for (const bytes of byteArray) {
    hash += bytes.toString(16).padStart(2, '0')
  }

  request.headers['x-amz-content-sha256'] = [
    {
      key: 'x-amz-content-sha256',
      value: hash
    }
  ]

  return request
}

コードを入力したらデプロイボタンを押して更新します。

続いて、Configuration > Permission にてロール名をクリックします。

信頼関係(Trust Relationships)タブをクリックして編集ボタンを押します。

編集画面には以下のように Statement.Principal.Service を配列に変更し、 edgelambda.amazonaws.com を追加します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
            "lambda.amazonaws.com",
            "edgelambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

ひとまずこれで共通リソースの準備は完了です。

React Router v7 の環境構築方法

Remix v2 が React Router に合流し、React Router v7 からフレームワーク(SSR)とライブラリー(SPA)の両方の使い方ができるようになりました。この記事ではフレームワーク利用を前提として AWS Lambda にデプロイしていきます。

1. プロジェクト作成

公式ドキュメントに記載されている通り、以下のコマンドを実行してプロジェクトを作成し、ローカル環境での実行を確認します。

npx create-react-router@latest my-react-router-app
cd my-react-router-app
npm i
npm run dev

localhost:5173 を開くと以下のような画面が表示されます。

2. ビルド

まずは、細かい変更ですが、以下のように package.json の scripts で一部修正を加えておきます。

  "scripts": {
    "build": "react-router build",
    "dev": "react-router dev",
    "start": "react-router-serve ./index.js", << ./build/server/index.js を ./index.js に変更

以下のようにビルドコマンドを実行します。React Router はプロジェクト生成時にデフォルトの Dockerfile に同じようなコードが書かれており、同じようなビルドの手順が書かれています。今回は Docker ではなく build コマンドを叩いてから生成されたものを Docker に入れるやり方になりますので、ビルドを実行して生成された build フォルダーを .gitignore に登録してコミット対象から外しておいてください。

# ビルドを実行
npm run build 

# package.json を build/server/ へコピーする
cp package.json build/server/

# ビルド後にバンドルは生成されるが、node_modules は含まれないため、
# コピーした package.json を使って dependencies のモジュールを入れます
npm install --prefix build/server/ --omit=dev 

3. Dockerfile を作成し、ECR へプッシュする

ビルドできたら、Docker のイメージを作成します。デフォルトで生成された Dockerfile を使っても大丈夫ですが、以下のようなシンプルな設定でも進められますので、書き換えておきます。また、デフォルトで生成されている .dockerignore については削除しておいてください。

FROM public.ecr.aws/docker/library/node:20-slim

# Lambda Web Adapter を反映させます
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter

ENV PORT=3000
WORKDIR "/var/task"

ADD build/server/ /var/task/

CMD ["npm", "run", "start"]

Dockerfile の編集ができたら、AWS コンソールを開き、前項で作成した ECR の画面からプッシュコマンドを確認します。まずは AWS 認証情報(アクセスキーまたは AWS Profile など) をセットして、以下のコマンドの順でコピーして入力していきます。

順調にいけば、ECR の画面からイメージが登録されていることを確認できます。

4. 静的ファイルを S3 バケットへアップロード

「(共通)事前に必要なAWSリソースを作成する」にて作成しておいた S3 バケットに、静的ファイルをアップロードします。

aws s3 sync ./build/client/ s3://my-react-router-app

(※こちらは必須ではないため、静的ファイルを S3 へ分けない場合は、前項の Dockerfile にて build/server/ だけでなくbuild/client/ も含めるようにしてください。)

5. AWS Lambda および Amazon CloudFront を作成、デプロイする

ここから、本格的なデプロイ作業をしていきます。まずは、コアとなる AWS Lambda 関数を作成します。AWS マネージドコンソールから AWS Lambda を開き、関数の新規作成ボタンを押してください。

「Architecture」項目に関しては、Docker イメージを作成した環境に合わせてください。例えば、m1/m2 mac や t4g など AWS Graviton プロセッサーを利用するインスタンスタイプ環境でビルドしたコンテナイメージの場合は arm64 になります。それ以外では、通常は、 x86_64 を選択します。

作成した直後では、以下のようにデフォルトのメモリサイズやタイムアウトの値が小さすぎるため、Edit ボタンを押してそれぞれ 256MB10秒 に設定します。また、以下の画面より、Function URL を設定します。

以下のように選択して Function URL を作成します。

作成が完了すると、以下のように Function URL を確認することができます。

続いて、CloudFront Distribution を作成します。Amazon CloudFront のコンソールを開き、Distribution の作成ボタンを押します。

まずは、Origin の項目に作成した Lambda Function URL を入れ、Create new OAC ボタンをクリックします。

ダイアログが表示されますが、特に操作する項目はなく、デフォルトのまま作成ボタンを押して進めます。

下の方にスクロールしていくと、AWS WAF と Description を入力する部分を以下のように設定してください。WAF の場合は CloudFront を作成した後でも必要に応じて適宜有効にして設定することが可能です(IPアドレス制限・bot対策・各種攻撃に対応できる AWS Managed Rule の設定等が可能です)。Description は、作成した Distribution がどのアプリにつながっているかを一覧でわかりやすくするため、なるべくわかりやすい名前にしておいてください。

ここまできたら、画面最下段にある作成ボタンで Distribution を作成します。作成直後、画面上に OAC を有効にするための CLI コマンドのコピーボタンが表示されますので、コピーしてターミナルから実行します。

展開してみると以下のようになりますので、 <YOUR_FUNCTION_NAME> にコンテナイメージを適用した Lambda 関数名を指定します。

aws lambda add-permission \
--statement-id "AllowCloudFrontServicePrincipal" \
--action "lambda:InvokeFunctionUrl" \
--principal "cloudfront.amazonaws.com" \
--source-arn "arn:aws:cloudfront::000011112222:distribution/E1234ABC5D0EFG" \
--region "us-east-1" \
--function-name <YOUR_FUNCTION_NAME>

※ 静的ファイルを S3 に振り分ける場合

CloudFront コンソールの Origin タブから Origin を新規作成します。

Origin 登録画面では S3 バケットを指定し、こちらも Lambda Function URL を指定した時と同様に OAC を作成します。

ダイアログが表示されたら、先ほどと同様にそのまま作成ボタンを押して進めます。

先ほどと少し異なるのは、作成後すぐ Copy policy ボタンが現れますので、コピーしてメモ帳などに一旦控えておきます。

最後までスクロールして Origin を作成します。Origin の作成が確認できたら、次は Behavior タブに移動します。

Behavior 設定画面では、Path pattern に /*.* と入力し、先ほど登録した S3 Origin を指定します。

「Cache Policy 」とありますが、キャッシュが有効になっていると、 build/client/ の中身を更新するたびに Invalidation を作成してキャッシュを無効化する必要があるため、その手順をスキップしたい場合は CachingDisabled にしておくと良いです。

続いて、S3 バケットに移動し、Permissions タブから先ほど控えておいた OAC のバケットポリシーを更新します。

コピーしておいた JSON ドキュメントを貼り付けます。デフォルトのまま、特に修正を加える必要はありません。

{
  "Version": "2008-10-17",
  "Id": "PolicyForCloudFrontPrivateContent",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipal",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-react-router-app/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::000011112222:distribution/E..."
        }
      }
    }
  ]
}

最後のステップとして、Lambda Function URL へのアクセス時の IAM 認証を解決するため、Content Hash を付け加える Lambda@Edge を有効化します。 us-east-1 にてLambda@Edge 用に作成しておいた関数を開き、右上の Actions ボタンから Lambda@Edge へのデプロイを行います。

表示されたダイアログでは以下のように入力し、デプロイボタンを押します。

設定はこれで一通り完了です。

6. 動作確認を行う

ここまで設定ができたら、数分ほどで CloudFront のデプロイが行われ、完了表示を確認してから CloudFront が発行したドメインを開きます。

以下のように表示されていれば完了です。以降は、リリース作業として、AWS Lambda のコンテナイメージと S3 の中身を CLI コマンドでアップデートしていくだけの運用が可能です。

Next.js v15 の環境構築方法

Next.js 15 に関しても前項の React Router v7 とほとんど変わることはありません。前項の「5. AWS Lambda および Amazon CloudFront を作成、デプロイする」以降のやり方は同じですが、その前段のコンテナイメージ作成と S3 へのアップロードまではフレームワーク仕様の違いがあるため、以下解説していきます。

1. プロジェクトを作成

公式ドキュメントに記載されている通り、以下のコマンドを実行してプロジェクトを作成し、ローカル環境での実行を確認します。

npx create-next-app@latest

✔ What is your project named? … my-next-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for `next dev`? … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
Creating a new Next.js app in /home/ec2-user/_sandbox/my-next-app.

cd my-next-app
npm run dev

localhost:3000 を開くと以下のような画面が表示されます。

2. ビルド

まずは、 next.config.ts に以下の設定を追加してください。

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* output: "standalone" を追加します */
  output: "standalone"
};

export default nextConfig;

その後、以下のコマンドでビルドを実行します。完了すると、 .next/standalone/ 以下にデプロイ対象のアセットが生成されていることを確認できます。

npm run build

3. Dockerfile を作成し、ECR へプッシュする

ビルドできたら、Docker のイメージを作成します。Dockerfile を新規作成し、以下のように書いておきます。

FROM public.ecr.aws/docker/library/node:22-slim

COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT=3000
WORKDIR "/var/task"

ADD ./.next/standalone/ /var/task

CMD ["node", "server.js"]

(※以下、前項の React Router v7 と同じ手順です。)

Dockerfile の編集ができたら、AWS コンソールを開き、前項で作成した ECR の画面からプッシュコマンドを確認します。まずは AWS 認証情報(アクセスキーまたは AWS Profile など) をセットして、以下のコマンドの順でコピーして入力していきます。

順調にいけば、ECR の画面からイメージが登録されていることを確認できます。

4. 静的ファイルを S3 バケットへアップロード

「(共通)事前に必要なAWSリソースを作成する」にて作成しておいた S3 バケットに、静的ファイルをアップロードします。こちらは必須ではないため、静的ファイルを S3 へ分けない場合は Dockerfile にて ADD . /var/task とプロジェクト全体を含めるようにし、最後は npm run start を実行するようにしてください。

aws s3 sync .next/static/ s3://my-next-app-assets/_next/static/
aws s3 cp public/ s3://my-next-app-assets/ --recursive

5. AWS Lambda および Amazon CloudFront を作成、デプロイする

(※前項の React Router v7 と同じ手順です。)

6. 動作確認

順調にいけば、CloudFront のドメインを開くと以下のような画面が表示されます。

終わりに

いかがだったでしょうか。正直、本構成の初期構築には細かい設定項目や値がたくさんあるため、構築に際して戸惑う方も少なくないかと思います。しかし、その細かい設定を乗り越えた先には以下のようなメリットを享受することができるため、チャレンジしていただく価値は十分にあると私は考えております。

  • 固定費用ゼロ
  • インフラ領域の運用ゼロ
  • アクセス量に応じた自動スケールと CDN (Amazon CloudFront) キャッシュによる高パフォーマンス
  • Lambda のコンテナイメージと静的ファイルのみS3へアップロードしていくシンプルなデプロイ運用(無停止・ローリングデプロイが可能です)

また、コンソール上での構築は、どのような構成であっても、複雑かつ多くの細かい設定が不可欠です。弊社では、このような構成をワンコマンドで構築できる IaC カタログや、構築時にトラブルが起きた時の対応方法、本構成における導入事例やアプリケーション運用ノウハウなど、様々な形での支援を行っております。ぜひ一度お問い合わせください。

Written by
COO

金 仙優

Sonu Kim

  • Facebook->
  • X->
  • GitHub->

Share

Facebook->X->
Back
to list
<-