[Auth0]Next.jsスタンドアロンモードでフロントエンドをAPI GatewayとLambdaのサーバーレス構成にしてデプロイしてみた

2023.11.30

先日「Next.jsで生成した静的ページに認証を追加してみた」の記事で、Next.js+SSGで生成した静的ファイルをCloudFront+S3で公開しました。

S3で公開するために、わざわざフロントエンド資材をBuild&Exportしていたわけですが
(Auth0SDKもクライアントサイド用のSDKに変更せざるを得なかった)

ここで疑問が生まれました。

  • せっかくNext.jsを使うんだからレンダリング戦略はSSRを採用したい
  • SSRなフロントエンドをサーバーレス構成でデプロイすることは出来なんだろうか🤔

この疑問に対して、AWS AmplifyやVercel、Netlifyあたりが有力な選択肢になるでしょう。
しかし、今日はフロントエンドを敢えてAPIGateway+Lambda(Docker)にデプロイをしてみます。

なぜそんな回りくどいことを...
待ってください、ちゃんとメリットがあるんです。

  • 何よりまず安価(Amplifyも安価)
  • ECS on Fargateに移行することが簡単

私は、後者が特に嬉しいと感じていて

ECS on Fargateは、多くないですが設定が面倒ですし、常時稼働分の時間に比例したコストが発生します。
例えばPOCのフェーズでは、ECS on Fargateを選択せずサーバーレスを選択して、速く、小さく始めることには意味があると思っています。
そして、プロジェクトの規模が大きくなってからやっぱりECS on Fargateを使いたいとなった場合でも、アプリケーションとミドルウェアは変更せずにインフラだけを移行すれば良いです。

つまり、プロジェクト立ち上げ初期においてアーキテクチャに移行柔軟性があるということは大きな強みです。

ざっと意味を見出したところで、技術検証やっていきましょう。

CDK初期化

ディレクトリ名はstandalone_nextjsでやっていきます。

mkdir standalone_nextjs && cd standalone_nextjs
cdk init app --language=typescript

Next.jsプロジェクト作成

path/to/standalone_nextjs

npx create-next-app@latest

# project nameはfrontendにします。その他の設定はお好みで。

Auth0 SDKインストール

./frontend/

npm i @auth0/nextjs-auth0

公式サンプルを参考に実装します。

./frontend/app/layout.js

      <UserProvider>
        <body>{children}</body>
      </UserProvider>

./frontend/app/page.js

export default async function Home() {
  const { user } = (await getSession()) ?? {};

  return (
    <>
      {!user && (
        <div>
          <a href="/dev/app/api/auth/login">Login</a>
        </div>
      )}
      {user && (
        <div>
          <img src={user.picture} alt={user.name} />
          <h2>{user.name}</h2>
          <p>{user.email}</p>
          <a href="/dev/app/api/auth/logout">Logout</a>
        </div>
      )}
    </>
  );
}

Auth0設定

あとはコールバックURLの設定を忘れずにしておきましょう。

.env.localの設定も忘れずに

Next.jsの出力設定

configで出力形式をstandaloneに設定します。ここがこの記事で一番大事な部分です。

./frontend/next.config.js

const nextConfig = {
  output: "standalone",
  basePath: '/app',
}

ローカルで動作確認

npm run build
npm run start

ローカルでログインできればフロントエンドは完成です。

デプロイ

API Gateway+Lambda(Docker)でデプロイするので、AWS CDKとDockerfileを実装していきましょう。

Dockerfile

frontendディレクトリ配下に作成してください。

./frontend/Dockerfile

FROM node:20-alpine AS base

FROM base AS builder
WORKDIR /build
COPY package*.json ./

RUN npm ci
COPY . ./
RUN npm run build

FROM base AS runner
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.0 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT=3000

ADD .env.local .env.local
RUN export $(xargs < .env.local)

COPY --from=builder /build/next.config.js ./
COPY --from=builder /build/public ./public
COPY --from=builder /build/.next/static ./.next/static
COPY --from=builder /build/.next/standalone ./

EXPOSE 3000

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

ここにもポイントがありまして
Lambda Web AdaperをつかってLambdaが必要している入出力インターフェースに変換します。
便利すぎて大好きです。

CDK

./bin/index.ts

import { App, Stack, StackProps, Duration } from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as ecr from 'aws-cdk-lib/aws-ecr-assets';

export class StandaloneNextjsStack extends Stack {
    constructor(scope: App, id: string, props: StackProps = {}) {
        super(scope, id, props);

        const handler = new lambda.DockerImageFunction(this, 'Handler', {
            code: lambda.DockerImageCode.fromImageAsset('./frontend', {
                platform: ecr.Platform.LINUX_AMD64,
            }),
            memorySize: 256,
            timeout: Duration.seconds(30),
        });

        const apigw = new apigateway.RestApi(this, 'SaNextApi', {
            restApiName: 'SaNext',
            deployOptions: {
                stageName: 'dev'
            }
        })

        const lambdaIntegration = new apigateway.LambdaIntegration(handler);
        const resource = apigw.root.addResource('app');
        const proxy = resource.addResource('{proxy+}');
        resource.addMethod('ANY', lambdaIntegration);
        proxy.addMethod('ANY', lambdaIntegration);
    }
}

const app = new App();
new StandaloneNextjsStack(app, 'SanextjsStack', {});
app.synth();

以上で実装は終わりです。

動作確認

デプロイしましょう。

cdk deploy --all

デプロイしたらAPI GatewayのURLを取得できるので、以下を修正・設定してください。

 ちなみにカスタムドメインをAPI Gatewayに設定できる場合、これらリクエストパスのカスタマイズは不要になります。

■next.config.js

./frontend/next.config.js

const nextConfig = {
  output: 'standalone',
  basePath: '/app',
  assetPrefix: 'https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/app',
};

■Auth0設定

  • CallbackURL
    • https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/app/auth/callback

そしてURLにアクセスして画面を確認すると...

うまく行きました!

考察

この記事では、将来的にECS on Fargateにマイグレーションすると仮定してDockerfileを実装しました。さらにAuth0による認証も実装しました。
DockerfileではなくBuild資材をアップロードするようにし、Auth0周りの設定・実装を抜きにすれば、かなりシンプルにフロントエンドをサーバーレス構成でデプロイすることができます。
一度やってしまえばコードはテンプレ化して使い回すことができますし、アーキテクチャ検討する際に選択肢に入れていいと思います。

関連技術

AWS Lambda レスポンスストリーミング

2023年4月7日、Lambdaはレスポンスデータが利用可能になった時点で呼び出し元にデータを順次送信できるようになりました。これにより、本記事のようなWebアプリケーションをLambdaで実行する場合のレスポンス待ち時間が短縮し、ユーザーエクスペリエンスが向上します。さらに6MBから20MBまでレスポンスペイロードを送信できるようです。これは嬉しい。詳細は以下の記事を御覧ください。

https://aws.amazon.com/jp/blogs/news/introducing-aws-lambda-response-streaming/

Lambda Web Adapter

本記事でも登場していますが、LambdaイベントをHTTPリクエストに変換することで、Lambda上でWebフレームワークを動かせるように出来るツールです(本来はLambda関数のみ実行可能)詳細は以下の記事を御覧ください。

https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter/

AWS Lambdaどんどん便利になっていく...!