Lambda Web Adapterを使ってFastAPIのコンテナをLambdaにデプロイしてみた

こんにちは!Insight Edge で Developer をしている Matsuzakiです。
弊社ではよくバックエンドをFastAPIで構築し、Lambdaにコンテナとしてデプロイする構成が取られています。 FastAPIをLambdaで動かす方法としては今までMangumを使っていましたが、どうやらかなり簡単に導入できるらしい(一行追加するだけ?!)+新しい試みということで、今回『Lambda Web Adapter』を使ってコンテナ製FastAPIアプリケーションをLambdaで動かしてみました。
本記事では、Mangumと比較しながらその設定方法などをご紹介します。

Lambda Web Adapter、気になっていたけれど試したことがないという方など、ご参考になれば幸いです。

本記事でわかること

  • Lambda Web Adapterの概要
  • FastAPIで利用する際のLambda Web Adapterの導入方法
  • Mangumと比較したLambda Web Adapterのユースケース

※ 今回、Lambda Web Adapterに関する性能の検証は実施していません。

目次

Lambda Web Adapterとは

一言で言うと、「様々なWebフレームワークで記述されたアプリケーションを(ほぼ)共通の書き方でLambda上で動かせるようにするツール」です。

通常、フレームワークをLambdaで動かす際にはフレームワークが受け取るリクエストの形式とLambdaが受け取るイベント形式間のギャップを埋めるため、各フレームワーク毎に特定のライブラリやアダプタを使うことが一般的かと思います。 しかし、このLambda Web Adapterを使うと、どのフレームワークを使うにせよ、”基本的には” 以下のようなコード一行をDockerfileに追加するだけでLambda上で任意のWebフレームワークを動かせるようになります。

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


※ "基本的には"と言ってますが、環境や使うイメージによっては上記一行だけではなく、少々追加の設定が必要になります。

詳しくはLambda Web Adapterの公式GitHubをご参照ください。

Lambda Web Adapterのメリット

個人的には、Lambda Web Adapterを使うメリットは大きく以下3点と考えます。

  • 導入の手軽さ
  • 移植性の高さ
  • 一貫した記述方法

導入の手軽さ

1つ目のメリットは、「導入の手軽さ」です。

先程も触れましたが、基本的にはDockerfileにコードを一行(または数行)加えるだけで導入ができます。追加ライブラリのインストールや、アプリケーションのコード自体に手を加える必要はありません。 まずは1回試してみようと気軽に挑戦しやすいと思います。

移植性の高さ

2つめのメリットは「移植性の高さ」です。

Lambda Web Adapterを使用すると、「同じDockerイメージを異なるプラットフォームで利用できる」という利点があります。これは非常に大きなメリットです。

例えば、「同じイメージをLambdaでもFargateでも利用したい」というケースや、「Lambdaで実行していたアプリケーションををFargateに移行したい」というケースがあると思います。 通常、異なるプラットフォーム間でアプリケーションを移行する際には、環境ごとにコードの修正や設定の変更が必要になる場合があります。

しかし、Lambda Web Adapterを使用すると、同じDockerイメージをLambda、EC2、Fargate、ローカルコンピューター上で実行することができます。
異なる実行環境間でイメージを変更せずに実行できるというのは非常に便利です。

一貫した記述方法

最後3つ目のメリットは「一貫した記述方法」です。

一般的に、異なるWebフレームワークをLambdaで実行する際、各フレームワーク専用のアダプターやライブラリが必要になります。これには学習コストが伴うと共に、システム内で複数のフレームワークを使用する場合、その管理はさらに複雑になります。

それもLambda Web Adapterを使用することにより、フレームワークによらず共通の書き方で導入することができます。 これにより、学習コストや管理の煩雑さを減らすことができます。


上記、「導入の手軽さ」「移植性の高さ」「一貫した記述方法」はLambda Web Adapterを使用する大きなメリットと言えるのではないでしょうか。

※ Lambda Web Adapterの採用が最適な選択かどうかは、プロジェクトの具体的な要件や状況によって異なります。

導入方法

さて、ここからはようやくメインである、Lambda Web Adapterの導入方法を記載していきます。 また、今回はこれまで利用していたMangumの実装方法との比較も行いながら、FastAPI製コンテナをLambda上で動かす方法を以下の順でご紹介します。

  • Mangumを使用した実装方法(※これまでのやり方)
  • Lambda Web Adapterを使用した実装方法

ぜひ、実装内容を比較しながら見ていただければと思います。

※ 今回はLambda Web Adapterの設定方法に焦点を当てるため、FastAPIのセットアップやLambdaなどのインフラ構築は完了していることを前提とします。

Mangumを使用した実装方法

ここでは、Mangumを使っていた今までの実装内容を載せていきます。

※ Mangumは、FastAPIのようなASGIアプリケーションをLambdaで動作できるように変換するためのアダプタです。 詳しくはMangum公式ドキュメントをご参照ください。

アプリ側のコード

アプリケーションのエントリーポイントとなるmain.pyは以下のように実装しています。 FastAPIのインスタンスをMangumでラップしています。 これによって、FastAPIアプリケーションはLambdaのイベントとコンテキストに対応するようにラップされ、Lambdaの関数として実行可能になります。

from fastapi import FastAPI
from mangum import Mangum

app = FastAPI()

# FastAPIのルーティングやビジネスロジックの実装(省略)

lambda_handler = Mangum(app)

Dockerfile

AWS提供のLambda用イメージを使用しています。
先程のmain.py内のlambda_handlerをLambdaのハンドラとして登録するために、CMDを上書きしています。

FROM public.ecr.aws/lambda/python:3.11

# 依存関係のインストールなど(省略)

CMD [ "main.lambda_handler" ]


導入の手軽さという意味では、Mangumもかなり手軽なものの、ライブラリの追加とアプリコードの修正が必要となります。

以上が、Mangumを使ってLambda上でFastAPIを使えるようにする方法です。

Lambda Web Adapterを使用した実装方法

今回のメインです。 ここからは実際にLambda Web Adapterを使った実装方法を記載していきます。

アプリ側のコード

main.pyは、以下となります。

from fastapi import FastAPI

app = FastAPI()

# ヘルスチェック用
@app.get("/")
async def health_check():
    return {"message": "success"}

# FastAPIのルーティングやビジネスロジックの実装(省略)

Mangumを使用しないため、単純にFastAPIインスタンスを作成して利用しています。 Lambda Web Adapterではデフォルトでルートパス(/)に対して自動的にヘルスチェックを行うため、ルートにヘルスチェック用の関数を追加しています。

Dockerfile

Dockerfileはこのようになります。 一行目以外は、Mangum利用時と異なっています。

FROM public.ecr.aws/lambda/python:3.11
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1 /lambda-adapter /opt/extensions/lambda-adapter

# 依存関係のインストールなど(省略)

ENTRYPOINT ["uvicorn"]
CMD [ "main:app", "--host", "0.0.0.0", "--port", "8080"]


具体的に中身を見ていきましょう。 まず、冒頭でも触れた以下の部分です。

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

この一行でLambda Web Adapterがイメージに含まれ、Lambda上で使用可能になります。言語や利用するイメージによっては、既存のDockerfileにこの行を追加するだけでLambda上で動作するようになります。

ただ、今回の条件(FastAPIを利用かつAWS管理イメージを利用)では追加の設定が必要となります。

追加した以下2行について解説していきます。 (このたった2行に辿り着くまでになかなかハマりました。日下さん、その節はありがとうございます!)

ENTRYPOINT ["uvicorn"]
CMD [ "main:app", "--host", "0.0.0.0", "--port", "8080"]

Lambda Web Adapterではサーバープログラムがコンテナ内で実行されます。このため、FastAPIアプリケーションをホストするためにUvicornサーバーを起動します。 また、Lambda Web Adaptorは HTTP GETリクエストを8080ポート(デフォルト)で送信するため、8080ポートでリッスンするように指定しています。

今回のようにAWS管理イメージを使用する場合は、AWSのエントリーポイントによってデフォルトのCMDがオーバーライドされるため、ENTRYPOINTも上書きします。これにより、Uvicornサーバーが適切に起動し、FastAPIアプリケーションがLambda環境で正常に動作するようになります。

※ AWS管理イメージを利用しない場合はENTRYPOINTの上書きは不要であるため、以下のように設定します。

CMD [ "uvicorn", main:app", "--host", "0.0.0.0", "--port", "8080" ]

補足

※ Lambda Web Adapterがデフォルトでアクセスするポート番号を変更する場合は、以下のようなコードをDockerfileに追加して環境変数PORTを上書きします。

ENV PORT {ポート番号}

※ ヘルスチェックのパスを変更する場合は、以下のようなコードをDockerfileに追加して環境変数READINESS_CHECK_PATHを上書きします。

ENV READINESS_CHECK_PATH {ヘルスチェックのパス}

※ M1/M2チップ搭載のMacを利用する際は、異なるアーキテクチャ間の互換性を確保するために以下のオプションをビルドコマンドに追加します。

--platform linux/amd64

MangumとLambda Web Adapter

本記事では、Lambda Web Adapterを使用してFastAPIをLambda上で動かす方法をMangumを使用した方法と比較しながらご紹介しました。
では今回のようにFastAPIとLambdaを組み合わせる場合、どちらのアプローチをどのようなケースで選択するのが良いのでしょうか。

個人的な考えとしては、
Lambdaだけで動かすのであればMangum

他のプラットフォームでも動かす可能性があるのであればLambda Web Adapter

が適しているのではないかと思いました。

導入の手軽さ」で言えば、Lambda Web Adapterの方が若干勝るものの、両者とも手軽に設定できるため大差はありません。

違いは、「移植性の高さ」と「イベントデータの取得の容易さ」にあると考えます。

「移植性の高さ」では、同じイメージを他プラットフォームで再利用可能であるLambda Web Adapterが勝ります。

一方、Lambdaからの「イベントデータの取得の容易さ」に関しては、Lambdaイベントの生データに直接アクセス可能であるMangumの方が勝ります。

例として、LambdaとAPI Gatewayを統合してFastAPI製のAPIを構築する際に、APIの認可としてCognitoユーザープールをオーソライザーとして使用し、「ユーザーの認証情報(クレーム)をLambdaイベントのデータから取得する」というケースを考えてみます。

ログイン中のユーザー名をアプリに表示したい場合、ユーザー属性ごとにアクセス制御したい場合などにこのような認証情報が必要となるでしょう。
ではこのケースにおいて、MangumとLambda Web Adapterではデータの取得方法にどのような違いがあるのでしょうか。 以下で見ていきましょう。

まず、Mangumを使用する際は、以下のようなコードで直接的かつ簡潔にLambdaイベントのデータから認証情報を取得できます。

auth_info = request.scope["aws.event"]["requestContext"]["authorizer"]["claims"]

Lambdaイベントの生データに直接アクセスできるため、結果として認証情報の取得を容易にします。
一方Lambda Web Adapterでは以下のようにいくつかの追加のステップが必要になります。

# headersを探す
headers = dict(request.scope["headers"])
# 'x-amzn-request-context'の値を文字列に変換
x_amzn_request_context_str = headers[b"x-amzn-request-context"].decode("utf-8")
# JSON形式に変換
x_amzn_request_context = json.loads(x_amzn_request_context_str)
auth_info = x_amzn_request_context["authorizer"]["claims"]

Lambda Web Adapterを使用してイベントデータを取得する場合は、上記のようにデコードやJSONへの変換処理などが必要になる場合があり、やや複雑さが増します。

比較すると、Mangumを使用したアプローチは、Lambdaイベントデータへのアクセスがより容易で直感的であることが明らかです。

今回性能面については検証できておりませんが、他記事として こちら を拝見する限り、Lambda Web Adapterを利用することで特別遅くなるということはなさそうです。 ただ、利用するフレームワークによって異なる可能性があるため、実際にご利用する際はご自身で性能を検証することをお勧めします。

感想

今回初めてLambda Web Adapterを使ってみましたが、「一瞬で設定できそう?」という想定とは裏腹に、利用するイメージや環境ならではの設定など結構ハマりどころがありました。しかし、ハマった反面学ぶことが多く、新たな知識を得る良い機会となりました。
Lambda Web Adapterは、様々なフレームワークに対応した非常に便利なアダプタなので、気になっていた方は一度試してみてはいかがでしょうか。
本記事がどなたかのご参考になれば幸いです!