📑

FastAPI,mangumをコンテナ化してAPIにする

2023/02/02に公開

過去ログ

1回目

https://zenn.dev/shigeru_oda/articles/f1d360786c41568d16fa

2回目

https://zenn.dev/shigeru_oda/articles/ee2ec43507a62f3e7c1d

やりたいこと

前回やれたこと

構成図

2回目の記事でFastAPI,mangumを利用して、swagger-ui上でのAPI管理ができるようになりました。これらをコンテナ化した上で、Lambda+API Gatewayを作ってテストを行います。
SAMのDeployではなくCLIでのデプロイとしています。SAMは次回にでも。

ファイル/フォルダ構成

sam-app-1
- events
  |- event.json
- hello_world
  |- Dockerfile <- 新規作成
  |- app.py
  |- __init__.py
  |- requirements.txt
- tests
  |- __init__.py
  |- integration
    |- __init__.py
    |- test_api_gateway.py
  |- requirements.txt
  |- unit
    |- __init__.py
    |- test_api_gateway.py
- __init__.py
- README.md
- response.json
- samconfig.toml
- template.yaml

ファイル準備

Dockerfile

./sam-app-1/hello_world/Dockerfileを作成します。
基本的な記載方法はAWS公式に従いますが、pythonのVersionは3.9を利用します。
またCMDはapp.pyに記載されたハンドラーの名前となります

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-create.html#images-create-from-base

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

# Install the function's dependencies using file requirements.txt
# from your project folder.

COPY requirements.txt  .
RUN  pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"

# Copy function code
COPY app.py ${LAMBDA_TASK_ROOT}

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "app.lambda_handler" ] 

app.py

変更点は無いですが、再度掲載

from fastapi import FastAPI
from mangum import Mangum

app = FastAPI()

@app.get("/hello")
def root():
    return {"message": "Hello World"}

lambda_handler = Mangum(app)

requirements.txt

変更点は無いですが、再度掲載

requests
fastapi
mangum

環境構築

ECRリポジトリ準備

aws ecr create-repository --repository-name hello_world 

Docker準備

# AWS情報取得
REGION=$(aws configure get region)
ACCOUNTID=$(aws sts get-caller-identity --output text --query Account)

# build
docker build -t hello_world .

# tag設定
docker tag hello_world:latest ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/hello_world:latest

# ECRログイン
aws ecr get-login-password | docker login --username AWS --password-stdin ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com

# ECRへpush
docker push ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/hello_world:latest

IAM準備

権限の問題でCloud9ではエラーとなるので、ここはCloudShellで実行下さい

# Role作成
aws iam create-role --role-name lambda-hello_world \
--assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'

Lambda準備

# Role変数設定
ROLE_ARN=arn:aws:iam::${ACCOUNTID}:role/lambda-hello_world

# DIGEST取得
DIGEST=$(aws ecr list-images --repository-name hello_world --out text --query 'imageIds[?imageTag==`latest`].imageDigest')

# Lambda関数作成
aws lambda create-function \
  --function-name hello_world \
  --package-type Image \
  --code ImageUri=${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/hello_world@${DIGEST} \
  --role ${ROLE_ARN}

Lambdaテスト

実行結果の"body"が{"message":"Hello World"}であることを確認できます

# payload準備
cat << EOF > payload.json
{
  "resource": "/",
  "path": "/hello",
  "httpMethod": "GET",
  "isBase64Encoded": true,
  "queryStringParameters": {
    "foo": "bar"
  },
  "requestContext": {
    "httpMethod": "GET"
  }
}
EOF

# Lambda実行
aws lambda invoke \
  --function-name hello_world \
  --payload file://payload.json \
  --cli-binary-format raw-in-base64-out \
  response.json

# レスポンス確認
cat response.json

# ゴミ削除
rm payload.json response.json

API Gateway作成

# REST APIの作成
aws apigateway create-rest-api \
  --name 'api_hello_world' \
  --description 'api_hello_world' \
  --endpoint-configuration types=REGIONAL

# API IDの取得
apigateway_id=$(aws apigateway get-rest-apis \
  --query 'items[?name==`api_hello_world`].id' \
  --output text);echo ${apigateway_id}

# リソース IDの取得
resource_id=$(aws apigateway get-resources \
  --rest-api-id ${apigateway_id} \
  --query 'items[?path==`/`].id' \
  --output text);echo ${resource_id}

# リソースの作成
aws apigateway create-resource \
  --rest-api-id ${apigateway_id} \
  --parent-id ${resource_id} \
  --path-part '{proxy+}'

# リソース IDの取得
resource_id_proxy=$(aws apigateway get-resources \
  --rest-api-id ${apigateway_id} \
  --query 'items[?path==`/{proxy+}`].id' \
  --output text);echo ${resource_id_proxy}

# メソッドの作成
aws apigateway put-method \
  --rest-api-id ${apigateway_id} \
  --resource-id ${resource_id_proxy} \
  --http-method GET \
  --authorization-type "NONE" \
  --no-api-key-required 

# インテグレーションの設定
aws apigateway put-integration \
  --rest-api-id ${apigateway_id} \
  --resource-id ${resource_id_proxy} \
  --http-method GET \
  --type AWS_PROXY \
  --integration-http-method POST \
  --uri "arn:aws:apigateway:${REGION}:lambda:path/2015-03-31/functions/arn:aws:lambda:${REGION}:${ACCOUNTID}:function:hello_world/invocations"

# Lambda関数の実行権限付与
aws lambda add-permission \
  --function-name hello_world \
  --statement-id hello_world \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:${REGION}:${ACCOUNTID}:${apigateway_id}/*/*/*"

# APIのデプロイ
aws apigateway create-deployment \
  --rest-api-id ${apigateway_id} \
  --stage-name default \
  --stage-description default \
  --description default

APIテスト

実行(成功例)

URI="https://${apigateway_id}.execute-api.${REGION}.amazonaws.com/default/hello"
curl -H "Content-Type: application/json" -X GET ${URI}

結果(成功例)

{"message":"Hello World"}

実行(失敗例)

URI="https://${apigateway_id}.execute-api.${REGION}.amazonaws.com/default/hello-test"
curl -H "Content-Type: application/json" -X GET ${URI}

結果(失敗例)

{"detail":"Not Found"}

まとめ

FastAPI,mangumでswagger-uiでAPIを管理しつつ、コンテナ化まで出来ました。

参考

https://aws.amazon.com/jp/builders-flash/202103/new-lambda-container-development/?awsf.filter-name=*all

Discussion