はじめに
responderという、比較的新しい(2018年10月~)Python Webフレームワークがあります。
作者はrequestsなどを作った方で、いわくFlaskとFalconのいいとこどりのようなフレームワークだそうです。
Pythonで簡単なWebページを作ろうと思い立った際にresponderのことを知り、気になったので使ってみることにしました。
見た限りではApacheを用いた解説がなかったので、自分の構築方法をメモ代わりに載せておきます。(もしあればぜひ教えてください……)
環境
- Ubuntu 18.04.3 LTS
- Apache 2.4.29
- Python 3.6.8
- Gunicorn 19.9.0
- responder 1.3.2
※ 現在のresponderの最新バージョンはv2.0.5ですが、v1.3.2時点で下記の設定を行ったあとに最新にバージョンアップしても使えています。
ググってみた感じではNginxの方が構築しやすそうでしたが、元々Apacheを入れていたサーバなのでそのまま作ってみることにしました。
ちなみに、Pythonの環境構築ツールについてはvenvのみ使用しています。
venvで環境を作成した上で、作業ディレクトリに以下のようなdirenv設定ファイルを適用すると、作業ディレクトリにcd
すると同時にactivate
されるので便利です。
source <venvのactivateファイルのフルパス>
STEP0:応答を返すプログラムを書く
以下のプログラムをグローバルに公開して動かすことを目標とします。
プログラム自体に関しては公式のQuick Startに。
import responder
api = responder.API()
@api.route("/{who}")
def greet_world(req, resp, *, who):
resp.text = f"Hello, {who}!"
if __name__ == '__main__':
api.run()
この場合、例えば/world
にGETでアクセスするとHello, world!
と表示されたり、/testtesttest
にGETでアクセスするとHello, testtesttest!
と表示されたりします。
STEP1:responderのビルトインサーバで動かす
responderにはビルトインサーバとしてUvicornが内蔵されています。
まずはresponder(+ Uvicorn)で起動を試します。
$ python main.py
INFO: Started server process [693]
INFO: Waiting for application startup.
INFO: Uvicorn running on http://127.0.0.1:5042 (Press CTRL+C to quit)
$ curl http://127.0.0.1:5042/world
Hello, world!
プログラムを実行するだけで自動的にサーバが立ち上がり、つなげるようになっているのが分かります。
サーバを落とすときは書いてあるとおりCtrl+C
で大丈夫です。
STEP2:Gunicornを用いて動かす
Uvicorn公式ページいわく本番環境とするにはGunicornを使う方がいいそうなので、公式の設定を参考に動かしてみます。
(この記事を書こうとしてからかなり時間が経ってしまったので、タイムスタンプが数か月前ですが気にしないでください)
$ pip install gunicorn
$ gunicorn -k uvicorn.workers.UvicornWorker main:api
[2019-10-31 09:39:11 +0900] [1227] [INFO] Starting gunicorn 19.9.0
[2019-10-31 09:39:11 +0900] [1227] [INFO] Listening at: http://127.0.0.1:8000 (1227)
[2019-10-31 09:39:11 +0900] [1227] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2019-10-31 09:39:11 +0900] [1230] [INFO] Booting worker with pid: 1230
[2019-10-31 09:39:12 +0900] [1230] [INFO] Started server process [1230]
[2019-10-31 09:39:12 +0900] [1230] [INFO] Waiting for application startup.
$ curl http://127.0.0.1:8000/world
Hello, world!
Gunicornを立ち上げる際の引数についてはざっくり以下のような感じ。
-
-k uvicorn.workers.UvicornWorker
: ワーカークラスにUvicornを指定する -
main:api
: 起動するモジュールを指定。記法は「モジュール名(プログラム名):responder.API()
の変数名」
Gunicornの設定ファイルを作成する
Gunicornの設定項目は設定ファイルから読み込むこともできます。設定ファイルがあった方が後々都合がいいので、作成しておきます。
上記コマンドで用いた引数は最低限のものです。ここにログファイルの保存場所などを追加する形で設定ファイルを作成します。
import multiprocessing
import os
name = "gunicorn"
accesslog = "<アクセスログを書き込みたいファイル名>"
errorlog = "<エラーログを書き込みたいファイル名>"
bind = "localhost:8000"
worker_class = "uvicorn.workers.UvicornWorker"
workers = multiprocessing.cpu_count() * 2 + 1
worker_connections = 1024
backlog = 2048
max_requests = 5120
timeout = 120
keepalive = 2
user = "www-data"
group = "www-data"
debug = os.environ.get("DEBUG", "false") == "true"
reload = debug
preload_app = False
daemon = False
各項目に関しては公式Docsを参照。
設定値は、参考サイトのものを真似させてもらっています。
この設定ファイルを適用してGunicornを立ち上げるには、以下のコマンドを使います。
$ gunicorn --config gunicorn.py main:api
STEP3:Apacheをリバースプロキシサーバとして用いて動かす
最後に、Apacheを通して接続できるよう設定します。
Apacheが動いているhttp://example.com/ に接続された際、Gunicornが待ち構えている http://localhost:8000 にプロキシするように設定を行います。
GunicornにプロキシするためのApache側設定
まずはリバースプロキシ用の設定ファイルを作成します。
ProxyRequests Off
ProxyPass "/" "http://localhost:8000/"
ProxyPassReverse "/" "http://localhost:8000/"
設定ファイル&プロキシ関連モジュールの有効化を行います。
$ sudo a2enconf responder
$ sudo a2enmod proxy_http proxy
設定ファイルが正しく記述できているかを確認後、リロード。
$ sudo apache2ctl configtest
Syntax OK
$ sudo systemctl reload apache2ctl
Gunicornの自動起動設定
自動起動をしやすくするため、Gunicornの起動をsystemdで管理できるように設定します。
まずは設定ファイルの作成。
[Unit]
Description=gunicorn - responder
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=<gunicorn.pyやmain.pyを置いているディレクトリのフルパス>
ExecStart=<Gunicornのフルパス> --config <gunicorn.pyのフルパス> main:api
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
こちらも分かりにくい項目について説明を入れておきます。
- ファイル名: <任意のサービス名>.serviceで大丈夫です。コマンドでの使用例は`systemctl restart <設定したサービス名>` など。
- [Service]-User, Group: Apacheのものを使用していますが、これで正しいのかは不明……。WorkingDirectory配下のファイルの所有者も変更しておいた方がいいかもしれません。
- [Service]-ExecStart: STEP2の「Gunicornの設定ファイルを作成する」にあるコマンドをフルパスで記述したものです。
作成後、サービスを立ち上げて自動起動をするよう設定します。
$ sudo systemctl start webapp.service
$ sudo systemctl enable webapp.service
念のため、きちんと動いているか確認します。
$ sudo systemctl status webapp.service
● webapp.service - gunicorn - responder
Loaded: loaded (/etc/systemd/system/webapp.service; enabled; vendor preset: enable
Active: active (running)
(以下省略)
起動、自動起動ともに成功していますね。
完成!
設定がうまくいっていれば、curlやブラウザで接続できるはずです。
$ curl http://example.com/world
Hello, world!
未解決?問題
何故かApacheを通すとhtmlのContent-Type
ヘッダが消失するという現象が起こっています。
(Gunicornからサーバを立ち上げた際にはきちんと付いています。謎現象)
暫定策としてレスポンスを返す際、強制的にContent-Type: text/html; charset=UTF-8
を付けるコードを追加することにしています。
おまけのメモ
ついでに、個人的に便利だと思った/ググって見つけにくかったresponderの文法についても書き留めておきたいと思います。
ルーティングの書き方
ルーティング(と処理部分)は、冒頭の書き方以外にもあるようです。
import responder
api = responder.API()
class Who:
def on_get(self, req, resp, *, who):
# GETのときは自動的にこっちの処理をする
resp.text = f"Hello, {who}!"
async def on_post(self, req, resp, *, who):
# POSTのときは自動的にこっちの処理をする
data = await req.media()
resp.text = f"{data}"
# ルーティングの設定
api.add_route("/{who}", Who)
if __name__ == '__main__':
api.run()
冒頭の書き方がFlask風、今書いた書き方がFalcon風なんでしょうか?
自分自身は「クラスでon_get
などを用いて記述+デコレータでルーティング設定」で書いていますが、もしかすると邪道なんですかね……。
Jinja2のフィルタを追加する
staticなファイルのパスを定義したり、何度も繰り返す処理をすっきりと書きたいときに使えます。
def css_filter(path):
return f"./static/css/{path}"
def list_filter(my_list):
return_text = "<ul>\n"
for l in my_list:
return_text += f"<li> {l} </li>\n"
return_text += "</ul>"
return return_text
import responder
import jinja_myfilter
api = responder.API()
# フィルタを追加
# v1.xの場合
api.jinja_env.filters.update(
css = jinja_myfilter.css_filter,
html_list = jinja_myfilter.list_filter
)
# v2.xの場合(2020/05/12追記)
# (アンダースコアがあることから_envは内部的な値扱いのようですが、
# ソースを見てもこれ以外の指定方法を見つけることができませんでした……)
api.templates._env.filters.update(
css = jinja_myfilter.css_filter,
html_list = jinja_myfilter.list_filter
)
@api.route("/")
def greet_world(req, resp):
param = ["項目1", "項目2"]
resp.content = api.template("index.html", param=param)
if __name__ == '__main__':
api.run()
<link rel="stylesheet" type="text/css" href="{{ 'form.css' | css }}">
<!-- Jinja2で処理されて以下のようになる
<link rel="stylesheet" type="text/css" href="./static/css/form.css">
-->
{% autoescape false %}
{{ param | html_list }}
{% endautoescape %}
<!-- Jinja2で処理されて以下のようになる
<ul>
<li> 項目1 </li>
<li> 項目2 </li>
</ul>
-->
なお、htmlタグを含む文字列が返される場合は{% autoescape false %}~{% endautoescape %}
で囲まないと自動エスケープが働きます。ただし渡しているパラメータがユーザ入力のものだった場合、当然パラメータもエスケープされずに出力されてしまうので気をつけてください。フィルタ内でhtml.escape()
などを用いて処理しておくのが無難でしょうか。
参考サイト
ResponderをUvicornやGunicornでデプロイする方法 - 技術とかボドゲとかそんな話をしたい
Python responder 入門のために… 下調べ - Qiita
Django + Nginx + Gunicorn でアプリケーションを立ち上げる | WEBカーテンコール
【第1回】ResponderとKerasを使って機械学習Webアプリケーションを作ってみる【大枠作成編】 – 株式会社ライトコード
改行をに変換するJinja2のカスタムフィルター - Google App Engine+Pythonで脱プログラミング初心者を目指す日記