Leapcell: The Best of Serverless Web Hosting
WSGIを使用した最小限のPython Webアプリケーションの構築
PythonのWebアプリケーションを書いたことがある人、特にオンラインでデプロイを行ったことがある人は、必ずWSGIプロトコルを耳にしたことがあるはずです。これは、PythonのWebサーバとWebアプリケーションの間のデータ交換インターフェイスを定義しています。この説明はかなり抽象的かもしれませんので、以下で実際の例を通じて詳しく説明しましょう。
本番環境でのWebアプリケーションのデプロイ
DjangoやFlaskなどのWebアプリケーションフレームワークを使用してWebアプリケーションを開発したとしましょう。これらのフレームワークの公式ドキュメントでは、通常、フレームワークの組み込みサーバー、例えばDjangoのpython manage.py runserver
やFlaskのflask --app hello run
は、開発段階でのデバッグにのみ適しており、本番環境でのトラフィックを処理することはできないと指摘されています。本番環境にデプロイする際には、WebアプリケーションをWebサーバの背後で実行する必要があります。一般的なWebサーバにはGunicornやuWSGIがあります。Webサーバは、プロセスモデルやスレッドモデルなどの並列処理オプションを提供し、Webアプリケーションの並列処理性能を向上させます。
上記の単純なシナリオでは、4つの技術選択の組み合わせがあります:Gunicorn + Django、Gunicorn + Flask、uWSGI + Django、およびuWSGI + Flask。各組み合わせにおいてWebアプリケーションフレームワークが異なるWebサービス適応コードを提供する必要がある場合、その複雑さは$N^2$に達し、明らかにコスト効率が悪くなります。WSGIが存在する理由は、WebサーバとWebアプリケーションの間のインターフェイスを定義することで、フレームワーク開発者はこのインターフェイスに対してのみコーディングを行えばよいということです。こうすることで、Webアプリケーション開発者は選択においてより自由になります。例えば、同じDjangoコードであれば、GunicornとuWSGIの両方で実行することができます。
Webアプリケーションフレームワークを使用しない場合
DjangoやFlaskがWSGIにどのように適応するかを議論する前に、まず問題を単純化しましょう。Webフレームワークの役割は、ルーティングやHTTPリクエストの解析など、便利な機能を提供し、私たちがより簡単かつ迅速にWebアプリケーションを開発できるように支援することです。しかし、非常に単純なアプリケーションの場合は、フレームワークを使用しない選択もできます。
PEPによって定義されたWSGIインターフェイスは非常にシンプルで、(しかも使用するべきではない)どんなWebフレームワークも使用する必要はありません:
HELLO_WORLD = b"Hello world!\n"
def simple_app(environ, start_response):
"""Simplest possible application object"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [HELLO_WORLD]
この単純なWebアプリケーションは、environ
環境変数辞書とstart_response
関数を通じてWebサーバとやり取りします。そしてWebサーバは正しいパラメータが渡されるように保証します。
上記のコードがapp.py
として保存されており、Gunicornがインストールされていると仮定します。以下のコマンドを使用してアプリケーションを起動することができます:
gunicorn app:simple_app
デフォルトでは、Gunicornはポート8000にバインドされます。私たちはcurl
を使用してリクエストを送信してテストすることができます:
$ curl http://localhost:8000
Hello world!
このように、すべてが期待通りに動作しています。同時に、このWebアプリケーションのコードロジックは非常に単純です。リクエストパス(例えば/
、/api
など)やリクエストメソッド(例えばGET、POST、PUTなど)を考慮しておらず、常にステータスコード200とHello World!
をレスポンスボディとして返します。
$ curl http://localhost:8080/not-found
Hello world!
$ curl -X POST http://localhost:8080
Hello world!
Hello Worldを超えて
先に述べたように、通常のWebアプリケーションは通常、複数のエンドポイントを持ち、異なるリクエストに応じて異なるレスポンスを返すことが期待されます。
Webサーバは、リクエストのすべての情報をenviron
辞書に保存します。また、他の環境変数も含まれます。すべてのキーの中で、特に以下の3つに注目する必要があります:
-
REQUEST_METHOD
:リクエストメソッド、例えばGET、POSTなど。 -
PATH_INFO
:リクエストパス。 -
wsgi.input
:ファイルオブジェクト。リクエストボディにデータが含まれる場合、このオブジェクトを通じて読み取ることができます。もう1つのキーCONTENT_LENGTH
は、リクエストボディの長さを示し、通常はこの2つが一緒に使用されます。
/
パスに新しいPOSTインターフェイスを実装し、JSON形式のパラメータを受け取るようにしたいとしましょう。ユーザーが{"name": "xxx"}
を渡すと、WebアプリケーションはHello, xxx!
を返し、一方、GETインターフェイスは変わらず、引き続きHello, World!
を返すようにします。コードは以下の通りです:
import json
def simple_app(environ, start_response):
request_method = environ["REQUEST_METHOD"]
path_info = environ["PATH_INFO"]
response_headers = [('Content-type', 'text/plain')]
if path_info == '/':
status = '200 OK'
if request_method == 'GET':
body = b'Hello world!\n'
elif request_method == 'POST':
request_body_size = int(environ["CONTENT_LENGTH"])
request_body = environ["wsgi.input"].read(request_body_size)
payload = json.loads(request_body)
name = payload.get("name", "")
body = f"Hello {name}!\n".encode("utf-8")
else:
status = '405 Method Not Allowed'
body = b'Method Not Allowed!\n'
else:
status = '404 NOT FOUND'
body = b'Not Found!\n'
start_response(status, response_headers)
return [body]
リクエストパスとリクエストメソッドを処理するだけでなく、いくつかの簡単なクライアントエラー検出も追加しました。例えば、/
以外のパスにアクセスすると404を返し、/
にGETまたはPOST以外のメソッドでアクセスすると405を返します。以下はいくつかの簡単なテストです:
$ curl http://localhost:8080/
Hello World!
$ curl -X POST http://localhost:8080/ -d '{"name": "leapcell"}'
Hello leapcell!
$ curl -X PUT http://localhost:8080/
Method Not Allowed!
$ curl http://localhost:8080/not-found
Not Found!
Flaskに近づける
Webアプリケーションのロジックが複雑になるにつれて、simple_app
関数はますます長くなります。このような「スパゲッティコード」は明らかに良好なプログラミング慣習に沿っていません。私たちはFlaskのAPIを参考にして、簡単なカプセル化を行うことができます。
例えば、関数を呼び出せるクラスに変換して、Webアプリケーション開発者がWSGIアプリケーションを取得できるようにします。routes
を使用して、すべてのパスからハンドラ関数へのマッピングを保存します。environ
をrequest
オブジェクトにカプセル化するなどです。
class MyWebFramework:
def __init__(self):
self.routes = {}
def route(self, path):
def wrapper(handler):
self.routes[path] = handler
return handler
return wrapper
def __call__(self, environ, start_response):
request = self.assemble_request(environ)
path_info = environ["PATH_INFO"]
if path_info in self.routes:
handler = self.routes[path_info]
return handler(request)
else:
status = '404 NOT FOUND'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [b'Not Found!\n']
app = MyWebFramework()
@app.route("/my_endpoint")
def my_endpoint_handler(request):
# business logic here to handle request and assemble response
response_headers = [('Content-type', 'text/plain')]
status = '200 OK'
body = b'Endpoint response!\n'
return [body]
このように、MyWebFramework
の部分は徐々にWebアプリケーションフレームワークに抽象化でき、Webアプリケーションの実際のビジネスロジックは、各ハンドラ関数を書くだけです。flask.app.Flask
のソースコードを見ると、同じような実装方法が使われています。FlaskアプリケーションはFlask
コアクラスから派生しており、このクラス自体がWSGIアプリケーションです。
Djangoの設計はやや異なります。Djangoは、非同期リクエストをサポートするためにASGI(Asynchronous Server Gateway Interface)プロトコルを提案し、実装しました。Djangoアプリケーションは、内部関数を通じてASGIアプリケーションまたはWSGIアプリケーションに変換することができます。私たちがWSGIの部分にのみ注目すると、その原理は先に紹介したものと似ていることがわかります。
おすすめの読み物
Leapcell: The Best of Serverless Web Hosting
最後に、Pythonサービスをデプロイするための最高のプラットフォーム:Leapcell をおすすめします。
🚀 好きな言語で構築
JavaScript、Python、Go、またはRustで簡単に開発できます。
🌍 無料で無制限のプロジェクトをデプロイ
使用した分だけ支払います — リクエストがなければ請求はありません。
⚡ 使った分だけ支払い、隠された費用はありません
アイドル料金はなく、シームレスにスケーラブルです。
🔹 Twitterでフォローしてください:@LeapcellHQ