1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsをLambda Web Adapterでサーバレスに動かしてみた[AWS SAM]

Last updated at Posted at 2024-11-01

はじめに

こんにちは。elephantnodeと申します。

普段は都内で社内情シス+Web系エンジニアとして業務しています。

勤め先でNext.jsを使ったWebサイトをAWS LambdaLambda Web Adapterを使ってサーバレスに動かしてみたので、構築した手順についてご紹介します。

AWSへのデプロイにはAWS SAMを利用します。

またデプロイしたあとにCloudFrontOAC(オリジンアクセスコントロール)とWAFを利用して、Lambdaを保護しながら公開する方法もやってみましたのでそちらもご紹介します。

AWS Lambda Web Adapterとは

LambdaでWEBアプリを実行するためのオープンソースソフトウェアツールです。

AWS Lambda Web Adapterは、「Webフレームワークで作ったアプリ」を入れたコンテナを、そのままAWS Lambdaで動かせるようにするLambda拡張ツールです。

対応しているWebフレームワークはExpress.js、Next.js、Flask、ASP.NET、Laravelなど、コンテナで動かせるものはほとんど動かせるようです。

img_lambda-web-adapter_01.7a24a1b11450e466c9de632b6e6f2dd57347177b.png

もともとAWS Lambdaはコンテナで動いているのですが、入出力インターフェースが専用設計なので、WEBアプリケーションのコンテナは通常動作しません。

しかしAWS Lambda Web Adapterを利用すると、Lambdaへ送信されたイベントをHttpリクエストに変換してウェブアプリケーションへ渡し、その反対にウェブアプリケーションのレスポンスをLabmbdaへ返すように動作します。LambdaをWEBサーバーにしてしまうのですね。

こちらがGithubのリポジトリです。

サードパーティのハックかと思ってましたが、本家のアプリ!!

構築環境について

手元のマシンはmacですが、使用したSAMやNext.jsは下記のような構成でした。

パッケージ バージョン
AWS SAM CLI 1.100.0
node.js(ローカル) v20.11.1
node.js(コンテナ) v22.11.0-slim
docker version 26.1.4, build 5650f9
Next.js 15.0.2

AWS SAM CLIのインストールや使い方はこちらの方の記事がおすすめです。

Next.jsのインストールについてはNext.jsのDocsが良いと思います。

手順

Next.jsアプリケーションを作成

まずはローカル環境にNext.jsのアプリケーションを構築します。

npx create-next-app@latest

Need to install the following packages:
create-next-app@15.0.2
Ok to proceed? (y) y
✔ What is your project named? … sample-next-app-sam
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for next dev? … Yes
✔ Would you like to customize the import alias (@/* by default)? … No

<<省略>>

Success! Created sample-next-app-sam at *****/sample-next-app-sam

ほとんどそのままEnterしてます。

❯ tree -L 2
.
└── sample-next-app-sam
    ├── README.md
    ├── next-env.d.ts
    ├── next.config.ts
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── postcss.config.mjs
    ├── public
    ├── src
    ├── tailwind.config.ts
    └── tsconfig.json

4 directories, 8 files

プロジェクトが作成されます。

Next.js にstandalone buildの設定

AWS lambdaはデプロイパッケージのサイズに250MBのクオータ制限があるため、最小限の内容でビルドしたいので、プロジェクトフォルダのnext.config.tsoutput:standaloneを設定します。

next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  output: "standalone" //追加
};

export default nextConfig;

ここでアプリケーションをローカル環境でビルドして、出来上がった.nextフォルダにstandaloneフォルダがあるか確認します。

npm run build

> sample-next-app-sam@0.1.0 build
> next build

   ▲ Next.js 15.0.2

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (5/5)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ○ /                                    5.46 kB         105 kB
└ ○ /_not-found                          897 B           101 kB
+ First Load JS shared by all            99.7 kB
  ├ chunks/215-f207ea7968f9b6d8.js       45.2 kB
  ├ chunks/4bd1b696-23516f99b565b560.js  52.6 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)  prerendered as static content
tree -L 1 -d -a ./.next
./.next
├── cache
├── diagnostics
├── server
├── standalone
├── static
└── types

.nextフォルダにstandaloneフォルダが作成されていますね。

run.shを作成

コンテナ実行時にサーバー内でキャッシュディレクトリを作成しつつ、サーバーを起動させるシェルスクリプトをプロジェクトのディレクトリに配置します。run.shの名前でファイルを作成します。

run.sh
#!/bin/bash -x

[ ! -d '/tmp/cache' ] && mkdir -p /tmp/cache

exec node server.js

Dockerfile作成

Next.jsアプリケーションをビルドして動かすコンテナ用のDockerfileをプロジェクトディレクトリに作成します。

使用するコンテナイメージはAWSのECR Public Galleryから選びます。nodeの22.11.0-slimをイメージにしてみました。

FROM public.ecr.aws/docker/library/node:22.11.0-slim as builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM public.ecr.aws/docker/library/node:22.11.0-slim as runner
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT=3000 NODE_ENV=production
ENV AWS_LWA_ENABLE_COMPRESSION=true
WORKDIR /app
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/run.sh ./run.sh
RUN ln -s /tmp/cache ./.next/cache
RUN chmod +x ./run.sh

CMD exec ./run.sh

最初のコンテナでビルドして、必要なファイルのみ実行コンテナに移して、最後に先程のrun.shを実行してサーバーを動かします。

COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter 

ここでLambda Web Adapterをインストールしています。たったこれだけで導入できるので、非常にシンプルですね。

template.yaml作成

AWS SAM CLIでデプロイするためのtemplate.yamlをプロジェクトフォルダに作成します。

Lambda Web AdapterのGithubリポジトリにはNext.js用のサンプルがありますが、API Gatewayを使うパターンと、コンテナを使わないzip方式関数URL(Function URLs)を利用してAWS Lambda Response Streamingが使えるパターンと三種類の実装方式が紹介されています。

今回はOACに対応するようにLambda 関数URL(Function URLs)を利用する方式にします。

template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  nextjs lambda streaming response
  lambda streaming response SAM template using nextjs
Globals:
  Function:
    Timeout: 300

Resources:
  StreamingNextjsFunction:
    Type: AWS::Serverless::Function
    Properties:
      MemorySize: 512
      PackageType: Image
      Architectures:
        - x86_64
      Environment:
        Variables:
          AWS_LWA_INVOKE_MODE: response_stream
      FunctionUrlConfig:
        AuthType: NONE
        InvokeMode: RESPONSE_STREAM
    Metadata:
      DockerTag: v1
      DockerContext: ./
      Dockerfile: Dockerfile

Outputs:
  StreamingNextjsFunctionOutput:
    Description: "Streaming Nextjs Function ARN"
    Value: !GetAtt StreamingNextjsFunction.Arn
  StreamingNextjsFunctionUrlOutput:
    Description: "nextjs streaming response function url"
    Value: !GetAtt StreamingNextjsFunctionUrl.FunctionUrl

FunctionUrlConfigInvokeMode: RESPONSE_STREAMを指定し、環境変数のAWS_LWA_INVOKE_MODEにもresponse_streamが設定してあります。

OACは後ほど実装するので、最初はFunctionUrlConfigのAuthTypeNONEにしています。ここは後ほど変更します。

Response Streamingについては、AWSのブログの記事で詳しく解説されていますが、この記事内では割愛させていただきます。

プロジェクトのファイル構成

ここまでのプロジェクトのトップ階層にあるファイル構成です。

tree -L 1 -a
.
├── .eslintrc.json
├── .git
├── .gitignore
├── .next
├── Dockerfile
├── README.md
├── next-env.d.ts
├── next.config.ts
├── node_modules
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── run.sh
├── src
├── tailwind.config.ts
└── tsconfig.json

AWS SAMでビルドとデプロイ

AWS SAM CLIでビルドします。

sam build --use-container

<<省略>>

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided

ビルド中に、Next.jsのビルドを行っている部分も表示されます。無事にビルドできたので、AWSへデプロイします。初回のみ--guidedをオプションにして質問形式で設定していきます。

sam deploy --guided 

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: StreamingNextjs
        AWS Region [ap-northeast-1]: 
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: y
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [y/N]: n
        StreamingNextjsFunction Function Url has no authentication. Is this okay? [y/N]: y
        Save arguments to configuration file [Y/n]: y
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]: 

        Looking for resources needed for deployment:

        Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-1h7wayipdynpc
        A different default S3 bucket can be set in samconfig.toml and auto resolution of buckets turned off by setting resolve_s3=False
         Image repositories: Not found.
         #Managed repositories will be deleted when their functions are removed from the template and deployed
         Create managed ECR repositories for all functions? [Y/n]: y

        Saved arguments to config file
        Running 'sam deploy' for future deployments will use the parameters saved above.
        The above parameters can be changed by modifying samconfig.toml
        Learn more about samconfig.toml syntax at 
        https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

<<省略>>

CloudFormation outputs from deployed stack
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                     
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 StreamingNextjsFunctionOutput                                                                                                           
Description         Streaming Nextjs Function ARN                                                                                                           
Value               arn:aws:lambda:ap-northeast-1:*******                               

Key                 StreamingNextjsFunctionUrlOutput                                                                                                        
Description         nextjs streaming response function url                                                                                                  
Value               https://*******.lambda-url.ap-northeast-1.on.aws/                                                              
-------------------------------------------------------------------------------------------------------------------------------------------------------------


Successfully created/updated stack - StreamingNextjs in ap-northeast-1

デプロイが成功すると、Lambda関数のARNと、関数URL(Function URLs)が出力されます。

ARNには関数名が入っていますので、控えておきます。

出力された関数URLにブラウザからアクセスします。

2024_11_01_11_54.png

Next.jsのアプリが表示されれば、デプロイ成功です。LambdaがWebサーバーとして機能して、Next.jsのアプリが動いています。

CloudFrontとOACの設定

続いて、デプロイしたLambdaにCloudFrontでOAC(オリジンアクセスコントロール)を設定していきます。

Lambdaの関数URLはデフォルトでPublicになっていますが、ランダムな長めの文字列になっていて、URLが漏れない限りは部外者が実行してしまうケースはなかなか無いと思いますし、Lambda側で処理するイベントを知らないことには実行しても何かが返ってくることは考えにくいことです。

しかし、Lambda Web Adapterを使うと、アプリを表示するたびに実行されます。Webアプリは攻撃される可能性も当然高くなってくるので、DDoS攻撃などがあれば、高額請求が発生する可能性も!

そのため、関数URLはCloudFrontを前において、アクセスできるオリジンを限定(OAC)して、直接実行されないように変更します。

またCloudFrontは構築時に簡易版のWAFも設定できるので、そちらも設定していきます。

こちらの記事を参考にさせていただきました。

CloudFront ディストリビューションの作成

先ほどコピーしたLambda 関数URLからドメイン部分のみ(https://のスキームや最後のスラッシュなどは除く)を抜き出してOrigin domainに設定します。

貼り付けた画像_2024_11_01_16_05.png

その後、少し下のOrigin access controlの右側のCreate new OACをクリックします。

Cursor_と_Amazon_CloudFront.png

OACの作成画面が表示されますので、名前を入力し、Createを選択します。

Amazon_CloudFront.png

作成が終わると構築後に案内するCLIコマンドでLambdaのポリシーをアップデートしなさいとアラートが表示されます。

Amazon_CloudFront.png

パブリックで公開するので、WAFも設定します(追加料金)。
本当はアプリケーションごとに攻撃手口への対策が異なるので、AWS WAFでACLを設定したいところですが、取り急ぎ標準のもので。

ここまでで一旦、ディストリビューションを作成します。

Cursor_と_Amazon_CloudFront.png

画面が切り替わると、Lambda functionのポリシーをアップデートするためのCLI Commandをコピーできるボタンが表示されますので、これをクリックしてクリップボードに保存します。

aws lambda add-permission \
--statement-id "AllowCloudFrontServicePrincipal" \
--action "lambda:InvokeFunctionUrl" \
--principal "cloudfront.amazonaws.com" \
--source-arn "arn:aws:cloudfront::*************" \
--region "ap-northeast-1" \
--function-name <YOUR_FUNCTION_NAME>

こちらがクリップボードにコピーされた内容です。<YOUR_FUNCTION_NAME>の部分を先程のデプロイした結果のFunction ARNにある、Lambdaの関数名に変更して、コンソールから実行します。

関数名はAWSのWEBコンソールから直接、Lambda関数を探しても良いと思います。

コマンド実行後にLambdaの設定>アクセス権限からポリシーを確認すると、CloudFrontからの実行を有効にするポリシーが追加されています。

StreamingNextjs-StreamingNextjsFunction-kfDqcZjiXT3q___関数___Lambda.png

Cursor_と_StreamingNextjs-StreamingNextjsFunction-kfDqcZjiXT3q___関数___Lambda.png

この状態になったら、CloudFrontが発行したディストリビューションドメインにブラウザからアクセスしてみます。

Amazon_CloudFront-2.png

2024_11_01_17_44.png

CloudFrontのURLでアプリケーションが表示できました。

Lambda FunctionのAWS_IAM認証をONにする

CloudFrontからのアクセスはできましたが、このままだと、Lambdaの関数URL(Function URLs)はまだアクセスできる状態にあるので、AWS SAMのtemplate.yamlを変更して、IAM認証をONにします。

template.yaml
Resources:
  StreamingNextjsFunction:
    Type: AWS::Serverless::Function
    Properties:
      MemorySize: 512
      PackageType: Image
      Architectures:
        - x86_64
      Environment:
        Variables:
          AWS_LWA_INVOKE_MODE: response_stream
      FunctionUrlConfig:
        AuthType: AWS_IAM
        InvokeMode: RESPONSE_STREAM
    Metadata:
      DockerTag: v1
      DockerContext: ./
      Dockerfile: Dockerfile

AuthTypeをAWS_IAMに設定して、ビルドしてデプロイします。デプロイ完了後にLambdaのコンソールから設定>関数URLの情報を見ると認証タイプがAWS_IAMになります。

Cursor_と_StreamingNextjs-StreamingNextjsFunction-kfDqcZjiXT3q___関数___Lambda.png

アクセス権限のポリシーもCloudFrontからの実行権限のみになっています。

StreamingNextjs-StreamingNextjsFunction-kfDqcZjiXT3q___関数___Lambda.png

この状態でLambdaの関数URLにブラウザからアクセスすると、"Forbidden"がメッセージとして返却され、アクセスができなくなっています。

Cursor_と_pfjj2lmpnlv6yi23j5jabkpv5y0kacab_lambda-url_ap-northeast-1_on_aws_と_Next_js___Lambda_Web_Adapter.png

無事、OACが設定されて、CloudFrontからのアクセスに集約することができました!

最低限のセキュリティもできたので、Route 53でCloudFrontにカスタムドメインでエイリアスを作ればオリジナルドメインで運用できるサイトが出来上がりです。

なぜLambdaでNext.jsを?

Next.jsを運用するのであれば本家のVercelを使うか、AWSを利用するなら、AWS Amplifyを利用する方法がまずは検討候補になると思います。

今回のプロジェクトで利用する外部のAPIが、接続元IPアドレス制限があったので、IPを付与できる中間のAPIを構築する必要がありました。最初はLambdaにVPCを設定し、Cloudfront + S3 + Lambdaという構成でSPAのお手軽Webアプリケーションを作ることを検討していました。

初期構想 (1).png

よくある構成ですね。この構成でもCloudFrontがありますし、API Gatewayがあるので、OACもWAFも実装できます。

以前はVueを使って構築していましたが、

  • 標準でtailwind.cssが使える
  • SSR(サーバーサイドレンダリング)が使える
  • 技術情報が豊富

ということでNext.jsを使ってみたかったのです。

そこでまずはVue.jsを置き換えたアプリを作成したところ、S3を使った構成ではNext.jsは静的ビルドしかできず、APP routerも使えないということがわかり、他の方法を模索したところ、Lambda Web Adapterを知りました。

結果的に最終的な形はこの様になっています。

lambda web adapter.png

CloudFrontとLambdaだけ(WAFは自動生成されてますが)です。だいぶシンプルに管理できそうです。

料金について

最後に料金ですが、Lambdaは実行した回数と、実行時間で料金が発生します。無料利用枠もあり、1 か月あたり 100 万件のリクエスト、1 か月あたり 400,000 GB 秒のコンピューティング時間までは無料で使用できます。

仮に100万リクエストで、1アクセスあたり1.2秒程度(1200ms)で512MBのメモリを割り当てたマシンで動かした見積もりが下記でした。

割り当てたメモリ量: 512 MB x 0.0009765625 GB (MB 単位) = 0.5 GB
割り当てられたエフェメラルストレージの量: 512 MB x 0.0009765625 GB (MB 単位) = 0.5 GB
料金の計算
1,000,000 リクエスト x 1,200 ミリ秒 x 0.001 ミリ秒から秒への変換係数 = 1,200,000.00 合計コンピューティング (秒)
0.50 GB x 1,200,000.00 秒 = 600,000.00 合計コンピューティング (GB-s)
600,000.00 GB-s - 400000 無料利用枠 GB-s = 200,000.00 GB-s
Max (200000.00 GB-s, 0 ) = 200,000.00 合計請求対象 GB-s
Tiered price for: 200,000.00 GB-s
200,000 GB-s x 0.0000166667 USD = 3.33 USD
合計階層コスト = 3.3333 USD (1 か月のコンピューティング料金)
1 か月のコンピューティング料金: 3.33 USD
1,000,000 リクエスト - 1000000 無料利用枠のリクエスト = 0 1 か月の請求対象リクエスト
Max (0 1 か月の請求対象リクエスト, 0 ) = 0.00 1 か月の合計請求対象リクエスト
1 か月のリクエスト料金: 0 USD
0.50 GB - 0.5 GB (追加料金なし) = 0.00 関数あたりの GB 請求可能エフェメラルストレージ
エフェメラルストレージの月額料金: 0 USD
Lambda のコスト - 無料利用枠をご利用の場合 (毎月): 3.33 USD

3.33 USD!:open_mouth: 本当ですかね・・・

しかし、気になるはLamda HTTPレスポンスストリーミングをつかったリクエストです。

1回あたりに返却されるデータサイズの平均が6MBまでは無料ですが、大きめの画像などが入っていると、課金対象になるようです。

仮に平均が10MBで100万回レスポンスストリーミングが発生するとこうなりました。

10 MB - 6 MB (特定のレスポンスペイロードに使用されていない容量) = 4.00 MB (呼び出しあたりの請求対象となる処理されたバイト数)
Max (4.00000000 MB, 0 ) = 4.00 MB (請求対象)
4.00 MB x 0.0009765625 GB (MB 単位) = 0.00390625 GB (呼び出しあたりの請求対象となる処理されたバイト数)
1,000,000 リクエスト x 0.00390625 GB/呼び出し = 3,906.25 GB (請求対象の合計)
3,906.25 GB - 100 GB/月 (無料利用枠) = 3,806.25 GB (請求対象の合計)
Max (3806.25 GB, 0 ) = 3,806.25 GB (請求対象の合計)
3,806.25 GB x 0.008 USD = 30.45 USD (1 か月あたりに処理されるバイトの料金)
Lambda HTTP レスポンスストリーミングのコスト (毎月): 30.45 USD

Lambda関数における通常のペイロードは最大6MBまでですが、Streamingを有効にすると20MBまでサポートされるようになり、大きな画像などを扱えば、うっかりすると超過してしまいそうです。

Next.js側でレスポンスストリーミングを利用した実装は今のところ予定にはありませんが、将来実装を考える際には十分に注意する必要がありそうです。

また、ここでは含めていませんが、CloudFrontの料金やWAFなども追加で加算されてきますし、VPCを構築していれば、そちらの料金もかかってきます。

おわりに

いままでバッチ処理や自作API程度にしか使っていなかったAWS LambdaがWebサーバーとして使えるとは驚きでした。

ログモニタリングの課題や、WAFの細かな調整、キャッシュマネジメントなども課題はありますが、まずは小さなプロジェクトで試していきたいと思います。

知見のある方から、お気づきの点などご指摘いただけると幸いです。

ここまでお読みいただき、ありがとうございました。

Next.jsでmicrocmsも使ってみたので、そちらも記事にする予定。

参考記事

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?