😸

Lambdaでコンテナを動かすLambda Web Adapterって実際どうなの? ~NestJSのコンテナをLambdaにデプロイする~

2023/08/31に公開

Lambda Web Adapterとは?

一言で言うと、「様々なWebフレームワークで作られたコンテナで動くWebアプリを、超簡単にLambdaで動かせるようにする」仕組みです。
この、超簡単にというところがポイントで、本当に超簡単です。

具体的には、Webアプリ用のDockerfileに以下の1行追加するだけでLambda Web Adapterが使えるようになります。

COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.0 /lambda-adapter /opt/extensions/lambda-adapter

以下の公式のドキュメントに記載のある通り、公式の実装例では以下のフレームワークが例として挙げられているようですが、基本的には任意のWebフレームワークで動作します。

Python : FastAPI, Flask
Node.js : Express.js, Next.js
Java : SpringBoot
Rust : Axum
Go : Gin

https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter/?awsf.filter-name=*all

実際に、NestJSに関しても動作することが確認できたので、その内容について説明していきます。

他の方法と比べた時のLambda Web Adapterのメリット

WebのAPIをクラウドで動かす場合、代表的な方法として以下が思いつくかと思います。

  1. ECSを使う
  2. 純粋なLambdaを使う
  3. serverless-expressなどの、各WebフレームワークをLambdaで動かすためのライブラリを使う

1のECSを使う方法が最も定番で手堅いだと思いますが、ECSには、クラスター・サービス・タスクなどの概念があり、手軽に構築できるとは言い難いです。
2の純粋なLambdaを使う方法については、以下のようにLambda特有の記法となるので、Lambdaという特定サービスにロックされてしまい、いざという時の移植がしにくくなります。また、ローカルで実行しにくいというデメリットもあります。

export const handler = async (event, context) => {
  // ここに処理を記述
}

3については、Webフレームワークを使用できるというメリットはあるものの、基本的にはLambda上でしか動かないため、ローカルで実行するには一工夫が必要となります。

Lambda Web Adapterを使うと、これら1〜3のデメリットを解消してくれます。

つまり、特定の環境にロックされないWebアプリを、Lambdaという手軽に構築できる実行環境で動かせるように、超簡単・手軽に設定できる、しかもあらゆるWebフレームワークでという点がLambda Web Adapterのメリットになります。

NestJS & Serverless Frameworkでの導入方法

では、ここからNestJSで導入する方法を記載します。
NestJSの環境構築自体は終わっている&Serverless Frameworkを使う前提とします。
(自分ではない優秀なエンジニアの方が作ってくれました。)

Dockerfile

FROM node:18-buster-slim as builder

WORKDIR /build

COPY package.json .
COPY yarn.lock .
RUN yarn

COPY . .
RUN yarn build 

# -----------------------------
FROM node:18-buster-slim

COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.0 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT 3000
ENV READINESS_CHECK_PATH /healthcheck

WORKDIR /app

COPY --from=builder /build/node_modules node_modules 
COPY --from=builder /build/dist dist
COPY --from=builder /build/package.json . 

EXPOSE 3000 
CMD [ "yarn", "start:prod" ]

本題からは少しそれますが、dockerのマルチステージビルドを使って、ビルド環境と実行環境でイメージを分けています。これにより、アプリケーションを実行するコンテナのイメージサイズを小さくすることができ、イメージのpushにかかる時間や実行環境のダウンロードされるまでの時間が短くなります。Lambda上でコンテナを動かす際は、できるだけ導入した方が良いと思います。

具体的に中身を見ていくと、Lambda Web Adapterを導入するために追記した内容は以下3行です。

COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.0 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT 3000
ENV READINESS_CHECK_PATH /healthcheck

冒頭では1行と言いましたが、ポート番号とヘルスチェックのパスを変えたかったので、下の2行を追加しています。

PORT : Web アプリが Listen しているポート番号 (デフォルト = 8080)
READINESS_CHECK_PATH : Web サーバーの起動確認をするための API エンドポイントのパス (デフォルト = /)

Serverless Framework

以下の設定を、serverless.tsに追記します。

  provider: {
    ecr: {
      images: {
        app: {
          path: './', // Dockerfileのパスを記載
          platform: 'linux/amd64',
        },
      },
    },
  functions: {
    'test-function': {
      name: 'test-function',
      url: true, // Lambda Function URLを使う場合の設定
      image: {
        name: 'app',
        command: ['yarn', 'start:prod'],
      },
    },
  }

API Gatewayなどの設定をするのが面倒だったので、Lambda Function URLを使用してAPIを立ち上げています。
詳細は以下ドキュメントを参照ください。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-urls.html

性能面の評価

これまで、Lambda Web Adapterの良い点ばかり説明してきましたが、性能面がどうなのか、という点が気になります。

ここでは性能を「コールドスタートにかかる時間」と「コールドスタートを除いたレイテンシ」に分けて考えます。
冒頭の公式ドキュメントの「性能面はどうなの?」という章を見ると、以下のことが言えそうです。

  • コールドスタートにかかる時間:Lambdaで動かす以上、一定の時間を要する。コンテナであることやLambda Web Adapterを使うことによる影響はなさそう。
  • コールドスタートを除いたレイテンシ:コンテナで動かすことによるレイテンシの増加はあるが、Lambda Web Adatperを使うことによる影響はなさそう。

以上の結果から、いずれの指標においても、Lambda Web Adapterだから特別遅くなる、ということはなさそうです。

次に、実際にNestJSで導入して見た時の結果をExpressと比較します。
(公式ドキュメントにおいても、基本的にExpressを前提に書かれていたので。)

こちらも、自分ではない優秀なエンジニアの方が計測してくれました!

以下、結果(Express、NestJSそれぞれで100リクエストを並列で投げた時の平均)です。

コールドスタートにかかる時間(ms) コールドスタートを除いたレイテンシ(ms)
Express 398 78
NestJS 1378 178

ドキュメントにも記載の通り、CloudWatch Logs Insightsで以下のクエリを発行することで、計測できるようです。

stats count(@initDuration), count(@duration), 
  avg(@initDuration), pct(@initDuration, 99), max(@initDuration), min(@initDuration), stddev(@initDuration),
  avg(@duration), pct(@duration, 99), max(@duration), min(@duration), stddev(@duration)
| filter @message like /^REPORT/

結果を見てみると、コールドスタートにかかる時間、コールドスタートを除いたレイテンシともに3倍〜4倍くらいかかる結果になっています。
やはり、いろんな機能が入っているNestJSだと、挙動が重たくはなりそうです。
とはいえ、コールドスタート時以外は、数百msでレスポンスが返ってきますし、性能要件の厳しいアプリケーション以外では、十分選択肢になりうるのではないでしょうか。

まとめ

Dockerfileに1行追加するだけでLambdaでWebアプリを動かせるようになるLambda Web Adapter、便利で実用的だなと個人的には思いました。
App Runnerもコンテナで動くWebアプリを簡単に作ることができますが、Lambdaで動かすことによってアイドル時には課金されなくなる点がメリットなのかなという理解です。
Lambdaでコンテナを動かす以上、性能面でのトレードオフはありますが、そこまで性能面の要求の厳しくないアプリを構築する場合は、ぜひ使ってみると良いかなと思いました!

NCDCエンジニアブログ

Discussion