はじめに
仕事でバックエンドに AWS API Gateway + AWS Lambda + Mangum + FastAPI で構築したサーバーレスなアプリケーションをプロダクションに持っていくことができた。ひと息したところで、別途 Azure を使うプロジェクトから参加要請が来た。ここはAzureならAzure API Management + Azure Functions だよなぁと思ってたけど、チュートリアル的にはひとつひとつの関数を追加する方法はあるが、のなんらかのフレームワークを利用する方法が探せず。まあ仕事の中で考える・勉強しつつやるかなと思ってたけど。コロナ禍の3連休、マンボーで外にも出たくないし、オリンピックもつまらないし、で Azure の勉強がてら試したメモ。
Azure Functions での WSGI/ASGI 状況 (2021/08)
結局、Azure Functions Python Library を使い、WsgiMiddleware や AsgiMiddleware からアプリケーションを繋いであげれば良い。
過去にテスト的に?
と組み合わせる必要があったが現在は上記不要。
例えば、日本語の記事、Qiita の記事だとこちらが見つかりましたが、結局 ASGI の方は、 Mangum の作者が Azure 向けに Bonnette を開発したが、もうメンテしないんでってことで、そこらへんのアイデアが Azure Functions Python Library の方に取り込まれていったので、特に追加のライブラリーなしで azure-functions
だけで使える(v1.7.1以降)。同様に WSGI の方は、 azf-wsgi があったがこちらの方も先行して azure-functions
自体に取り込まれていた(v1.1.0以降)。
動かしてみる
FastAPI で行きたいと思っていたが、すでにアプリケーションの方は Flask で開発が進んでいたので、 Flask と WSGI で。
Azure のリソース(Resource Group, Functions, Blob Storage, etc) の作成方法等には触れません。
雛形を作る
まず、クイックスタート: コマンド ラインから Azure に Python 関数を作成するを参考に進める。
どうも、この手順通りに進めると、プロジェクト名や関数名が UpperCamelCase で.NET的だったり、Python仮想環境の位置がどうなのか?とかPython的な違和感があるので、そこら辺を読み替える。
Functions App プロジェクトと仮想環境の作成
$ mkdir myproject
$ cd myproject
2020/08/08 現在Azure側は私の環境では3.8.6だったので合わせておきました。
$ pyenv local 3.8.6
$ python -m venv .venv
$ source .venv/bin/activate
Azure Functions Core Tools - funcコマンドで雛形作成
$ func init .
Select a number for worker runtime:
1. dotnet
2. dotnet (isolated process)
3. node
4. python
5. powershell
6. custom
Choose option: 4
python
Found Python version 3.8.6 (python3).
Writing requirements.txt
Writing .funcignore
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /Users/username/Workspaces/myproject/.vscode/extensions.json
関数追加
Http Triggerで名前をfunctions_wsgi
として追加します。普通作ったままだと、/api/functions_wsgi
というパスで呼び出す事になりますが、後述するように全部のパスをWSGI/ASGIでフレームワークに渡すので実際にはこの名前はなんでも良いです。
$ func new --template "Http Trigger" --name functions_wsgi
ディレクトリー構成
これで、これまでの作業で、こんな構成になっているかと。
.
├── functions_wsgi
│ ├── __init__.py
│ └── function.json
├── host.json
└── requirements.txt
ここに、普通の?Flask/WSGIのアプリを置きます。
.
├── flask_app # Flask/WSGI WebApp
│ ├── __init__.py
│ └── app.py
├── functions_wsgi # Functions entry -> WSGI
│ ├── __init__.py
│ └── function.json
├── host.json
└── requirements.txt
こんな感じで、 デフォルトの Werkzeug なり、Gunicorn なりでちゃんと動くもの。(私は Azure Functions Core Tools の雛形で作成される Azure Functions 環境依存の関数を Flask化しました)
$ python flask_app/app.py
* Serving Flask app 'app' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
Azure Functions と WSGI/ASGIを統合
functions_wsgi/function.json
と host.json
ファイルを次の様に変更し、デフォルトのパス /api
をやめて、全てのパスをルーティングされるように変更。
"scriptFile": "__init__.py",
"bindings": [
{
+ "route": "{*route}",
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[2.*, 3.0.0)"
+ },
+ "extensions": {
+ "http": {
+ "routePrefix": ""
+ }
}
}
で、 scriptFile:
に設定されている、functions_wsgi
の __init__.py
の中身を次の様にする。多分FastAPI/ASGIなどなら azure.functions.AsgiMiddleware
で良いはず。
import azure.functions as func
from flask_app.app import app
def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
return func.WsgiMiddleware(app.wsgi_app).handle(req, context)
これで、 全てのリクエストがWSGI/ASGIアプリの方に流れていく。
できたもの
ローカルで動かす
Azure Functions Core Tools 上で実行。
$ func start
Found Python version 3.8.6 (python3).
Azure Functions Core Tools
Core Tools Version: 3.0.3477 Commit hash: 5fbb9a76fc00e4168f2cc90d6ff0afe5373afc6d (64-bit)
Function Runtime Version: 3.0.15584.0
.
.
.
functions_wsgi: [GET,POST] http://localhost:7071/{*route}
http://localhost:7071/{*route}
となっている通り、全てのルートが functions_wsgi
に流され、 Flask なりに渡されるので、そっちで指定したパスで動ける。
/pet/{pet_id}
みたいなURLは試してないけど、
- GET
/hello?name={user_name}
- POST
/hello
- GET
/foo
- POST
/bar
を用意して、動作してるんで、あとはFlaskの話なのでまあいけるんじゃないかと。
$ curl -X GET 'http://localhost:7071/hello'
This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.
$ curl -X GET 'http://localhost:7071/hello?name=hoge'
Hello, hoge. This HTTP triggered function executed successfully.
$ curl -X POST 'http://localhost:7071/hello' -H "Content-Type: application/json" -d '{"name": "hoge"}'
Hello, hoge. This HTTP triggered function executed successfully.
$ curl -X GET 'http://localhost:7071/foo'
test
$ curl -X GET 'http://localhost:7071/bar'
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>
$ curl -X POST 'http://localhost:7071/bar' -H "Content-Type: application/json" -d '{"name": "hoge"}'
{"name":"hoge"}
デプロイ
Azure 自体にデプロイというか発行し、ローカル同様に動作してるんですが…
$ func azure functionapp publish xxxxxxxx
Getting site publishing info...
Creating archive for current directory...
Performing remote build for functions project.
Deleting the old .python_packages directory
Uploading 4.83 KB [###############################################################################]
Remote build in progress, please wait...
Updating submodules.
.
.
.
Detecting platforms...
Detected following platforms:
python: 3.8.6
Version '3.8.6' of platform 'python' is not installed. Generating script to install it...
.
.
.
Resetting all workers for xxxxxxxx.azurewebsites.net
Deployment successful.
Remote build succeeded!
Syncing triggers...
Functions in xxxxxxxx:
functions_wsgi - [httpTrigger]
Invoke url: https://xxxxxxxx.azurewebsites.net/{*route}?code=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
あとで調べる
デプロイ時に、
Invoke url: https://xxxxxxxx.azurewebsites.net/{*route}?code=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
なんだか知らないけど、codeが発行されるが1、これをパラメーターとして追加しないと404になる。これをどうしたら良いのか不明。チュートリアあるにある素の Functions では発行されなかったが。
あとは、API Management、ここはインフラとしてかな。JWT検証とか不要になりそうなんで便利そう。
-
コメント欄参照 Azure Functions の HTTP トリガー ↩