これは、なにをしたくて書いたもの?
AWS Lambdaで通常のWebアプリケーションフレームワークを使えるという、AWS Lambda Web Adapterを1度試しておきたいなと
思いまして。
今回はFastAPIで試してみたいと思います。動作確認をするのはローカル環境です。
AWS Lambda Web Adapter
AWS Lambda Web AdapterのGitHubリポジトリーはこちら。
GitHub - awslabs/aws-lambda-web-adapter: Run web applications on AWS Lambda
AWS Lambda Web Adapterを使うと、AWS Lambda関数をHTTP 1.1/1.0をサポートするWebアプリケーションフレームワークを使って
開発できるとされています。オーバーヘッドも小さいようです。
特徴はこちら。
- AWS LambdaでWebアプリケーションを実行する
- Amazon API GatewayのRest APIおよびHttp APIのエンドポイント、Lambda関数URL、ALBをサポート
- Lambdaマネージドランタイム、カスタムランタイム、Docker OCIイメージをサポート
- 任意のWebフレームワークと言語をサポートし、コードの依存を新しく追加する必要はない
- 自動でバイナリレスポンスにエンコード
- Gracefulシャットダウンのサポート
- レスポンスのペイロード圧縮をサポート
- レスポンスのストリーミングをサポート
- 非HTTPトリガーイベントをサポート
AWS Lambda Web Adapter / Features
Lambda関数URLというのは、AWS Lambda関数のためのHTTPエンドポイントですね。こちらを使うと、AWS Lambda関数をHTTPで
直接呼び出すことができます。
Lambda 関数 URL の作成と管理 - AWS Lambda
HTTP以外のイベントもサポートするんですね。
使い方はこちら。
AWS Lambda Web Adapter / Usage
まず、導入方法について見ていきましょう。
DockerイメージもしくはOCIイメージにAWS Lambda Web Adapterを追加する方法と、Zipパッケージの場合はLambdaレイヤーとして
導入する方法があるようです。
- AWS Lambda Web Adapter / Usage / Lambda functions packaged as Docker Images or OCI Images
- AWS Lambda Web Adapter / Usage / Lambda functions packaged as Zip package for AWS managed runtimes
起動順としてはAWS Lambda Web Adapterが拡張機能として起動した後に、AWS Lambda上で構築されたWebアプリケーションが起動する
という流れになるようです。
起動したかどうかはヘルスチェックで確認するようですね(環境変数でポートとパスを変更可能)。
AWS Lambda Web Adapter / Readiness Check
デフォルトではGET /
に対して応答し、ポート8080でリッスンするように構成した方がよいでしょう。
その他、設定は環境変数で行うようです。
AWS Lambda Web Adapter / Configurations
AWS Lambdaでのコンテキストについて。
- AWS Lambda Web Adapter / Request Context
- AWS Lambda Web Adapter / Lambda Context
- AWS Lambdaが関数ハンドラーに渡すオブジェクト
- Using the Lambda context object to retrieve Python function information - AWS Lambda
- AWS Lambda Web Adapterでは、この情報を
x-amzn-lambda-context
というHTTPヘッダーでWebアプリケーションに送信する
シャットダウン時にはSIGTERM
シグナルが送信されるようなので、Webアプリケーション側でシグナルをハンドリングすることで安全に
シャットダウンができるとされています。
AWS Lambda Web Adapter / Graceful Shutdown
ローカルでの実行は、AWS SAMの利用が挙げられていますね。
AWS Lambda Web Adapter / Local Debugging
非HTTPイベント(Amazon SQS、Amazon SNS、Amazon DynamoDB、Amazon Kinesis、Amazon Managed Streaming for Apache Kafka、
Amazon EventBridge、Amazon Bedrock Agentなど)もサポートしていることは、こちらに書かれています。
AWS Lambda Web Adapter / Non-HTTP Event Triggers
パス/events
に対して、HTTP POSTでリクエストを転送するようです。イベントの内容はHTTPボディとして送られ、レスポンスはJSONと
することになるようです。
あとは各言語やフレームワーク向けのサンプルがあります。
AWS Lambda Web Adapter / Examples
今回使いたいFastAPIはこちらですね。
https://github.com/awslabs/aws-lambda-web-adapter/tree/v0.8.4/examples/fastapi
ちなみに、AWS Lambda Web Adapter自体はRustで書かれているようです。
https://github.com/awslabs/aws-lambda-web-adapter/tree/v0.8.4/src
確かに実装内容としては、HTTP 1.1/1.0向けのWebアプリケーションであればなんでも動きそうですね。
参考)
AWS Lambda Web Adapterを活用する新しいサーバーレスの実装パターン - Speaker Deck
Lambda Web Adapter でウェブアプリを (ほぼ) そのままサーバーレス化する Lambda Web Adapter - 変化を求めるデベロッパーを応援するウェブマガジン | AWS
では、今回はFastAPIを使ってAWS Lambda Web Adapterを試してみます。
環境
今回の環境はこちら。
$ python3 --version Python 3.12.3 $ pip3 --version pip 24.0 from /usr/lib/python3/dist-packages/pip (python 3.12)
Docker。
$ docker version Client: Docker Engine - Community Version: 27.4.1 API version: 1.47 Go version: go1.22.10 Git commit: b9d17ea Built: Tue Dec 17 15:45:46 2024 OS/Arch: linux/amd64 Context: default Server: Docker Engine - Community Engine: Version: 27.4.1 API version: 1.47 (minimum version 1.24) Go version: go1.22.10 Git commit: c710b88 Built: Tue Dec 17 15:45:46 2024 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.7.24 GitCommit: 88bf19b2105c8b17560993bee28a01ddc2f97182 runc: Version: 1.2.2 GitCommit: v1.2.2-0-g7cb3632 docker-init: Version: 0.19.0 GitCommit: de40ad0
AWS SAM。
$ sam --version SAM CLI, version 1.132.0
FastAPIを使ってアプリケーションを作成する
まずはFastAPIを使ってアプリケーションを作成しましょう。
FastAPIのインストール。型チェック用のmypy、テスト用にpytestとhttpxもインストールしておきます。
$ pip3 install fastapi[standard] $ pip3 install mypy pytest httpx
インストールしたライブラリーの一覧。
$ pip3 list Package Version ----------------- ---------- annotated-types 0.7.0 anyio 4.7.0 certifi 2024.12.14 click 8.1.7 dnspython 2.7.0 email_validator 2.2.0 fastapi 0.115.6 fastapi-cli 0.0.7 h11 0.14.0 httpcore 1.0.7 httptools 0.6.4 httpx 0.28.1 idna 3.10 iniconfig 2.0.0 Jinja2 3.1.4 markdown-it-py 3.0.0 MarkupSafe 3.0.2 mdurl 0.1.2 mypy 1.14.0 mypy-extensions 1.0.0 packaging 24.2 pip 24.0 pluggy 1.5.0 pydantic 2.10.4 pydantic_core 2.27.2 Pygments 2.18.0 pytest 8.3.4 python-dotenv 1.0.1 python-multipart 0.0.20 PyYAML 6.0.2 rich 13.9.4 rich-toolkit 0.12.0 shellingham 1.5.4 sniffio 1.3.1 starlette 0.41.3 typer 0.15.1 typing_extensions 4.12.2 uvicorn 0.34.0 uvloop 0.21.0 watchfiles 1.0.3 websockets 14.1
requirements.txt
も作成しておきます。
$ pip3 freeze > requirements.txt
簡単なソースコードを作成。
main.py
from typing import Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class MessageRequest(BaseModel): message: str @app.get("/") def health_check() -> str: return "OK!" @app.get("/hello") def get_hello(message: Union[str, None] = None) -> dict: if message is None: msg = "Hello FastAPI" else: msg = message return {"messageFromGet": msg} @app.post("/hello") def post_hello(message: MessageRequest) -> dict: return {"messageFromPost": message.message}
開発サーバーを起動。
$ fastapi dev main.py
確認。
$ curl localhost:8000 "OK!" $ curl localhost:8000/hello {"messageFromGet":"Hello FastAPI"} $ curl localhost:8000/hello?message=test {"messageFromGet":"test"} $ curl -XPOST -H 'Content-Type: application/json' localhost:8000/hello -d '{"message": "Hello"}' {"messageFromPost":"Hello"}
OKですね。
テストも作成しておきましょう。
__init__.py
を作成。
$ touch __init__.py
テストコード。
test_main.py
from fastapi.testclient import TestClient from main import app client = TestClient(app) def test_healch_check() -> None: response = client.get("/") assert response.status_code == 200 assert response.text == '"OK!"' def test_get_hello() -> None: response = client.get("/hello") assert response.status_code == 200 assert response.json() == {"messageFromGet": "Hello FastAPI"} response = client.get("/hello", params={"message": "Hello Test FastAPI"}) assert response.status_code == 200 assert response.json() == {"messageFromGet": "Hello Test FastAPI"} def test_post_hello() -> None: response = client.post("/hello", json={"message": "Hello Test FastAPI"}) assert response.status_code == 200 assert response.json() == {"messageFromPost": "Hello Test FastAPI"}
これで準備は完了です。
AWS Lambda Web Adapterを含んだDockerイメージを作成する
それでは、こちらを参考にして作成したアプリケーションに対するDockerfile
を作成します。
https://github.com/awslabs/aws-lambda-web-adapter/blob/v0.8.4/examples/fastapi/app/Dockerfile
こんな感じにしました。なお、実際にAWS Lambda Web Adapterを使ったDockerfile
を書く時は、後述のAWS SAMで使う用に書き直したものを
見た方がよいかと思います。
Dockerfile
FROM public.ecr.aws/docker/library/python:3.12-slim COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter COPY requirements.txt ${LAMBDA_TASK_ROOT} COPY main.py ${LAMBDA_TASK_ROOT} RUN pip install -r requirements.txt --no-cache-dir ENTRYPOINT ["uvicorn"] CMD ["--host=0.0.0.0", "--port=8000", "main:app"]
AWS Lambda Web Adapterを加えているのはここです。
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
Dockerイメージをビルド。
$ docker image build --platform linux/amd64 -t kazuhira/hello-web-adapter:0.0.1 .
起動。
$ docker container run -it --rm --platform linux/amd64 -p 8000:8000 kazuhira/hello-web-adapter:0.0.1
ここでの動作確認結果は、curlの時と同じなので省略します。
ちなみに、こんな感じでアクセスしてもNot Foundになります。
$ curl localhost:8000/2015-03-31/functions/function/invocations -d '{"message": "Hello"}' {"detail":"Not Found"}
またDockerfile
でバインドするアドレスを0.0.0.0
にしていましたが、これを削除すると(ローカルのみからアクセス可とすると)
docker container run
ではアクセスできなくなります。
ENTRYPOINT ["uvicorn"] CMD ["--host=0.0.0.0", "--port=8000", "main:app"]
この場合は通常のWebアプリケーションとして使いましょう、ということですね。
AWS SAMで試す
続いては、AWS SAMで試してみましょう。こちらもローカルで動かします。
AWS SAMプロジェクトを作成。
$ sam init --name hello-sam-lambda-adapter --base-image amazon/python3.12-base --app-template hello-world-lambda-image --package-type Image --no-tracing --no-application-insights --structured-logging $ cd hello-sam-lambda-adapter
デフォルトで生成されているコードは削除しておきます。
$ rm -rf hello_world events tests
FastAPI用のディレクトリを作成し、ここで先ほど作成したPythonコードとrequirements.txt
、Dockerfile
をコピー。
$ mkdir fastapi $ cp /path/to/{main.py,requirements.txt,Dockerfile} fastapi
テストコードはここでは除外します。
Pythonコードは特に変えていませんが、Dockerfile
は以下のように書き直しました。
fastapi/Dockerfile
FROM public.ecr.aws/docker/library/python:3.12-slim COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter COPY requirements.txt ${LAMBDA_TASK_ROOT} COPY main.py ${LAMBDA_TASK_ROOT} ENV AWS_LWA_PORT=8000 # 以下でも意味は同じ # ENV PORT=8000 RUN pip install -r requirements.txt --no-cache-dir CMD exec uvicorn --port=${AWS_LWA_PORT} main:app
ポイントはここと
ENV AWS_LWA_PORT=8000 # 以下でも意味は同じ # ENV PORT=8000
ここですね。
CMD exec uvicorn --port=${AWS_LWA_PORT} main:app
AWS SAMをローカルで動作させる時はLambda Runtimeインターフェースが動作するのですが、これが8080ポートを使うので
AWS Lambda Web Adapterが期待するデフォルトのポート(8080)から変更した方が無難です。
AWS Lambda Web Adapter / Local Debugging
それで環境変数AWS_LWA_PORT
を使っています。
ENV AWS_LWA_PORT=8000 # 以下でも意味は同じ # ENV PORT=8000
なお、AWS_LWA_PORT
はPORT
でも代替することができます。
AWS Lambda Web Adapter / Configurations
これに気づいた後に、サンプルのDockerfile
がポートを環境変数PORT
で指定している意味がわかりました。
https://github.com/awslabs/aws-lambda-web-adapter/blob/v0.8.4/examples/fastapi/app/Dockerfile
そしてCMD
で環境変数を使おうと思うと、こうなります。
CMD exec uvicorn --port=${AWS_LWA_PORT} main:app
Dockerfile
的にはあまりよろしくない書き方な気がしますが、仕方ないでしょう…。
またdocker container run
で使っていた時とは異なり、0.0.0.0
へのバインドは不要になります。
AWS SAMのテンプレートファイルはこのようにしました。
template.yaml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > python3.12 Sample SAM Template for hello-sam-lambda-adapter Globals: Function: Timeout: 3 LoggingConfig: LogFormat: JSON Resources: FastApiFunction: Type: AWS::Serverless::Function Properties: PackageType: Image Architectures: - x86_64 Events: GetHello: Type: Api Properties: Path: /hello Method: get PostHello: Type: Api Properties: Path: /hello Method: post Metadata: Dockerfile: Dockerfile DockerContext: ./fastapi DockerTag: 0.0.1 Outputs: FastApi: Description: API Gateway endpoint URL for Prod stage for FastApi function Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" FastApiFunction: Description: FastApi Lambda Function ARN Value: !GetAtt FastApiFunction.Arn FastApiFunctionIamRole: Description: Implicit IAM Role created for FastApi function Value: !GetAtt FastApiFunctionRole.Arn
ひとつのAWS Lambda関数に、複数のAmazon API Gatewayのイベントを割り当てた感じですね。
FastApiFunction: Type: AWS::Serverless::Function Properties: PackageType: Image Architectures: - x86_64 Events: GetHello: Type: Api Properties: Path: /hello Method: get PostHello: Type: Api Properties: Path: /hello Method: post Metadata: Dockerfile: Dockerfile DockerContext: ./fastapi DockerTag: 0.0.1
ではビルド。
$ sam build
Dockerイメージができました。
Successfully tagged fastapifunction:0.0.1
Amazon API Gatewayのエミュレーターを起動します。
$ sam local start-api
確認。
$ curl localhost:3000/hello {"messageFromGet":"Hello FastAPI"} $ curl localhost:3000/hello?message=test {"messageFromGet":"test"} $ curl -XPOST -H 'Content-Type: application/json' localhost:3000/hello -d '{"message": "Hello"}' {"messageFromPost":"Hello"}
OKですね。
Amazon API Gateway(のエミュレーターですが)経由でも、FastAPIを使って作成したAWS Lambda関数を呼び出せることが確認できました。
おわりに
AWS Lambda Web AdapterをFastAPIを使って試してみました。
1度試しておきたいなと思っていたので、今回どういうものかざっくり把握できたので良かったです。
どちらかというとDockerfile
の書き方で苦労したりしましたけど(笑)。