はじめに
最近ハイボールにハマっているSREのたっち(@TatchNicolas )です。
昨日オンライン開催されたJAWS DAYS 2020にて、JX通信社もサーバレスをテーマとして発表をしました。(by 植本さん )
発表でもありましたように、上記プロジェクトにおいて開発当時はスピードを優先してプロジェクトメンバーの手に馴染んでいて分担もしやすいフレームワークとしてFlaskを採用しました。
一方で、JX通信社としてはFlaskよりもFastAPIを使うプロジェクトが増えてきており、今後もその傾向は続く見込みです。
そこで、特設ページ作成やAPI提供など初動としての開発が一段落したのを機に、JAWS DAYSで発表した仕組みを今後のために発展させる検証をしたので紹介します。
TL; DR;
JAWSでは Serverless Framework+awsgi+Flask な構成でスピーディにコロナ特設ページ向けAPIを作った話をした
緊急性の高いプロジェクトであったが、非常にスピード感のある開発ができた
Flaskに代わるフレームワークとしてJX通信社ではFastAPI が流行中
OpenAPIドキュメント自動生成、Request/Responseをクラスとして定義するなどカッチリと開発ができる
しかしFlaskライクな軽量フレームワークで、シンプルで書きやすい
そこで、FastAPIを使うパターンでも前述の良い開発者体験を提供する仕組みを作った
サンプルコードはこちらにおいてあります。
github.com
前提
awsgiはAPIGatewayからLambda Proxy Integrationに渡されるイベントをWSGI*1 アプリケーションが理解できる形式に変換し、またWSGIアプリケーションが返すレスポンスをLambda Proxy Integrationに従った形式に変換してくれる便利なツールです。
github.com
WSGIの精神的後継として*2 、asyncに対応したASGI が登場しました。そのASGIに対応したフレームワーク(FastAPIなど)とLambda Proxy Integrationの間でアダプタ として動いてくれるのが、mangumです。
github.com
ざくっとまとめると以下のようになります。
WSGI
ASGI
フレームワーク例
Flask, Django*3
FastAPI,Sanic,Responder
Lambda Proxyアダプタ
awsgi
mangum
(HTTP Server)*4 *5
gunicorn, uWSGIほか
uvicorn
それでは早速やってみましょう。
やってみる
今回用意したサンプルコードはdocker-composeで動くようにしてあるので、下記のリポジトリをクローンしたら下記の要領で起動してみて下さい。
$ docker-compose up -d
$ docker-compose exec exam_results python -c ' from main import init_ddb_local; init_ddb_local() '
実際のコードを見ていきましょう。
(省略)
from fastapi import FastAPI, HTTPException
from mangum import Mangum
app = FastAPI()
(省略)
handler = Mangum(app, False )
(省略)
exam_results/main.py
のに app
という変数名でASGI準拠のアプリケーション(=FastAPIのインスタンス)を作り、それを引数として作ったMangumのインスタンスを handler
として定義します。
これを serverless.yml
の中でhandlerとして指定することで、APIGatewayから発生したイベントをMangumが受け取り、FastAPIに渡してくれる というわけです。
(省略)
functions :
exam_results :
events :
- http :
path : /{path+}
method : GET
private : false
cors : true
handler : exam_results.main.handler
environment :
PYTHONPATH : exam_results
DDB_TABLE : ${self:custom.envMapping.${self:provider.stage}.DDB_TABLE}
role : ManageStudentsRole
(省略)
細かなimportや環境変数設定は実際のサンプルコードを参照してください。
起動時の二行目のコマンドで、下記の関数を呼び出しています。
def init_ddb_local ():
if ExamResultsTable.Meta.host and not ExamResultsTable.exists():
print ('creating a table...' )
ExamResultsTable.create_table(
read_capacity_units=1 ,
write_capacity_units=1 ,
wait=True
)
print ('Done.' )
最初のif文でテーブルのモデル(ExamResultsTable
)の内部クラスMeta
のなかで、「環境変数から DDB_HOST
が指定されている」かつ「テーブルがまだ存在しない」場合のみPynamoDBを使って create_table
しています。
実際にリクエストをしてみましょう。
$ curl -X POST -H " Content-Type: application/json " -d ' {"name":"Alice","subject":"math","score":50} ' localhost:8001/scores
{ " name " :" Alice " ," subject " :" math " ," score " :50}
$ curl -X POST -H " Content-Type: application/json " -d ' {"name":"Bob","subject":"history","score":49} ' localhost:8001/scores
{ " name " :" Bob " ," subject " :" history " ," score " :49}
$ curl localhost:8001/scores?student =Alice
{ " exam_results " :[ { " name " :" Alice " ," subject " :" math " ," score " :50} ] }
$ curl localhost:8001/scores?subject =history
{ " exam_results " :[ { " name " :" Bob " ," subject " :" history " ," score " :49} ] }
次に、このアプリケーションをAWSにデプロイしてみます。(sls
コマンドのインストールやAWS操作のための権限は先に済ませておいてください。)
$ sls deploy --stage=dev
Serverless: Generating requirements.txt from pyproject.toml...
( いっぱいメッセージが出る )
Service Information
service: fantastic-service-with-fastapi
stage: dev
region: ap-northeast-1
stack: fantastic-service-with-fastapi-dev
resources: 13
api keys:
None
endpoints:
GET - https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/{ path+}
POST - https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/{ path+}
functions:
exam_results: fantastic-service-with-fastapi-dev-exam_results
layers:
None
Serverless: Removing old service artifacts from S3...
Serverless: Run the " serverless " command to setup monitoring, troubleshooting and testing.
endpoints
にあるURLに対して、localhost:8001
に対して行ったのと同じ操作ができると思います。
あとは sls deploy --stage=dev
の dev
を stg
prd
に書き換えて実行すれば、それぞれ独立した環境を作成することができます。
コマンドがシンプルなのでCI/CDも簡単に書くことができるでしょう。
また、/docs
にアクセスするとOpenAPIのドキュメントが自動生成されています。
自動生成されたdocs
何が嬉しいのか
開発スピードを高く保てる
早く上手に使えば運用やデプロイの負担が大幅に軽減されるLambdaと、ローカルでもガンガン開発しやすいWebアプリケーションフレームワークのいいとこ取りをすることができます。また、FastAPIの提供する「Web APIを作る」ために便利な機能を活用することができます。
Lambdaを手元で動かしたいという点で、本記事と似たことを実現しようとしているSAM Localは「Lambdaっぽいなにかを手元で動かす」発想です。一方で今回のやり方は「ローカルで動いているアプリケーションをASGI(FlaskであればWSGI)をインターフェイスとしてLambdaに載せる」という方法です。そのため、serverless.yaml
内ではhttpイベントのパスを{path+}
、 メソッドをANY
にしておくことでL7の仕事をAPIGatewayではなくLambda内のアプリケーションに任せています。
awsgiもmangumも(gunicornやuvicornといった)HTTP Serverを起動するわけではなく、単にイベントを変換しているだけなのでオーバヘッドもほとんどゼロなため、Lambdaがキチンとスケールしてくれればパフォーマンスの心配はありません。
いつでも実行環境を載せ替えられる
たとえば運用しつつ何らかの事情で「Lambdaじゃキツイな」となってきたらECSやk8sなどコンテナベースの実行環境に載せかえることも容易にできます。その際にアプリケーションは変更する必要がありません。ローカルで動かしていたDockerイメージをそのまま、またはDistrolessなどにちょっと書き換えるだけで引っ越すことができます。
今回は最小限の構成にするために、Pythonのコードはすべてexam_results/main.py
に詰め込みました。しかし実際のアプリケーションではもっと真面目にファイル・ディレクトリの構成を切り分けると思います。その場合はLambda用(exam_results/lambda.py
)とDocker用(exam_results/docker.py
)に入口をそもそも分けてしまったり、Poetryのextrasを使って依存ライブラリ管理を分けたりしても良いでしょう。
ハマったところ/改善したいところ
serverless-python-requirementsのモジュールとPoetryがうまく連携しない
パッケージ管理およびvenvの管理にはPoetryを使っていたのですが、Serverless Framework側でモジュール機能を使うと、プラグインが期待通りに動作せず*6 にpackageに依存ライブラリを含めてくれません。
PoetryだけではなくPipenvでも同じ問題があり、こちらはIssue報告されています。(執筆時点で未解決)
github.com
Pipfileやpyproject.tomlでなくrequirements.txtであれば individually: true
のオプションがちゃんと効いてLambda関数単位で依存関係を解決してくれるようです。なので、CI/CDのタイミングでコマンドを一行足してモジュールごとにPoetryにrequirements.txtを生成させば解決します。
ただし今回のサンプルコードでは、1スタック1関数として作ることで回避しました。*7
DynamoDB LocalとPynamoDBでスキーマ定義が二重管理になってしまう
宣言的に書くためにDynamoDBのスキーマ定義はserverless.ymlに書きたいところですが、それをローカル開発時のDynamoDBにそのまま適用することはできません。
今回アプリケーションはPynamoDBを使ってDynamoDBとやりとりをしており、PynamoDBのモデルはserverless.ymlで定義したスキーマと一致するように書いています。そこで、簡単に初期化関数を定義してPynamodDBの機能を使ってローカルのテーブルを作りました。
しかしこれは実際のアプリケーションには必要ないある種「余計な」コードがソースの中に紛れ込んでしまっています。serverless.ymlに書いた定義をそのままローカル開発環境にも適用できればよいのですが、スマートな方法があれば改善したいです。*8
さいごに
サーバレスはその恩恵と引き換えに、どうしてもプラットフォームの制約を受け入れる必要が生じます。そんな制約の一つが「イベントという形で入力を渡されるので、Lambdaとしての書き方を強制されて慣れ親しんだフレームワークとは勝手が異なる」ことだと思います。
しかしWSGI/ASGIというインターフェイス(とそれらを繋いでくれるmangumのようなツール)を活用することで、ローカルで作業のしやすいコンテナベースの開発とサーバレスな開発をシームレスに切り替えられます。JX通信社のプロダクトではECS+RDSで一部補助的にLambdaを使う構成が定番でしたが、最近はサーバレスを積極的に活用する場面も増えてきました。
今回はそんな社内の技術スタックの移り変わりにあわせた開発ができるように工夫してみたことの記録でした。少しでも皆さんの参考になれば幸いです。