Lambdaコンテナをローカル実行したら、WebブラウザからのアクセスでCORSで詰んだ話とFastAPIでの回避法
はじめに
AWS Lambda をコンテナイメージでローカル実行したとき、curl
では動作確認できたのに、Webブラウザからアクセスしようとすると CORS エラーでうまくいかない、という経験をしました。
この記事ではその原因と、FastAPI を使って回避する方法について紹介します。
そもそもなぜコンテナイメージでLambdaを作ろうとするのか?
AWS Lambdaはzipとコンテナimageの2つの展開方法があります。今回はコンテナイメージでLambdaを作りますが以下のようなメリットがあるため、Lambdaをコンテナイメージで作ります。
コンテナイメージLambdaのメリット
-
ライブラリ導入が圧倒的に楽
個人的にこれが一番の理由です。zip形式だと依存ライブラリをLambda LayerやEFSマウントして導入・管理する必要があります。
コンテナであれば、Dockerfile に RUN pip install と書くだけで済みます。 -
サイズ上限が大きい
zipアップロード形式では 250MB の制限がありますが、コンテナ形式では 10GBまでOK -
CI/CD に組み込みやすい
イメージビルド&ECRプッシュ→Lambda更新 というフローが確立できると、zipをアップロードするより運用がスムーズです。
コンテナイメージLambdaのデメリット
-
起動がやや遅くなることも
イメージサイズが大きくなるとコールドスタートが遅くなる可能性があります。ただし、工夫すればある程度は改善できます(軽量ベースイメージ、必要なものだけインストールなど)。 -
複雑な場合はECRとIAM管理が必要
権限まわりやECRの管理を含めると、初心者にはややハードルが高いかもしれません。
環境構成
今回は、検証のためDockerを用いて、LambdaのPythonのコンテナとそれにアクセスするクライアントを構築します。
筆者検証環境
- OS Windows11(Home)
- WSL2(ubuntu22.04)
- Docker導入済みであること
ディレクトリ構成
<project_root>
├── backend/
│ ├── app.py # Lambdaハンドラー
│ └── Dockerfile # LambdaまたはFastAPIとして起動
├── client/
│ ├── index.html # ブラウザから呼び出し
│ └── Dockerfile
└── docker-compose.yml # ローカルで統合実行
ファイルの内容
docker-compose.yml
services:
client:
container_name: test_client
build:
context: .
dockerfile: client/Dockerfile
tty: true
ports:
- "3100:3100"
volumes:
- ./client:/app
- test_client_node_modules:/app/node_modules
networks:
- test_network
backend:
container_name: test_backend
build:
context: .
dockerfile: backend/Dockerfile
tty: true
ports:
- "9000:8080"
volumes:
- ./backend:/var/task
networks:
- test_network
volumes:
test_client_node_modules:
driver: local
networks:
test_network:
driver: bridge
backend/Dockerfile
FROM public.ecr.aws/lambda/python:3.11
COPY ./backend/app.py ./
CMD ["app.lambda_handler"]
backend/app.py
import json
def lambda_handler(event, context):
if event.get("httpMethod") == "OPTIONS":
return {
"statusCode": 200,
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
"body": "",
}
return {
"statusCode": 200,
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
"body": json.dumps("Hello from Lambda container!"),
}
client/Dockerfile
FROM node:20-alpine
WORKDIR /usr/src/app
RUN npm install -g http-server
COPY ./client .
EXPOSE 3100
CMD ["http-server", "-p", "3100"]
client/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Lambda APIテスト</title>
</head>
<body>
<h1>APIテスト</h1>
<button id="apiButton">Lambdaを呼び出す</button>
<pre id="result"></pre>
<script>
async function fetchApiData() {
try {
const response = await fetch("http://127.0.0.1:9000/2015-03-31/functions/function/invocations", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({})
});
const data = await response.json();
return data;
} catch (error) {
console.error("APIリクエストエラー:", error);
throw error;
}
}
document.getElementById("apiButton").addEventListener("click", async () => {
const resultElem = document.getElementById("result");
resultElem.textContent = "リクエスト中...";
try {
const data = await fetchApiData();
resultElem.textContent = JSON.stringify(data, null, 2);
} catch (e) {
resultElem.textContent = "エラー: " + e;
}
});
</script>
</body>
</html>
検証環境立ち上げ
Docker導入済みであれば以下のコマンドで環境を立ち上げられる
docker compose build
docker compose up -d
実行
ローカル実行する(curlでアクセス)
Lambdaをローカルでテストするには、次のようなエンドポイントを使います。
今回はdocker-compose.ymlでbackendのサービスのportを9000:8080にしているので以下のようになります。
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
# 以下レスポンス
{"statusCode": 200, "headers": {"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type"}, "body": "\"Hello from Lambda container!\""}
このように問題なく動作し、レスポンスも得られます。
ローカル実行する(Webブラウザからアクセス、失敗!)
任意のWebブラウザ(Google Chrome等)からhttp://localhost:3100/
にアクセスして、「Labbdaを呼び出す」ボタンを押すと「エラー: TypeError: Failed to fetch」というメッセージが出てきます。
この時、ブラウザの開発者コンソールにはCORSエラーが表示されます。
原因の詳細は、正直言うとよくわかっていません。ブラウザでは事前に OPTIONS リクエスト(プリフライト)が送られるのですが、このURLではOPTIONS に対応していないためエラーとなるというこ都があるらしくその可能性はあります。
回避策:FastAPIをかませる
回避策として、FastAPIを使ってLambdaをHTTPエンドポイント化することで回避します。
修正ファイル
backend/Dockerflie
FROM python:3.11-slim
WORKDIR /var/task
COPY ./backend/app.py ./
RUN pip install fastapi uvicorn
CMD ["python", "-m", "uvicorn", "dev_main:app", "--host", "0.0.0.0", "--port", "8080", "--reload", "--reload-dir", "."]
ベースイメージの変更:public.ecr.aws/lambda/python:3.11→python:3.11-slim
fastapiとuvicornのインストール、fastapiの起動をするようにしています。
また、backend/dev_main.py では以下のように Lambda ハンドラーを FastAPI 経由でラップしています。
backend/dev_main.py
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from app import lambda_handler
app = FastAPI()
# CORS対応
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.post("/api")
async def lambda_compatible_endpoint(request: Request):
event = {"body": ""}
result = lambda_handler(event, context={})
return result
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)
また、クライアントのアクセス先URLも変更が必要です。
http://127.0.0.1:9000/2015-03-31/functions/function/invocations
↓
http://127.0.0.1:9000/api
修正後の動作
curlコマンドはアクセス先URLが変わります
curl localhost:9000/api -X POST
# レスポンスは先ほどと同じ
先ほどと同じようにブラウザからのアクセスし「Lambdaを呼び出す」をクリックすると
{
"statusCode": 200,
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
},
"body": "\"Hello from Lambda container!\""
}
のようなメッセージが画面上に表示されます。
まとめ
- Lambdaコンテナは curl では動くが、ブラウザでは CORS でブロックされる
- OPTIONS リクエストに対応していないことが原因と思われる
- FastAPI を挟むことで CORS にも対応可能
- ローカル検証であってもブラウザアクセスを考えるなら、FastAPIなどをかませる必要がある
サーバレスの開発って難しいです…
Discussion