AWS Lambda 上での Python API サービスをローカルでも検証するために serverless-wsgi を利用する

serverless framework を使って Web API を作りたいと思っていて色々調べていたのだけれど、以下のような場合にどうしようかなあと思っていた。

  • AWS S3 Bucket上に展開したWeb Hosting上からAPI Gateway経由でサービスを叩くアーキテクチャ
  • つまり別途フロントエンドのウェブページがあり、ブラウザを使ってAPI連携の検証をちゃんとしたい

もちろんステージの概念があるのでAWSにフルデプロイしながら検証することもできなくはない。
が、開発環境のテストにおいてローカルで完結できるというのは個人的な信条から必要だと思っているので何とかローカルで完結する検証環境をちゃんと作ってしまいたい。

TL;DR

  • serverless framework だと serverless-offline という plugin があるんだけれど現時点ではPythonバージョンだとちょっと使えなさそう
  • WSGI のレイヤーを挟むことでローカルで検証する場合のアプリケーションサーバーをローンチして検証できる
  • serverless framework の plugin として serverless-wsgi が提供されているのでこれを使う
  • localでの検証は sls wsgi serve で、本番へのデプロイは sls deploy でそれぞれ対応可能になる

今回の問題点

AWS Lambda のソースコード自体は Python のメインエントリをちゃんと書いて、handlerの設定を例えば次のようにしておくことでローカルで動作させるテスト自体は簡単にできる。
もちろん、単なるソースコードなので Mock を挟み込んだユニットテストを書くことも相当楽にできる。

def handler(event, context):
    pass

if __name__ == '__main__':
    # 必要に応じて fake を挟んだりする
    # event = ... 
    # context = ...
    handler(event, context)

その一方でちゃんとした HTTP Request / Response をローカルサーバーで返してくれるような仕組みにしようとすると、生で書こうとすると骨が折れる。

検討: serverless-offline

もちろんローカルでの検証を serverless framework が考えていないはずはなく plugin として提供されている。

https://github.com/dherault/serverless-offline

記事執筆時点(2018/07/27)の issue や pull request を見てみると、 python での動作を行えるようなプロジェクトがあるが、対応のモチベーションがやはり低めっぽい。 serverless framework が Node JS で基本書かれているから、当然と言えば当然だとは思うけれど。
また serverless-offline-python という npm パッケージでインストールができるようにはなっているが npm で入れてみても自分の環境ではうまく動かず README.md が serverless-offline の内容ほぼそのままなので独立した別プロジェクトとして本格的に取り組んでいるわけでもなさそう。

もちろん、色々調べれば使えなくはないのかもしれないし、将来的にはpythonでもちゃんとofflineが使えるようになってるのかもしれないけれど、今回は自分で思い描いているサービスを作りたい欲の方が勝っているので採用を見送った。

検討: python web application

そもそも、やりたいことは

ということで serverless と Python Web Application 周りの連携に関して前例を調べてみる。 すると、以下の公式ブログ記事が引っかかった。

https://serverless.com/blog/flask-python-rest-api-serverless-lambda-dynamodb/

ここでは既存の Flask アプリケーションを serverless framework で動作させるという記事だが、自分のやりたいケースとも一致する。 具体的にはこの記事の中で紹介されている serverless-wsgi plugin を利用して、WSGIというレイヤーを以下の通りうまく活用する。

WSGI対応のソースコードできちんと書ければ、うまくローカル・本番環境の差し替えが実現可能となる。

なお serverless-wsgi の実装を流し見した限り sls deploy 時に以下のことを行っている。

  • wsgi.py というファイルを sls deploy 時に Lambda 関数内に自動的に含める
  • Lambda の handler として wsgi.py を自動的に設定する
  • wsgi.py の中で自分が準備した pythonソースコードを呼び出す*1

レイヤーをうまく重ね合わせる、というテクニックを利用しているのでもちろんオーバーヘッドは増えるというデメリットはある。
ただし、今回はローカルとAWS API Gateway経由で動作するアプリケーションをうまく作る、というところでありオーバーヘッドのデメリットを上回って自分のやりたいことができるというメリットを採用することとした。

実際の実装

サンプルでは Flask を使っている。 自分は bottle を使っていたのだけれど、Web上の記事に bottle を使うものがあまり見かけられないので、折角だし bottle を使って実装してみることにする。 WSGI統一されてればなんでもこの方式でserverless framework + WSGI アプリケーションができるということを示すという意味でも。

WSGIアプリケーションの実装

Python Version は AWS Lambda Python3 に合わせて 3.6 を使用。 ソースコードは bottle の Example: “Hello World” in a bottle をほぼそのまま。 ただし、WSGI アプリケーション用のインスタンスを作って、それを serverless-wsgi から呼び出せるよう Bottle インスタンスを作成している。

# app.py
from bottle import Bottle, template

app = Bottle()


@app.route('/hello/<name>')
def index(name):
    return template('<b>Hello {{name}}</b>!', name=name)


if __name__ == '__main__':
    app.run(port=8000)

当初の目標であるローカルでの動作は以下で確認できる。

# 環境の準備
$ virtualenv .venv --python=python3.6
$ source .venv/bin/activate

# 後者2つは serverless-wsgi 内で利用する
$ pip install bottle werkzeug virtualenv
$ pip freeze
bottle==0.12.13
virtualenv==16.0.0
Werkzeug==0.14.1

# サーバーを起動
$ python app.py 
Bottle v0.12.13 server starting up (using WSGIRefServer())...
Listening on http://127.0.0.1:8000/
Hit Ctrl-C to quit.

# アクセスを確認
$ curl http://localhost:8000/hello/world
<b>Hello world</b>!
sls コマンドによるデプロイ

次はこれを sls deploy で実際にデプロイするための準備を行う。 まずはそのために必要な plugin などのインストール。

# plugin のインストール
$ npm install --save-dev serverless-wsgi serverless-python-requirements

# sls deploy で利用する requirements.txt の作成
$ pip freeze > requirements.txt

必要な設定を抜粋すると以下の通り。 いくつかは暗黙的に指定しなくてもいいものがあるけど、明示指定したいので記載してあるものも結構ある。

# serverless.yml
service: wsgi-bottle-test
frameworkVersion: ">=1.0.0 <2.0.0"
provider:
  name: aws
  runtime: python3.6
  profile: serverless
  region: ap-northeast-1
  memorySize: 128
  deploymentBucket:
    name: "<your deploy bucket>"
    serverSideEncryption: AES256
  versionFunctions: false

plugins:
  - serverless-wsgi
  - serverless-python-requirements

custom:
  wsgi:
    app: app.app         # 値は <ファイル名>.<WSGIアプリケーションインスタンスのインスタンス名>
  pythonRequirements:
    dockerizePip: false  # docker使うならtrue / 使うライブラリにネイティブビルドがないので docker を利用したビルドを実施しない

package:
  exclude:
    - .venv/**
    - node_modules/**

functions:
  app:                     # ここの名称は特に何でも問題ない
    handler: wsgi.handler  # ここは固定
    events:                # ルーティングを app.route などで指定した内容に依存させる
      - http: ANY /
      - http: ANY {proxy+}

まずはこの状態でローカルサーバーを sls コマンド経由で起動できるかを確認する。

$ sls wsgi serve
 * Running on http://localhost:5000/ (Press CTRL+C to quit)
 ...

$ curl http://localhost:5000/hello/world
<b>Hello world</b>!

これでローカルでの実行環境は整ったので、次は本番へのデプロイを行う。

$ sls deploy
Serverless: Packaging Python WSGI handler...
Serverless: Packaging required Python packages...
Serverless: Linking required Python packages...
Serverless: Installing requirements of requirements.txt in .serverless...
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Unlinking required Python packages...
Serverless: Injecting required Python packages to package...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (2.73 MB)...
Serverless: Validating template...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.............................
Serverless: Stack create finished...
Service Information
service: wsgi-bottle-test
stage: dev
region: ap-northeast-1
stack: wsgi-bottle-test-dev
api keys:
  None
endpoints:
  ANY - https://**********.execute-api.ap-northeast-1.amazonaws.com/dev
  ANY - https://**********.execute-api.ap-northeast-1.amazonaws.com/dev/{proxy+}
functions:
  app: wsgi-bottle-test-dev-app

$ curl https://**********.execute-api.ap-northeast-1.amazonaws.com/dev/hello/world
<b>Hello world</b>!

これで実際の API Gateway + AWS Lambda へのデプロイが実行できる。

元々実施したかったローカルでの検証は sls wsgi serve の実行で、本番へのデプロイは sls deploy でそれぞれ対応可能になった。
というか、serverless-wsgi の README に sls コマンド使ったローカルでの検証方法が書かれている以上、明らかにこういった自体を想定して作られているものなのでこの記事は単にそれの紹介にしかなっていない気がするが置いておく。

AWS上のデプロイ実装について

この設定の場合 API Gatewayへのマッピングは ANY と {proxy+} を使って*2任意のパスをキャッチし、そのすべてをProxy統合リクエストで Lambda に流すというパターンを利用している。 そのため、 app.py に新たな route を追加した場合でも wsgi.py がそのディスパッチ部分を吸収してくれる。

もっと詳しい使い方は公式リポジトリを参照。

https://github.com/logandk/serverless-wsgi