6
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

responder + Gunicorn + ApacheでWebサイトを公開する

Last updated at Posted at 2020-02-19

はじめに

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されるので便利です。

.envrc
source <venvのactivateファイルのフルパス>

STEP0:応答を返すプログラムを書く

以下のプログラムをグローバルに公開して動かすことを目標とします。
プログラム自体に関しては公式のQuick Startに。

main.py
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の設定項目は設定ファイルから読み込むこともできます。設定ファイルがあった方が後々都合がいいので、作成しておきます。
上記コマンドで用いた引数は最低限のものです。ここにログファイルの保存場所などを追加する形で設定ファイルを作成します。

gunicorn.py
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側設定

まずはリバースプロキシ用の設定ファイルを作成します。

/etc/apache2/conf-available/responder.conf
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で管理できるように設定します。
まずは設定ファイルの作成。

/etc/systemd/system/webapp.service
[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の文法についても書き留めておきたいと思います。

ルーティングの書き方

ルーティング(と処理部分)は、冒頭の書き方以外にもあるようです。

classを作成する+後でまとめてルーティングを設定する.py
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なファイルのパスを定義したり、何度も繰り返す処理をすっきりと書きたいときに使えます。

jinja_myfilter.py
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
main.py
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()
index.html
<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で脱プログラミング初心者を目指す日記

6
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?