1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS Lambda + FastAPI — サーバーレスAPIを作った

1
Posted at

はじめに

ECSでFastAPIをデプロイする方法は前の記事で書いた。

今回はもう一つの選択肢であるAWS LambdaでFastAPIを動かす方法を試した。ECSはコンテナが常時起動しているので費用が発生し続けるが、Lambdaはリクエストが来たときだけ実行されるのでリクエストが少ないAPIには向いている。

Lambdaで通常のWSGI/ASGIフレームワークを動かすにはMangumというアダプターが必要なのでそこから整理した。


LambdaとECSの使い分け

Lambda が向いているとき:
  ✓ リクエストが少ない(月数万〜数十万件程度)
  ✓ 常時起動のコストを避けたい
  ✓ バースト的なトラフィック(急増・急減がある)
  ✓ 小さなAPIやWebhookの受信

ECS が向いているとき:
  ✓ 常時高いトラフィック
  ✓ コールドスタートが許容できない(レスポンスタイムが厳しい)
  ✓ WebSocket接続が必要
  ✓ 長時間実行するタスク(Lambdaは最大15分)

Mangum — LambdaとFastAPIをつなぐアダプター

LambdaはHTTPリクエストをそのまま受け取るのではなく、API GatewayやFunctionURLを通じてLambda固有のイベント形式で受け取る。MangumはそのイベントをASGI形式に変換するアダプター。

pip install fastapi mangum
# main.py
from fastapi  import FastAPI
from mangum   import Mangum

app = FastAPI(
    title       = "My API",
    root_path   = "/prod",  # API Gatewayのステージ名
)

@app.get("/")
def root():
    return {"message": "Hello from Lambda!"}

@app.get("/users")
def list_users():
    return [{"id": 1, "name": "田中"}]

@app.get("/users/{user_id}")
def get_user(user_id: int):
    return {"id": user_id, "name": "田中"}

# LambdaのハンドラーとしてMangumを設定
handler = Mangum(app, lifespan="off")

handler = Mangum(app)の一行を追加するだけでFastAPIがLambdaで動く。lifespan="off"はLambda環境でのライフサイクルイベントを無効化する設定(Lambdaはサーバーが起動し続けないため)。


デプロイ方法

パターン① ZIP形式でデプロイ

# 依存関係をローカルにインストール
pip install fastapi mangum -t ./package

# アプリのコードをコピー
cp main.py ./package/

# ZIPに圧縮
cd package && zip -r ../function.zip .

# Lambdaにデプロイ
aws lambda update-function-code \
    --function-name my-fastapi \
    --zip-file fileb://../function.zip

パターン② コンテナイメージでデプロイ(推奨)

依存ライブラリが多い場合はコンテナイメージのほうが管理しやすい。

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

# 依存関係をインストール
COPY requirements.txt .
RUN pip install -r requirements.txt --no-cache-dir

# アプリのコードをコピー
COPY main.py .

# Lambdaハンドラーを指定
CMD ["main.handler"]
# ECRにプッシュ
aws ecr create-repository --repository-name my-fastapi

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

docker build -t my-fastapi .
docker tag my-fastapi:latest \
    123456789.dkr.ecr.ap-northeast-1.amazonaws.com/my-fastapi:latest
docker push \
    123456789.dkr.ecr.ap-northeast-1.amazonaws.com/my-fastapi:latest

# LambdaをECRのイメージから作成
aws lambda create-function \
    --function-name my-fastapi \
    --package-type Image \
    --code ImageUri=123456789.dkr.ecr.ap-northeast-1.amazonaws.com/my-fastapi:latest \
    --role arn:aws:iam::123456789:role/lambda-execution-role \
    --timeout 30 \
    --memory-size 512

API GatewayとFunctionURLの違い

LambdaをHTTPで公開する方法が2つある。

Lambda Function URL(シンプル)

# Function URLを作成(認証なし)
aws lambda create-function-url-config \
    --function-name my-fastapi \
    --auth-type NONE \
    --cors '{
        "AllowOrigins": ["https://myapp.example.com"],
        "AllowMethods": ["GET","POST","PUT","DELETE"],
        "AllowHeaders": ["Content-Type","Authorization"]
    }'
# Function URLを使う場合のMangum設定
handler = Mangum(app, lifespan="off")

設定が簡単。カスタムドメインが不要・細かいルーティングが不要な場合はこちら。

API Gateway(高機能)

# API Gatewayを使う場合
handler = Mangum(
    app,
    lifespan  = "off",
    api_gateway_base_path = "/prod",  # API Gatewayのステージ
)
# API Gatewayを作成
aws apigatewayv2 create-api \
    --name my-fastapi \
    --protocol-type HTTP \
    --target arn:aws:lambda:ap-northeast-1:123456789:function:my-fastapi
API Gatewayの機能:
  ✓ カスタムドメイン
  ✓ レート制限
  ✓ 認証(Cognito/JWT)
  ✓ WAFとの統合
  ✓ 複数のLambdaへのルーティング

環境変数とSecrets Manager

# main.py
import os
import json
import boto3
from functools import lru_cache

@lru_cache(maxsize=1)
def get_database_url() -> str:
    """Secrets Managerからデータベース接続情報を取得(キャッシュ付き)"""
    client = boto3.client('secretsmanager', region_name='ap-northeast-1')
    secret = client.get_secret_value(SecretId=os.environ['DB_SECRET_ARN'])
    creds  = json.loads(secret['SecretString'])
    return (
        f"postgresql://{creds['username']}:{creds['password']}"
        f"@{creds['host']}:{creds['port']}/{creds['dbname']}"
    )

from sqlalchemy     import create_engine
from sqlalchemy.orm import sessionmaker

def get_db():
    engine = create_engine(get_database_url())
    SessionLocal = sessionmaker(bind=engine)
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@lru_cacheを使うとLambdaの同じ実行環境内でSecrets Managerへのリクエストをキャッシュできる。毎回Secrets Managerを叩かなくて済むのでレイテンシが減る。


Lambda Layerの活用

依存ライブラリが大きい場合、Lambda Layerとして分離できる。

# Layerの作成
mkdir python
pip install fastapi mangum sqlalchemy psycopg2-binary -t python/
zip -r layer.zip python/

aws lambda publish-layer-version \
    --layer-name fastapi-deps \
    --zip-file fileb://layer.zip \
    --compatible-runtimes python3.12
# LambdaにLayerをアタッチ
aws lambda update-function-configuration \
    --function-name my-fastapi \
    --layers arn:aws:lambda:ap-northeast-1:123456789:layer:fastapi-deps:1

コンテナイメージを使う場合はLayerは不要。ZIP形式のデプロイでライブラリが50MBを超える場合にLayerが有効。


コールドスタート対策

Lambdaの一番の課題がコールドスタート。初回リクエスト時に実行環境を起動する時間がかかる。

# main.py — グローバルスコープで初期化してウォームスタートを活用

# ✓ グローバルで初期化(コンテナ起動時に1回だけ実行)
app    = FastAPI()
engine = None  # 遅延初期化

def get_engine():
    global engine
    if engine is None:
        engine = create_engine(get_database_url())
    return engine

# ✗ ハンドラー内で毎回初期化(リクエストのたびに実行)
def handler(event, context):
    engine = create_engine(...)  # 毎回実行されて遅い
# Provisioned Concurrencyで常にウォームな環境を維持(コスト増)
aws lambda put-provisioned-concurrency-config \
    --function-name my-fastapi \
    --qualifier prod \
    --provisioned-concurrent-executions 2
コールドスタートの目安:
  Python(ZIP形式): 500ms〜2秒
  Python(コンテナ): 2秒〜10秒
  Provisioned Concurrency: ほぼ0ms(常時起動)

コールドスタートが許容できない用途には向かない。内部APIやバッチ処理のトリガーなど、多少の遅延が許容できる場面で使う。


RDSへの接続 — VPC Lambdaの設定

LambdaをVPC内のRDSに接続する場合は、LambdaをVPCに配置する。

# LambdaをVPCに設定
aws lambda update-function-configuration \
    --function-name my-fastapi \
    --vpc-config SubnetIds=subnet-private-1,subnet-private-2,SecurityGroupIds=sg-lambda
注意点:
  VPC内のLambdaはインターネットに出られない
  → NATゲートウェイが必要(外部APIを叩く場合)
  → VPCエンドポイントでS3/Secrets Managerに接続する
# S3のVPCエンドポイントを作成(NATなしでS3にアクセス)
aws ec2 create-vpc-endpoint \
    --vpc-id vpc-xxx \
    --service-name com.amazonaws.ap-northeast-1.s3 \
    --vpc-endpoint-type Gateway \
    --route-table-ids rtb-xxx

RDS Proxy — Lambdaとの接続問題

LambdaはリクエストごとにDB接続を張るため、接続数が爆発する問題がある。

問題:
  Lambdaのインスタンスが1000個起動
  → それぞれがRDSに接続
  → RDSの接続数上限(例: PostgreSQL 100接続)を超える

解決策:
  RDS Proxy を挟む
  → 接続をプールしてRDSへの接続数を制限
# RDS Proxyを作成
aws rds create-db-proxy \
    --db-proxy-name my-rds-proxy \
    --engine-family POSTGRESQL \
    --auth '[{"AuthScheme":"SECRETS","SecretArn":"arn:aws:secretsmanager:..."}]' \
    --role-arn arn:aws:iam::123456789:role/rds-proxy-role \
    --vpc-subnet-ids subnet-private-1 subnet-private-2 \
    --vpc-security-group-ids sg-rds-proxy
# RDS Proxyのエンドポイントに接続
DATABASE_URL = "postgresql://user:pass@my-rds-proxy.proxy-xxx.ap-northeast-1.rds.amazonaws.com/mydb"

GitHub ActionsでCI/CD

# .github/workflows/deploy-lambda.yml
name: Deploy to Lambda

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id:     ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region:            ap-northeast-1

      - name: Login to ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push Docker image
        run: |
          docker build -t my-fastapi .
          docker tag my-fastapi:latest \
              ${{ secrets.ECR_REGISTRY }}/my-fastapi:${{ github.sha }}
          docker push \
              ${{ secrets.ECR_REGISTRY }}/my-fastapi:${{ github.sha }}

      - name: Update Lambda function
        run: |
          aws lambda update-function-code \
              --function-name my-fastapi \
              --image-uri ${{ secrets.ECR_REGISTRY }}/my-fastapi:${{ github.sha }}

      - name: Wait for update
        run: |
          aws lambda wait function-updated \
              --function-name my-fastapi

      - name: Run smoke test
        run: |
          FUNCTION_URL=$(aws lambda get-function-url-config \
              --function-name my-fastapi \
              --query 'FunctionUrl' --output text)
          curl -f "${FUNCTION_URL}health" || exit 1

コスト試算

Lambda の料金(2024年時点):
  リクエスト: $0.20 / 100万リクエスト
  実行時間:   $0.0000166667 / GB-秒

例: 月100万リクエスト、平均100ms、512MBメモリの場合
  リクエスト料金: $0.20
  実行時間料金: 1,000,000 × 0.1秒 × 0.5GB × $0.0000166667 = $0.83
  合計: 約$1.03/月

ECS Fargate との比較(0.25vCPU / 0.5GBメモリ、常時起動):
  $0.01208/時間 × 24時間 × 30日 = 約$8.7/月

→ 月100万リクエスト以下ならLambdaが安い

ECSとLambdaの構成比較まとめ

項目 ECS(Fargate) Lambda
起動時間 即時(常時起動) コールドスタートあり
コスト 常時課金 リクエスト課金
スケール 自動(設定必要) 自動(即時)
最大実行時間 無制限 15分
WebSocket 対応 非対応
VPC接続 標準 設定必要
DB接続 通常の接続プール RDS Proxy推奨
向いている用途 常時トラフィック バースト・低頻度

まとめ

  • MangumをアダプターとしてFastAPIをLambdaで動かせる
  • コンテナイメージでのデプロイが依存管理が楽
  • Function URLは設定が簡単。API Gatewayは高機能
  • コールドスタートはグローバル変数の活用とProvisioned Concurrencyで対策
  • VPC内RDSに接続する場合はRDS Proxyを使って接続数を制限する
  • 月100万リクエスト以下ならECSよりLambdaが安い

ECSとLambdaのどちらを使うかは「リクエスト数とコールドスタートの許容範囲」で決まる。社内向けのAPIや低頻度のWebhookはLambdaが向いている。Next.jsと直接組み合わせるメインのAPIはECSのほうが安定して使いやすい。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?