はじめに
「Flask で作った API がすでにある」
「でも、EC2 や ECS を立てるほどではない」
「とにかく早く AWS Lambda に載せたい」
そんなときに使える構成です。
この記事では、Flask アプリを大きく書き換えずに、AWS Lambda + API Gateway 上で動かす方法を紹介します。
ポイントは以下です。
- Flask の
@app.route()はそのまま使う - Lambda handler で API Gateway の event を Flask に渡す
- Lambda はコンテナイメージとしてデプロイする
- Serverless Framework で API Gateway と Lambda を定義する
- Lambda 用ではない Docker image も使える
全体構成
構成はこんな感じです。
Client
↓
API Gateway
↓
Lambda
↓
aws-wsgi
↓
Flask app
Lambda ネイティブに全部作り直すのではなく、API Gateway のリクエストを WSGI に変換して、Flask アプリに渡します。
ディレクトリ構成
今回は最小構成として、以下のようにします。
.
├── app.py
├── requirements.txt
├── Dockerfile
├── serverless.yml
└── package.json
Flask アプリを書く
まずは普通の Flask アプリを書きます。
# app.py
from flask import Flask, jsonify, request
import awsgi
app = Flask(__name__)
@app.route("/health_check", methods=["GET"])
def health_check():
return jsonify({"status": "ok"})
@app.route("/hello", methods=["POST"])
def hello():
body = request.get_json(silent=True) or {}
name = body.get("name", "world")
return jsonify({
"message": f"hello, {name}"
})
def handler(event, context):
return awsgi.response(app, event, context)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000)
重要なのはここです。
def handler(event, context):
return awsgi.response(app, event, context)
aws-wsgi を使うことで、API Gateway から渡される event / context を Flask が扱える形に変換できます。
つまり、Flask 側はいつも通り request や @app.route() を使えます。
requirements.txt
Flask==3.0.3
aws-wsgi==0.2.7
パッケージ名は aws-wsgi ですが、Python 側では import awsgi します。
Dockerfile
まずは一番シンプルに、AWS 公式の Lambda Python ベースイメージを使う例です。
FROM public.ecr.aws/lambda/python:3.11
WORKDIR ${LAMBDA_TASK_ROOT}
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["app.handler"]
AWS Lambda 用の Python ベースイメージを使うと、Lambda Runtime Interface Client が最初から入っているので楽です。
CMD ["app.handler"] は、
app.py の handler 関数を Lambda handler として使う
という意味です。
ここで大事なのは、Lambda コンテナでは Flask の HTTP サーバーを直接起動しないことです。
通常の Flask なら以下のように HTTP サーバーを起動します。
flask run
# or
gunicorn app:app
しかし Lambda コンテナでは、Lambda Runtime が handler を呼び出します。
そのため Dockerfile では、flask run や gunicorn を CMD に書くのではなく、Lambda handler を指定します。
Serverless Framework を入れる
package.json は最小だと以下のようになります。
{
"dependencies": {
"serverless": "^3.39.0"
},
"devDependencies": {}
}
インストールします。
npm install
serverless.yml
service: flask-on-lambda
frameworkVersion: "3"
provider:
name: aws
region: ap-northeast-1
stage: ${opt:stage, "dev"}
timeout: 30
memorySize: 512
ecr:
images:
flaskApp:
path: .
functions:
api:
image:
name: flaskApp
events:
- http:
path: /health_check
method: get
- http:
path: /hello
method: post
provider.ecr.images に path: . を指定すると、Serverless Framework が Docker image を build して ECR に push し、その image を Lambda に設定してくれます。
API のエンドポイントは Dockerfile ではなく、serverless.yml の events で決まります。
役割を分けると、以下のようになります。
- Dockerfile: Lambda が呼ぶ handler を指定する
- serverless.yml: API Gateway の URL/path を指定する
- Flask: path に対応する処理を書く
デプロイする
npx serverless deploy --stage dev
デプロイが終わると、API Gateway の URL が表示されます。
例:
endpoints:
GET - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/health_check
POST - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello
動作確認
curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/health_check
{"status":"ok"}
POST も試します。
curl -X POST \
https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello \
-H "Content-Type: application/json" \
-d '{"name":"lambda"}'
{"message":"hello, lambda"}
これで Flask アプリが Lambda 上で動きました。
Lambda 用ではない Docker image も使える
Lambda コンテナというと、まず AWS 公式の Lambda ベースイメージを使う方法があります。
FROM public.ecr.aws/lambda/python:3.11
これは一番簡単です。
ただ、実際のアプリでは OS パッケージやネイティブライブラリの都合で、Ubuntu などの独自ベースイメージを使いたいことがあります。
その場合でも、Lambda コンテナとして動かすことはできます。
必要なのは Lambda Runtime Interface Client です。
RUN pip install awslambdaric
そして ENTRYPOINT で awslambdaric を起動します。
ENTRYPOINT ["python", "-m", "awslambdaric"]
CMD ["app.handler"]
これで、Lambda がコンテナを起動したときに Runtime Interface Client が立ち上がり、app.handler を呼んでくれます。
つまり、ベースイメージ自体が Lambda 専用でなくても、以下を満たせば Lambda コンテナとして動かせます。
- Lambda Runtime Interface Client が入っている
- ENTRYPOINT で Runtime Interface Client を起動している
- CMD で handler を指定している
例えば Ubuntu 系の image でも、次のようにできます。
FROM ubuntu:22.04
WORKDIR /var/task
RUN apt-get update && apt-get install -y \
python3 \
python3-pip
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
RUN pip3 install awslambdaric
COPY . .
ENTRYPOINT ["python3", "-m", "awslambdaric"]
CMD ["app.handler"]
この構成のメリットは、Lambda 用ベースイメージに縛られず、アプリに必要な OS パッケージやネイティブライブラリを入れやすいことです。
例えば以下のようなケースでは便利です。
-
apt installで OS パッケージを入れたい - C/C++ 系の依存ライブラリがある
- 画像処理・数値計算・機械学習系のライブラリを使う
- 既存の Dockerfile をなるべく流用したい
- Lambda 公式イメージでは環境構築が面倒
import エラーが出る場合は、どの Python が呼ばれているか確認する
独自ベースイメージを使う場合、awslambdaric を起動する Python と、依存ライブラリをインストールした Python がズレることがあります。
例えば、Dockerfile で以下のように書いたとします。
RUN pip install -r requirements.txt
RUN pip install awslambdaric
ENTRYPOINT ["python", "-m", "awslambdaric"]
CMD ["app.handler"]
一見よさそうに見えます。
しかし環境によっては、python が /usr/bin/python を指していて、実際にライブラリを入れた venv や pyenv の Python とは別物になっていることがあります。
この状態で Lambda を起動すると、以下のような import エラーが出ることがあります。
Runtime.ImportModuleError: Unable to import module 'app'
ModuleNotFoundError: No module named 'xxx'
この場合は、まずどの Python が呼ばれているか確認します。
RUN which python
RUN python --version
RUN python -m pip list
また、venv や pyenv を使っている場合は、その Python を明示的に指定します。
ENTRYPOINT ["/path/to/venv/bin/python", "-m", "awslambdaric"]
CMD ["app.handler"]
自分の環境では、python だけで指定すると /usr/bin/python が呼ばれてしまい、依存ライブラリを入れた Python とズレて import エラーになりました。
そのため、venv 側の Python を直接指定しました。
ENTRYPOINT ["/opt/pysetup/.venv/bin/python", "-m", "awslambdaric"]
CMD ["app.handler"]
pyenv を使っている場合は、例えば以下のようになります。
ENTRYPOINT ["/root/.pyenv/shims/python", "-m", "awslambdaric"]
CMD ["app.handler"]
ポイントは、pip install した先の Python と、awslambdaric を起動する Python を一致させることです。
Lambda コンテナの import エラーは、コードの問題ではなく「起動している Python が違う」だけのことがあります。
import エラーが出たときは、アプリのコードを疑う前に、
which python
python --version
python -m pip list
で、Lambda コンテナ内で実際に使われている Python を確認すると原因に近づきやすいです。
この構成のうれしいところ
この構成の一番のメリットは、Flask アプリを Lambda 用に大きく書き換えなくていいことです。
例えば、すでにこういう Flask アプリがある場合でも、
@app.route("/users", methods=["POST"])
def create_user():
...
基本的にはそのまま使えます。
Lambda handler を1つ用意して、awsgi.response() に Flask app を渡せば、API Gateway からのリクエストを Flask に流せます。
また、コンテナイメージとしてデプロイするので、依存関係が多少重い場合でも ZIP デプロイより扱いやすいです。
さらに、Lambda 公式ベースイメージ以外の Docker image も使えます。
Ubuntu などに OS パッケージを入れて、そのまま Lambda コンテナにできるのは、既存の Dockerfile 資産を流用したい場合にかなり便利です。
デメリット
もちろん、この構成は万能ではありません。
「最短で Flask を Lambda に載せる」ための構成であって、「Lambda に最適化された設計」ではありません。
注意点はあります。
- コールドスタートが重くなりやすい
- 複数 API を1つの Lambda にまとめると、API ごとの timeout / memory 調整がしづらい
- 小さな修正でもコンテナイメージを build / push する必要がある
- API Gateway の制約を受ける
- 長時間処理には向かない
- Flask/Gunicorn でのローカル実行と Lambda 実行で差が出ることがある
特に依存ライブラリが重い場合、import 時間がそのままコールドスタートに効いてきます。
小話: Numba の AOT
Python で重い数値計算をしている場合、Numba を使うことがあります。
Numba の @njit は便利ですが、JIT なので初回実行時にコンパイルが走ります。
Lambda ではこの初回コンパイルが、コールドスタートや初回リクエストの遅さとして見えることがあります。
その場合、処理によっては AOT、つまり Ahead-of-Time compile を検討できます。
from numba.pycc import CC
from numba import njit
cc = CC("my_math")
@cc.export("add", "int64(int64, int64)")
@njit
def add(a, b):
return a + b
if __name__ == "__main__":
cc.compile()
これをビルドすると、.so のような compiled extension が生成されます。
from my_math import add
print(add(1, 2))
Lambda コンテナイメージにこの compiled extension を含めておけば、実行時に毎回 JIT コンパイルする必要がなくなります。
ただし、numba.pycc は将来的な非推奨・置き換え対象として扱われているため、新規採用する場合は注意が必要です。
「こういう逃がし方もある」くらいの小話として覚えておくとよさそうです。
まとめ
Flask アプリを AWS Lambda に載せるとき、必ずしも Lambda 用に全面的に書き換える必要はありません。
aws-wsgi を使えば、API Gateway の event を Flask に渡せます。
さらに Lambda コンテナイメージを使えば、依存関係がある程度重い Flask アプリでも載せやすくなります。
この構成は、次のようなケースに向いています。
- 既存の Flask アプリをまず Lambda に載せたい
- Lambda ネイティブに作り直す時間がない
- ZIP デプロイでは依存関係がつらい
- Ubuntu などの既存 Docker image を流用したい
- まず動くものを早く作りたい
一方で、コールドスタートやデプロイ単位、API ごとの細かい最適化には注意が必要です。
最初の一歩としてはかなり便利ですが、本格運用するなら Lambda らしい設計に寄せるかどうかも後で検討するとよさそうです。