はじめに
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のほうが安定して使いやすい。