Open4

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

masaobluemasaoblue

色々あってやることになったので詰まったところメモ
動いた後にまとめているので漏れているかもしれない...

メモ

Serverless WSGIの sls wsgi serve でもFlaskアプリ単体は起動できるけど、API GatewayからLambda Proxy Integrationするような構成をとっているときにはServerless Offlineのsls offline で起動できた方が色々捗るのでその環境を作る

Serverless Framework

とりあえずインストール。globalに入れたくない場合はよしなに

npm install -g serverless

https://www.serverless.com/framework/docs/getting-started

python

requirements.txtを定義して以下を記載。(werkzeug はどこかの手順で必要になって入れたんだけど、経緯を忘れてしまった)

requirements.txt
flask
werkzeug

インストール

pip install -r requirements.txt

Serverless WSGI プラグイン

FlaskはWSGIなのでこれを使う

https://www.serverless.com/plugins/serverless-wsgi

sls plugin install -n serverless-wsgi

api.py を以下の通り定義。このファイル名はserverless.yml側で指定する名前と合っていれば何でも良いけど、fastapi.py みたいに既存のパッケージと被る名前になっているとうまく読み込めないため注意(当たり前といえば当たり前だけど、動作確認用にこの名前をつけたら少しハマった...)

api.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にした

serverless.yml
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も起動できたのでなぜ発生するのかはよく分からない

masaobluemasaoblue

Serverless Offline

https://www.serverless.com/plugins/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"

参考

masaobluemasaoblue

余談: sls wsgi install で生成されるファイルについて

上記の通り、Serverless WSGIを使う場合 wsgi_handler.handler をプラグイン内で定義してくれており、このモジュールは sls wsgi install することでローカルファイルとして作成することができる。

sls wsgi install

ドキュメントには書かれてなさそうだけど、CHANGELOGを見るとv1.5.3で追加されていた。

実行すると以下の通り Unable to load virtualenv というエラーが発生するが、必要なファイル自体は生成されるため確認は可能。

なお、このエラーについて検索するとissueに当たるが、今のところ解決方法が書かれていない。

https://github.com/serverless/serverless/issues/11513

一応 packRequirements: false を指定するとエラーは出なくなる

custom:
  wsgi:
    app: api.app
    packRequirements: false # これを追加
masaobluemasaoblue

追記: デプロイする場合

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する必要あり。

https://www.serverless.com/plugins/serverless-python-requirements

最終的なserverless.ymlは以下の通り。

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が出力されることを確認した。

めでたしめでたし。