AWS Lambda Web AdapterでServerless Next.jsを実現する

こんにちは、フロントエンドエンジニアの堀江(@nandemo_3_)です。

2023年6月22、23日にAWS Dev Day 2023が開催されましたが、

「モダンフロントエンド デザインパターン〜優れたUXを実現するには〜」というフロントエンドの最新動向に関するセッションがありました。

speakerdeck.com

そこで、Serverless Next.jsとそれをAWSで実現するインフラストラクチャーが紹介されており、

今回は、それを具体的に実現する方法をまとめました。

はじめに

まず、今回Serverless Next.jsを実現するために、AWS Lambda Web Adapterを使います。

Lambdaは、主にAPIなどのサーバサイド処理をサーバレスで実現するために使われますが、

AWS Lambda Web Adapterを用いることで、Lambda関数をWebアプリケーションのバックエンドとして使用することができます。

紛らわしくて申し訳ありませんが、「Serverless Next.js」というライブラリのことではないので、ご了承ください。

環境構築

Next.jsのプロジェクト作成

では、Next.jsのプロジェクト作成をします。

 $ npx create-next-app

 What is your project named? … .
 Would you like to use TypeScript? …  Yes
 Would you like to use ESLint? … Yes
 Would you like to use Tailwind CSS? … No
 Would you like to use `src/` directory? … Yes
 Would you like to use App Router? (recommended) … Yes
 Would you like to customize the default import alias? … No

Next.js Standaloneの設定

今回、LambdaにNext.jsのビルド成果物をデプロイしますが、Lambdaはパッケージファイルサイズが250MBと決められているため、ビルド成果物のファイルサイズを削減する必要があります。

そのため、Next.jsのStandaloneモードに設定をします。

設定方法は、以下の通りnext.confing.jsに1行追加するだけです。

// next.confing.js
module.exports = {
  output: 'standalone',
}

npm run buildすることで、.next/standaloneが生成されます。

これで、Standaloneモードにすることができました。

Dockerfile作成

次に、Dockerfileを作成します。

AWS Lambda Web Adapterを使用したLambda関数を作成するには、コンテナイメージを用いるのが一般的のようです。

そのため、Dockerfileを作成します。

今回は、こちらのコードを参考にしました。(まるパクり)

すでに、Dockerfileがある場合は、以下のコードを1行追加するだけです。

# Dockerfile
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.5.0 /lambda-adapter /opt/extensions/lambda-adapter

AWS CDKの設定

続いて、AWS CDKでデプロイを行うので、その設定をしていきます。

今回は手軽にAWS CDKを使いましたが、AWS SAMでもできるので、慣れている方をお使いいただければと思います。

aws-cdkcdkもほぼ同じコマンドのようですが、aws-cdkの方が無駄なパッケージをインストールしないということだったので、こちらを使いました。

※aws cliのインストールは省略

$ npx aws-cdk@2 --version
Need to install the following packages:
  aws-cdk@2.87.0
Ok to proceed? (y) y
$ mkdir cdk
$ cd cdk
$ npx aws-cdk@2 init app --language typescript
$ npm install @aws-cdk/aws-apigatewayv2-integrations-alpha @aws-cdk/aws-apigatewayv2-alpha

CDKのスタックの設定

LambdaとAPI Gatewayを作成する定義をスタックに記述していきます。

code: DockerImageCode.fromImageAssetにはDockerfileのある相対パスを指定します。

// /cdk/lib/cdk-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Duration } from 'aws-cdk-lib';
import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha';
import { DockerImageCode, DockerImageFunction } from 'aws-cdk-lib/aws-lambda';
import { HttpApi } from '@aws-cdk/aws-apigatewayv2-alpha';
import { Platform } from 'aws-cdk-lib/aws-ecr-assets';

export class Frontend extends cdk.Stack {
  readonly endpoint: string;

  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id);

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

    // API GatewayIの定義
    new HttpApi(this, 'Api', {
      apiName: 'Frontend',
      defaultIntegration: new HttpLambdaIntegration('Integration', handler),
    });
  }
}

デプロイ準備

では、デプロイの作業に入りますが、 その前にcdk bootstrapを実行して、リソースをプロビジョニングします。

$ npm run cdk bootstrap
Error: ENAMETOOLONG: name too long, copyfile  ...

しかし、エラーが発生しました。cdkフォルダ配下もコピー対象となっており、ファイルパスの文字数上限を超えてしまったようです。

.dockerignorecdkを追加したら、エラーが解消されました。

// .dockerignore
node_modules
.next
cdk

再度実行しましたが、再びエラーが発生しました。

認証情報の設定をし忘れていたため、アクセス拒否されてしまいました。(初歩的なミス・・・)

 ⏳  Bootstrapping environment aws://xxxxxxxxxx/us-east-1...
 ❌  Environment aws://xxxxxxxxxx/us-east-1 failed bootstrapping: AccessDenied: User: arn:aws:iam::xxxxxxxxxx:user/xxxxxxxxxx is not authorized to perform: cloudformation:DescribeStacks on resource: arn:aws:cloudformation:us-east-1:xxxxxxxxxx:stack/CDKToolkit/* with an explicit deny in an identity-based policy

AWS マネージメントコンソールにてIAMユーザの作成と、aws configureでAWS CDKの認証情報とリージョンの指定を行いました。

$ aws configure
AWS Access Key ID [****************W7NP]: xxxxxxxxxx
AWS Secret Access Key [****************dBWK]: xxxxxxxxxx    
Default region name [ap-northeast-1]: ap-northeast-1
Default output format [text]:

最初、Default region nameを未設定にしてしまい、cdk bootstrapで作成されるサービス群が「us-east-1」に作成されてしまいました。

(この時は気づかなかったのですが、上記のエラーメッセージにも、デプロイ先のリージョンが「us-east-1」になっておりますね)

そのため、この後実行するcdk deployにて、参照リージョンの差異により、デプロイに失敗したので、指定のリージョンを設定しましょう。(今回は、「ap-northeast-1」にしました)

これで、cdk bootstrapが成功しました。

AWS マネージメントコンソールにて、S3バケットが作成されているのが分かるかと思います。(リージョンも「ap-northeast-1」になっていますね)

$ npm run cdk bootstrap

> cdk@0.1.0 cdk
> cdk bootstrap

 ⏳  Bootstrapping environment aws://xxxxxxxxxx/ap-northeast-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
CDKToolkit: creating CloudFormation changeset...
 ✅  Environment aws://xxxxxxxxxx/ap-northeast-1 bootstrapped.

デプロイ

最後に、デプロイを行います。デプロイは1発で成功しました。

$ npm run cdk deploy

Do you wish to deploy these changes (y/n)? y
Frontend: deploying... [1/1]
Frontend: creating CloudFormation changeset...

 ✅  Frontend

✨  Deployment time: 64.44s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxx:stack/Frontend/xxxxxxxxxx

✨  Total time: 68.11s

これで、AWS CDKによるAWS Lambda Web Adapterのデプロイが完了です。

マネジメントコンソールにて、API GatewayやLambda関数が作成されていることが確認できます。

API GatewayのAPIエンドポイントをブラウザでアクセスすると、Webサイトが表示されます。

ここからフロントエンドのカスタマイズをしましょう。(今回はこんな感じにしました)

ソースコード

今回のソースコードはこちらです。

github.com

最後に

今回は、簡単ではありますが、AWS Lambda Web Adapterを用いて、Next.jsのサーバレス化を実現してみました。

この次のステップに、LambdaでSSR Streamingを実現するというものがあります。(参考資料「Next.js 13 の SSR Streaming を AWS Lambda Response Streaming で実装する方法」より)

Next.jsのSSRは、サーバでのレンダリング処理が完了後にレスポンスを返すため、TTFB(Time To First Byte)が遅いという問題があります。

そのために、レンダリング処理が完了したものから、五月雨でレスポンスを返すSSR StreamingというものがNext.js 13でリリースされました。

AWS Lambda Response Streamingを持ちいれば実現できるようですが、今回は対象外としました。(トライしたものの簡単にはできませんでした・・頑張ります)

最後まで、読んでいただきありがとうございました。

参考資料

tmokmss.hatenablog.com

aws.amazon.com