CLOVER🍀

That was when it all began.

WebアプリケーションフレームワークをAWS Lambda関数で使えるAWS Lambda Web Adapterを、FastAPIかつローカルで試す

これは、なにをしたくて書いたもの?

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 GatewayRest 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が拡張機能として起動した後に、AWS Lambda上で構築されたWebアプリケーションが起動する
という流れになるようです。

起動したかどうかはヘルスチェックで確認するようですね(環境変数でポートとパスを変更可能)。

AWS Lambda Web Adapter / Readiness Check

デフォルトではGET /に対して応答し、ポート8080でリッスンするように構成した方がよいでしょう。

その他、設定は環境変数で行うようです。

AWS Lambda Web Adapter / Configurations

AWS Lambdaでのコンテキストについて。

シャットダウン時にはSIGTERMシグナルが送信されるようなので、Webアプリケーション側でシグナルをハンドリングすることで安全に
シャットダウンができるとされています。

AWS Lambda Web Adapter / Graceful Shutdown

ローカルでの実行は、AWS SAMの利用が挙げられていますね。

AWS Lambda Web Adapter / Local Debugging

非HTTPイベント(Amazon SQS、Amazon SNSAmazon DynamoDBAmazon KinesisAmazon 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.txtDockerfileをコピー。

$ 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_PORTPORTでも代替することができます。

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の書き方で苦労したりしましたけど(笑)。