本サイトでは過去の記事でLambda関数の作成方法を確認し、Lambda関数でDockerを利用する手順などを公開しました。本記事ではさらにDocker上でPythonのWebフレームワークであるFastAPIをインストールして複数のエンドポイントを持つサーバレスなアプリケーションを構築する方法を確認していきます。

最終的にLambda関数の関数URL(FunctionURL)を利用してインターネット上から複数のエンドポイントを使ってLambda関数が実行できるようになります。

FastAPIとは

FastAPIは主にAPIを構築する際に利用され、高速で構築が簡単に行えるPythonペースのWEBフレームワークです。

FastAPIは名前にAPIが含まれていることから分かる通りAPIを構築するためのフレームワークです。そのためFastAPIには複数のエンドポイントの設定を行います。FastAPIで構築する複数のエンドポイントはLambda上でASGIアプリケーションを実行するためのAdapterであるMangumを利用して実現します。

事前準備

本文書はmacOSを利用して動作確認を行っています。

Dockerを利用するためDockerがインストール済みである必要があります。インストールが完了している場合にはdockerコマンドを利用してバージョンを確認することができます。


% docker -v
Docker version 25.0.2, build 29cf629
 % which docker
/Users/mac/.docker/bin/docker

Dockerのイメージ、コンテナを利用して動作確認を行うので任意の場所にディレクトリを作成してください。ここではlambda_fastapiという名前のディレクトリを作成しています。


 % mkdir lambda_fastapi
 % cd lambda_fastapi

Dockerイメージの作成

lambda_fastapiディレクトリに移動後、app.pyファイルを作成します。app.pyファイル内でFastAPIを動作させるためのコードを記述します。FastAPI, Mangumをimportして異なる方法で動作確認を行うため3つのルーティングを追加しています。


from fastapi import FastAPI
from pydantic import BaseModel
from mangum import Mangum

class Item(BaseModel):
    name: str
    price: float

app = FastAPI()

@app.get('/')
async def root():
  return {"message": "Hello from lambda"}

@app.get("/uppercase")
async def uppercase(text: str):
    return {"message": text.upper()}

@app.post("/items")
async def create_item(item: Item):
    return item

handler = Mangum(app)

1つ目のルーティングはGETリクエストがあるとJSONデータを戻します。2つ目のルーティングuppercaseはGETリクエストからクエリパラメータを受け取り大文字に変換してJSONデータとして戻します。3つ目はルーティングitemsはPOSTリクエストでnameとpriceを含んだJSONデータを受け取り、そのまま受け取ったデータをJSONデータで戻します。

app.pyファイルでimportしたFastAPI, Mangumをインストールする必要があるのでrequirements.txtファイルを作成します。


fastapi
mangum

Dockerイメージを作成するためにDockerfileを作成します。FROMではAWSが提供しているPythonのDockerイメージpublic.ecr.aws/lambda/python:3.12を指定しています。COPYで作成済みのapp.py、requirements.txtファイルをイメージにコピーしています。RUNではpipを利用してrequirements.txtに記述したパッケージのインストールを行っています。CMDではapp.pyファイルのhandler関数を実行しています。


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

COPY app.py ${LAMBDA_TASK_ROOT}
COPY requirements.txt ${LAMBDA_TASK_ROOT}

RUN pip install -r ./requirements.txt

CMD ["app.handler"]

Dockerfileの作成が完了したらイメージのビルドを行います。


% docker build -t lambda_fastapi .

イメージのビルドが完了したらdocker imagesコマンドを実行して作成したイメージを確認しておきます。


 % docker images
REPOSITORY                                                            TAG       IMAGE ID       CREATED          SIZE
lambda_fastapi                                                        latest    2da16d740ab7   19 seconds ago   575MB

動作確認

作成したイメージを元にコンテナを作成して起動を行い、ローカル環境で設定通りの動作が行われるのか確認を行います。public.ecr.aws/lambda/python:3.12のイメージの中にLambda Runtime Interface Emulator(RIE)が含まれているためローカルでLambda関数の動作確認を行うことができます。RIE が8080ポートでリスニングをするためポートマッピングで 9000 ポートにマッピングしています。


 % docker run -p 9000:8080 lambda_fastapi   
15 Feb 2024 00:11:18,872 [INFO] (rapid) exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)

コマンドを実行するとhttp://localhost:9000/2015-03-31/functions/function/invocationsのエンドポイントが作成されます。ローカル環境の動作確認ではそのエンドポイントに対してcurlを利用してリクエストを送信します。

別のターミナルを開いてdocker psコマンドを実行すると起動しているコンテナの情報を確認することができます。コンテナ名はfervent_burnellと自動で命名されています。


 % docker ps
CONTAINER ID   IMAGE            COMMAND                   CREATED          STATUS          PORTS                    NAMES
383521cb230d   lambda_fastapi   "/lambda-entrypoint.…"   35 seconds ago   Up 33 seconds   0.0.0.0:9000->8080/tcp   fervent_burnell

コンテナが起動していることが確認できたら、curlを利用して3つのルーティングに対してリクエストを送信します。

GETリクエスト

最初に”/”のルーティングに対してGETリクエストを送信します。-dオプションに設定したresource, path, httpMethod, requestContextのいずれかが抜けているとエラーメッセージが表示されます。実行するとResponseのbodyの中に”Hello from lamdba”が入っていることがわかります。


% curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"resource": "/", "path": "/", "httpMethod": "GET", "requestContext": {}}'

{"statusCode": 200, "headers": {"content-length": "31", "content-type": "application/json"}, "multiValueHeaders": {}, "body": "{\"message\":\"Hello from lambda\"}", "isBase64Encoded": false}%    

/uppercaseルーティングはクエリパラメータを利用してKeyとValueを渡す必要があるためqueryStringParametersを設定します。queryStringParametersに設定した”Hello from lambda”が大文字になって戻されていることが確認できます。


% curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"resource": "/uppercase", "path": "/uppercase", "httpMethod": "POST", "requestContext": {}, "queryStringParameters": {"text": "Hello from lambda"}}' 

{"statusCode": 200, "headers": {"content-length": "31", "content-type": "application/json"}, "multiValueHeaders": {}, "body": "{\"message\":\"HELLO FROM LAMBDA\"}", "isBase64Encoded": false}%    

POSTリクエスト

/itemsルーティングに対してはこれまでの2つは異なりPOSTリクエスト(httpMethodの値がPOST)を利用します。/itemsのルーティングに送信するデータはbodyで設定を行います。実行するとbodyに設定したnameとpriceを含めJSONデータがそのまま戻されていることが確認できます。


% curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" \
-d '{
  "resource": "/items",
  "path": "/items",
  "httpMethod": "POST",
  "requestContext": {},
  "body": "{\"name\": \"Keyboard\",\"price\":10000}"
}'

{"statusCode": 200, "headers": {"content-length": "32", "content-type": "application/json"}, "multiValueHeaders": {}, "body": "{\"name\":\"Keyboard\",\"price\":10000.0}", "isBase64Encoded": false}%     

httpMethodの値をGETにして送信すると”Method Not Allowed”というメッセージが戻されます。


% curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" \
-d '{
  "resource": "/items",
  "path": "/items",
  "httpMethod": "GET", 
  "requestContext": {},
  "body": "{\"name\": \"Keyboard\",\"price\":10000}"
}'
{"statusCode": 405, "headers": {"allow": "POST", "content-length": "31", "content-type": "application/json"}, "multiValueHeaders": {}, "body": "{\"detail\":\"Method Not Allowed\"}", "isBase64Encoded": false}%     

bodyにnameにしか設定していない場合にはバリデーションが行われ必須項目が不足しているということでField requiredのメッセージが戻されます。ステータスコードは422 Unprocessable Entityです。


% curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" \
-d '{
  "resource": "/items",
  "path": "/items",
  "httpMethod": "POST", 
  "requestContext": {},
  "body": "{\"name\": \"Keyboard\"}"
}'

{"statusCode": 422, "headers": {"content-length": "155", "content-type": "application/json"}, "multiValueHeaders": {}, "body": "{\"detail\":[{\"type\":\"missing\",\"loc\":[\"body\",\"price\"],\"msg\":\"Field required\",\"input\":{\"name\":\"Keyboard\"},\"url\":\"https://errors.pydantic.dev/2.6/v/missing\"}]}", "isBase64Encoded": false}

ローカルの動作確認では期待通りの動作になることが確認できたのでこれからビルドしたイメージを利用してLambda関数を設定していきます。

Lambda関数の設定

ECRのリポジトリの作成

Lambda関数はAWS ECR(Elastic Container Registry)に保存したDockerイメージを利用します。そのため作成したDockerイメージを保存するリポジトリの作成を行います。ここでは新たにリポジトリを作成して作成したイメージをアップロードします。

ECRにリポジトリを作成する前にECRにログインを行います。123456789012の部分を各自のAWSのアカウントIDに変更して実行してください。


  % aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com

An error occurred (AccessDeniedException) when calling the GetAuthorizationToken operation: User: arn:aws:iam::123456789012:user/johndoe is not authorized to perform: ecr:GetAuthorizationToken on resource: * because no identity-based policy allows the ecr:GetAuthorizationToken action
Error: Cannot perform an interactive login from a non TTY device

IAMユーザに権限(ecr:GetAuthorizationToken)がない場合には上記のようにエラーメッセージが表示されます。

権限があり正しいコマンドで実行した場合には”Login Succeded”のメッセージが表示されます。


  % aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com
Login Succeeded

権限がない場合はECRを利用できるポリシー”AmazonEC2ContainerRegistryFullAccess”をユーザにアタッチします。

aws ecr get-login-password –region ap-northeast-1のみ実行するとTokenが戻され、–password-stdinに取得したTokenが渡されログイン処理が行われます。

ログイン完了後はcreate-repositoryコマンドを利用してlambda_fastapi_repoという名前のリポジトリを作成しています。リポジトリ名には任意の名前をつけることができます。ECRはプライベートとパブリック用のリポジトリを作成することができますが下記のコマンドではプライベート用のリポジトリを作成しています。


%  aws ecr create-repository --repository-name lambda_fastapi_repo --image-scanning-configuration scanOnPush=true --image-tag-mutability MUTABLE
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:123456789012:repository/lambda_fastapi_repo",
        "registryId": "123456789012",
        "repositoryName": "lambda_fastapi_repo",
        "repositoryUri": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda_fastapi_repo",
        "createdAt": "2024-02-15T10:05:34.335000+09:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": true
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}   

イメージのアップロード

リポジトリを作成後は作成済みのイメージにタグをつけるためdocker tagコマンドを実行します。イメージをECRにアップロードするためにはタグ付けは必須です。123456789012の箇所は各自のAWSのアカウントIDを指定してください。


 % docker tag lambda_fastapi:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda_fastapi_repo:latest
//書式
 % docker tag イメージ名: 作成したECRのリポジトリ名:

docker tagを実行後にdocker imagesコマンドを実行すると名前の異なる2つのイメージが同じIDで作成されていることがわかります。


 % docker images
REPOSITORY                                                              TAG       IMAGE ID       CREATED          SIZE
123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda_fastapi_repo   latest    2da16d740ab7   12 minutes ago   575MB
lambda_fastapi                                                          latest    2da16d740ab7   12 minutes ago   575MB  

タグ付けしたイメージのアップロードにはdocker pushコマンドを利用します。タグ付け後に作成されているイメージの名前を指定してください。


 % docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda_fastapi_repo:latest
The push refers to repository [123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda_fastapi_repo]
dc2a2155744d: Pushed 
63a762db1c25: Pushed 
931a33792f91: Pushed 
b2fbcdbc3abe: Pushed 
01237e4b624b: Pushed 
7393ae547845: Pushed 
08352d8f528a: Pushed 
4ad08681a382: Pushed 
8a302ef602af: Pushed 
latest: digest: sha256:aebf1e5218cdb9875bb2abcfb7715b048fafa2b7fcb869230dd9719ecc1fc321b size: 2203

作成後はdescribe-repositoriesで作成したリポジトリの情報を確認することができます。


% aws ecr describe-repositories
{
    "repositories": [
        {
            "repositoryArn": "arn:aws:ecr:ap-northeast-1:123456789012:repository/lambda_fastapi_repo",
            "registryId": "123456789012",
            "repositoryName": "lambda_fastapi_repo",
            "repositoryUri": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda_fastapi_repo",
            "createdAt": "2024-02-15T10:05:34.335000+09:00",
            "imageTagMutability": "MUTABLE",
            "imageScanningConfiguration": {
                "scanOnPush": true
            },
            "encryptionConfiguration": {
                "encryptionType": "AES256"
            }
        }
    ]
}

Lambda関数の作成

ECRにイメージがアップロードが完了したのでManagement ConsoleからLambda関数を作成します。

関数の作成画面のオプションではコンテナイメージを選択してください。コンテナイメージを選択するとイメージを参照ボタンから作成したリポジトリのイメージが選択できるようになるので選択してください。Lambda関数の名前はlambdaFastAPIとしています。

動作確認でLambda関数から別のサービスを利用するわけではないので設定するロールがない場合はロールには基本的なLambdaアクセス権限で新しいロールを作成を選択してください。自動で命名されたロールが追加されて設定されます。

関数の作成画面
関数の作成画面

Lambda関数を設定後、AWS CLIを利用して作成したLambd関数を実行するとUnhandledエラーが発生します。


% aws lambda invoke --function-name lambdaFastAPI response.json                          
{
    "StatusCode": 200,
    "FunctionError": "Unhandled",
    "ExecutedVersion": "$LATEST"
}

関数URLの設定

URLを利用してLambda関数が実行できるように関数URLの設定を行います。Management Consoleから作成したLambda関数のページに移動して設定タブから関数URLを設定クリックします。

関数URLのタブが開いたら関数URLを作成ボタンをクリックします。

作成したLambda関数のページ
作成したLambda関数のページ

関数URLの設定画面が表示されるのでLambda関数のURLからパブリックからアクセスできるように”NONE”を選択して”保存”ボタンをクリックします。

関数URLの設定画面
関数URLの設定画面

設定したURLが表示されているので関数URLのリンクをクリックしてください。

設定した関数URLの確認
設定した関数URLの確認

ブラウザ上にapp.pyで設定した”/”の戻り値が表示されればLambda関数が正常に動作していることになります。

fastAPIから戻されるJSONデータ
fastAPIから戻されるJSONデータ

/docsにアクセスするとルーティングの一覧を確認することができます。ここから各ルーティングに対しての動作確認を行うこともできます。

OpenAPIの表示
OpenAPIの表示

パブリックで公開しているのでどこからでもアクセスすることができるのでローカルのPCからcurlを利用して公開済みのLambda関数のURL+/itemsにPOSTリクエストを送信します。送信したデータがJSONデータで戻されることが確認できます。


% curl -X 'POST' \
  'https://b7vu2aqg2k3chtejqieuevkfeq0fzkmt.lambda-url.ap-northeast-1.on.aws/items' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Keyboard",
  "price": 10000
}'
{"name":"Keyboard","price":10000.0}%

app.pyファイルで設定した複数のエンドポイントに対してLambda関数を利用して実行できるようになりました。