Serverless Offlineを使ってpython & Flaskのアプリをローカルで動かす方法

色々あってやることになったので詰まったところメモ
動いた後にまとめているので漏れているかもしれない...
メモ
Serverless WSGIの sls wsgi serve
でもFlaskアプリ単体は起動できるけど、API GatewayからLambda Proxy Integrationするような構成をとっているときにはServerless Offlineのsls offline
で起動できた方が色々捗るのでその環境を作る
Serverless Framework
とりあえずインストール。globalに入れたくない場合はよしなに
npm install -g serverless
python
requirements.txtを定義して以下を記載。(werkzeug
はどこかの手順で必要になって入れたんだけど、経緯を忘れてしまった)
flask
werkzeug
インストール
pip install -r requirements.txt
Serverless WSGI プラグイン
FlaskはWSGIなのでこれを使う
sls plugin install -n serverless-wsgi
api.py
を以下の通り定義。このファイル名はserverless.yml側で指定する名前と合っていれば何でも良いけど、fastapi.py
みたいに既存のパッケージと被る名前になっているとうまく読み込めないため注意(当たり前といえば当たり前だけど、動作確認用にこの名前をつけたら少しハマった...)
from flask import Flask
app = Flask(__name__)
@app.route("/cats")
def cats():
return "Cats"
@app.route("/dogs/<id>")
def dog(id):
return "Dog"
で、serverless.ymlでいくつか設定。
自分の手元だとprovider.nameを python3.9
にするとローカル起動時にエラーが出たので3.10にした
service: example
provider:
name: aws
runtime: python3.10 # 3.9だとエラーになったので3.10を指定しておく
region: ap-northeast-1
plugins:
- serverless-wsgi
functions:
# api.pyとたまたま同じ名前が使われているが、この値はデプロイ時のリソースに
# 付与されるだけなので、pythonファイルの名前と一致している必要は特に無い
api:
# `wsgi.handler` は旧名称らしいのでwsgi_handlerにする
# このymlファイル内の `custom.wsgi.app` で設定したアプリケーションを呼べるようにしてくれる
handler: wsgi_handler.handler
events:
- http: ANY /
- http: ANY /{proxy+}
custom:
wsgi:
# ここは `[ファイル名(=module名)].[ファイル内でFlask()の結果が格納されている変数名] を指定
app: api.app
この時点で sls wsgi serve
すると起動確認ができる
参考: 3.9で sls wsgi serve
したときのエラー。3.8も3.10も起動できたのでなぜ発生するのかはよく分からない

Serverless Offline
上記の手順でinstallと設定を行う。最低限ならplugins配下に名前を追加するだけで良い
plugins:
- serverless-wsgi
- serverless-offline # 追加
ここで sls offline
を実行すると、起動はできるがエンドポイントにアクセスした際に No module named 'wsgi_handler'
になる
このエラーは、sls wsgi install
を実行してwsgi_handler.pyをローカルに生成することで解決が可能。
参考: https://github.com/logandk/serverless-wsgi/issues/124
# Serverless WSGIが内部で必要としているファイルをローカルに生成する
# `Unable to load virtualenv` というエラーが出るが無視しても問題は起きない(詳細は後述)
sls wsgi install
この状態で sls offline
を実行すると http://localhost:3000/dev/cats
などのURLにアクセスできるようになる。
おまけ: Flask側でLambda Proxy Integrationで渡される情報を取得する
sls offline
で起動した場合は request.environ.get("serverless.event")
で取得したイベントに諸々の情報が渡されてくるようになる。
今回はアクセス元のグローバルIPを取得したかったので、以下のようにして取得できることを確認した。(ローカル起動時は ::1
みたいなIPv6のローカルアドレスが取得できた)
@app.route("/cats")
def cats():
print(request.environ.get("serverless.event").get("requestContext").get("identity").get("sourceIp"))
return "Cats"
参考
- flaskでlambdaのevent情報にアクセスする方法
- event.requestContextの定義

sls wsgi install
で生成されるファイルについて
余談: 上記の通り、Serverless WSGIを使う場合 wsgi_handler.handler
をプラグイン内で定義してくれており、このモジュールは sls wsgi install
することでローカルファイルとして作成することができる。
sls wsgi install
ドキュメントには書かれてなさそうだけど、CHANGELOGを見るとv1.5.3で追加されていた。
実行すると以下の通り Unable to load virtualenv
というエラーが発生するが、必要なファイル自体は生成されるため確認は可能。
なお、このエラーについて検索するとissueに当たるが、今のところ解決方法が書かれていない。
一応 packRequirements: false
を指定するとエラーは出なくなる
custom:
wsgi:
app: api.app
packRequirements: false # これを追加

追記: デプロイする場合
packRequirementsをfalseにしない状態で sls deploy
でデプロイしてみると、Unable to load virtualenv
のエラーが出てデプロイに失敗した。
そしてpackRequirementsをfalseにしてデプロイするとコマンドは成功したが、アクセスした時にLambdaで以下のエラーが発生していた。
[ERROR] Runtime.ImportModuleError: Unable to import module 'wsgi_handler': No module named 'werkzeug' Traceback (most recent call last):
とりあえず今回は Serverless Python Requirements で対処してみた。(本当はLayerを作るべきかもしれない)
ローカルに .requirements.zip
が作成されるため適宜ignoreする必要あり。
最終的なserverless.ymlは以下の通り。
service: example
provider:
name: aws
runtime: python3.10
region: ap-northeast-1
plugins:
- serverless-wsgi
- serverless-offline
- serverless-python-requirements
functions:
api:
handler: wsgi_handler.handler
events:
- http: ANY /
- http: ANY /{proxy+}
custom:
wsgi:
app: api.app
packRequirements: false
pythonRequirements:
zip: true
これで sls deploy --stage dev --aws-profile default
でデプロイして、https://[デプロイされたID].execute-api.ap-northeast-1.amazonaws.com/dev/cats
にアクセスしたときにCloudWatch Logsに自宅のグローバルIPが出力されることを確認した。
めでたしめでたし。