📝

FastAPIをLambda/API Gatewayで動かそう

2024/07/11に公開

概要

FastAPIでREST APIを作成しました。
本番はAWSのLambda/API Gatewayで運用します。
今後、新しいアプリを作成した時に設定を使いまわしたいので、CDKで書きます。

実装

CDKアプリケーションの作成

FastAPIのルート直下にcdkディレクトリを作成し管理することにしました。
サンプルコードが多いため、CDKの言語はTypeScriptを採用します。

mkdir cdk                                           

cd cdk

cdk init app --language typescript

Lambda・API Gatewayの設定

公式ドキュメントを参考にして実装してみました。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigatewayv2_integrations-readme.html

@aws-cdk/aws-apigatewayv2 を使用すると、「Argument type Function is not assignable to parameter type IFunction」型エラーを解決できなかったので、@aws-cdk/aws-apigatewayv2-alpha を使用しました。

import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha";
import { HttpMethod, HttpApi } from "@aws-cdk/aws-apigatewayv2-alpha";

export class CdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const fastApiLambda = new lambda.Function(this, "FastApiLambda", {
      runtime: lambda.Runtime.PYTHON_3_12,
      handler: "main.app",
      code: lambda.Code.fromAsset("../app"),
    });

    const fastApiIntegration = new HttpLambdaIntegration(
      "FastApiIntegration",
      fastApiLambda
    );

    const httpApi = new HttpApi(this, "HttpApi");

    httpApi.addRoutes({
      path: "/{proxy+}",
      methods: [HttpMethod.ANY],
      integration: fastApiIntegration,
    });
  }
}

Mangum

FastAPIのエンドポイントとLambda関数を紐づけていきます。
Lambdaは1関数:1エンドポイントですが、1関数:複数エンドポイントを持つようにしてくれるmangum をインストールします。

pip install mangum

appmangumに取り付けるだけです

main.py
app = FastAPI()

@app.get("/test1", status_code=200)
async def root():
    return {"message": "Successfully API test1"}

@app.get("/test2", status_code=200)
async def root():
    return {"message": "Successfully API test2"}

handler = Mangum(app)

依存関係用意

Lambdaにデプロイしても、fastapi, mangumがないと怒られます。
パッケージ化してLambdaのレイヤーに設定する必要があります。

パッケージ化

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-package.html

毎回決まったコマンドなので、シェルスクリプトを用意しました。

layer.sh
mkdir python

pip install \
--platform manylinux2014_aarch64 \
--target=python/python/lib/python3.12/site-packages/ \
--implementation cp \
--python-version 3.12 \
--only-binary=:all: --upgrade \
-r requirements.txt

cd python

zip -r ../fastapi-layer.zip .

cd ..

rm -rf python

// ディレクトリ構成
app-root/
├──app/
├──cdk/
└──layer.sh

LayerをLambdaに設定してあげます。

    const layer = new lambda.LayerVersion(this, "CustomLayer", {
      code: lambda.Code.fromAsset("../app/fastapi-layer.zip"), // 作成したパッケージ名
      compatibleRuntimes: [lambda.Runtime.PYTHON_3_12],
      description: "FastAPI and Mangum dependencies",
      compatibleArchitectures: [Architecture.ARM_64],
    });

    const fastApiLambda = new lambda.Function(this, "FastApiLambda", {
      runtime: lambda.Runtime.PYTHON_3_12,
      handler: "main.handler", // エントリーポイントを変更
      code: lambda.Code.fromAsset("../app"),
      layers: [layer], // 追加
      architecture: Architecture.ARM_64,
    });

テスト用に作成したエンドポイントに向かってGETリクエストを投げてみると成功!

備考

Lambdaのテスト方法

  • GETであればブラウザから実行
  • Lambdaのテストイベントでテンプレート「apigateway-aws-proxy」を選択してpath, httpMethodを変更して実行

FastAPIディレクトリ構成

ディレクトリ構成に悩んだため、備忘録として残しておきます。

app-root/
├──app/
│   ├──routers/
│   ├──main.py
├──cdk/
└──venv/

routersからimportしたい。

main.py
from app.routers import user
from fastapi.responses import JSONResponse

app = FastAPI()

app.include_router(user.router)

Lambda実行時に発生したエラー

Unable to import module 'main': No module named 'app’

事象:appディレクトリがLambdaにアップロードされず見つからない。routersmain.py がルートディレクトリに置かれる。

from .routers と相対パスにしても「Unable to import module 'main': attempted relative import with no known parent package」でダメでした。

最終的にapp-rootから必要ないディレクトリを除外してアップロードすることで解決しました。
もっと効率がいい方法ありそう。

code: lambda.Code.fromAsset("../../app-root", {
        exclude: ["cdk", "venv", "fastapi-layer.zip"],
      }),

Discussion