Flask+Pipenv+Gunicorn+Apacheでwebサーバーを構築する

## 概要
開発時は、Flaskのbuild in サーバーで事足りましたが、いざ本番環境にdeployするときには、`WARNING: This is a development server. Do not use it in a production deployment.`とあるように、build in サーバーは使わない方がいいので、python系のフレームワークを本番環境で使うときにはどうしたらいいのかをまとめます。

もともとinstallされているpythonを使うと依存関係でごちゃごちゃするので、pyenvでinstallしたpythonおよびpipでpipenvをinstallし、そのpipenvを使って、パッケージを管理すると比較的わかりやすいです。

1. pipenvをinstallするためのpackageをinstallする
2. pyenvをinstallする
3. pyenvで好みのversionのpythonをinstallする

参考:
[pyenvを用いたpython環境構築手順（CentOS7.1)](https://qiita.com/ksugawara61/items/ba9a51ebfdaf8d1a1b48)

4. globalで利用するpipでpipenvをinstallする
5. pipenvでpackageを管理する(今回の場合は、flaskやgunicornなど)


## 開発環境
- CentOS Linux release 8.1.1911
- pipenv, version 2018.11.26
- pyenv 1.2.18-7-gae4d4893
- Server version: Apache/2.4.37 (centos)


## WSGI(Web Server Gateway Interface)について

![secrets-of-a-wsgi-master-2-1024.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264708/b35b51a6-cbee-737e-7f0c-291c18a34d64.jpeg)

PythonのwebアプリケーションをapacheやNginxといったwebサーバーと併用するときに必要となるInterfaceがWSGIというものです。WSGIは、webサーバーからのリクエストをバックエンドのPython webアプリケーションに飛ばします。そして、リスポンスが、webサーバーを介してクライエントに返されます。また、staticファイルに関しては、webサーバーが直接アクセスような仕組みにすることができます。WSGIの仕様に関しては、PEP 3333でまとまっています。



参考：
[PEP 3333を日本語訳しました
](https://qiita.com/sabigara/items/e02f7b08e7902427313a)
[Secrets of a WSGI master](https://www.slideshare.net/GrahamDumpleton/secrets-of-a-wsgi-master)

Flaskのdeployに関する[公式ホームページ](https://flask.palletsprojects.com/en/1.1.x/deploying/)にはWSGIに関して、`mod_wsgi (Apache)`を利用した例も挙げられていますが、今回は、[Gunicorn](https://github.com/benoitc/gunicorn)を使って設定しました。'


## pipenvのコマンド例
```sh
# 仮想環境のPATH
 pipenv --venv
/home/centos/.local/share/virtualenvs/flask-xxxxxx
# 仮想環境のpythonインタプリタ
pipenv --py
/home/centos/.local/share/virtualenvs/flask-xxxxxx/bin/python

# 仮想環境の生成&必要packageのinstall
pipenv install

# 仮想環境の削除
pipenv --rm

# 仮想環境のactivate
pipenv shell
```
pipenvを使うことで、pythonの仮想環境を構築することができます。仮想環境の場所は、デフォルトだと`$HOME/.local/share/virtualenvs/`以下に設定されますが、`PIPENV_VENV_IN_PROJECT=true pipenv install`とすると、カレントディレクトリ以下に`.venv`が生成されます。


## Flaskの設定

簡易的に次のようなディレクトリ構成だとします。

```
tree .
.
├── Pipfile
├── Pipfile.lock
.
.
.
├── app.py
└── gunicorn_config.py
```

開発時には、`pipenv run python app.py`でbuild in サーバーが起動することを確認します。

```py:app.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

if __name__ == '__main__':
    app.run(host='0.0.0.0')
```


## Gunicornの設定
gunicornは、コマンドでも指定できますが、次のような設定ファイルを読み込ませることも可能です。特に重要なのは、bindでgunicornの起動portの指定とworkersの数を設定することでしょうか。`accesslog = '-'`や`errorlog = '-'`はコンソールへの出力を意味します。

```py:gunicorn_config.py
bind = '127.0.0.1:8000'
backlog = 2048

workers = 8
worker_class = 'sync'
worker_connections = 1000
timeout = 30
keepalive = 2

umask = 0
errorlog = '-'
loglevel = 'info'
accesslog = '-'
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
```

参考：
[gunicorn/examples/example_config.py](https://github.com/benoitc/gunicorn/blob/master/examples/example_config.py)


## サービスの設定
サービスが停止したら、3秒後に再起動する設定を入れています。
ExecStartは、絶対パスで指定します。
gunicornは、`-c`オプションで、設定ファイルを読み込みます。
`app:app`は、app.pyのappを指定しています。

```
[Unit]
Description=Python Flask App
After=network-online.target

[Service]
User=centos
WorkingDirectory=/var/www/html/flask
ExecStart=/var/www/html/flask/.venv/bin/gunicorn -c gunicorn_config.py app:app
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
```

注意:
今回は、pythonの仮想環境をプロジェクトのディレクトリで管理する設定にしています。

## Apacheの設定
`proxyPass`で、クライエントから`/`にアクセスが来たら、`http://127.0.0.1:8000/`にリクエストを飛ばします。`ProxyPassReverse`を設定することで、Flask App内のリダイレクト処理を適切に行います。

```
<VirtualHost *:80>
        ServerName your.domain.name.com
        ProxyPass / http://127.0.0.1:8000/
        ProxyPassReverse / http://127.0.0.1:8000/
</VirtualHost>
```

## トラブルシューティング

### flaskの`url_for`のredirectが`http://127.0.0.1/8000/hoge`のようになってしまう


これは、クライエントからHTTPSで通信する時に発生するエラーで、次のようにrequestが送られていることが原因です。

`Browser -----HTTPS----> Reverse proxy -----HTTP----> Flask`

例えば、DNSサーバーとして`Cloudflare`を使えば、`flexible`の設定で、クライアントからの通信は、https、`Cloudflare`からwebサーバーへの通信は、httpで行われます(下記参照)。

![スクリーンショット 2020-05-05 15.15.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264708/ae3a594f-9d31-ec3c-d90a-147c0340b2a2.png)


ここで、`Reverse proxy`の役割を果たすapacheの`VirtualHost`の設定に次のHeaderを追加すればFlask側でうまく処理してくれます。

```conf
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
```
`X-Forwarded-Proto` リクエストヘッダーを使用すると、クライアントが`Cloudflare`への接続に使用したプロトコル (HTTP または HTTPS) を識別することができます。

参考：
[Flask Proxy Setups](https://flask.palletsprojects.com/en/0.12.x/deploying/wsgi-standalone/#proxy-setups)
[Setting X-Forwarded-Proto under Apache 2.4](https://webmasters.stackexchange.com/questions/97005/setting-x-forwarded-proto-under-apache-2-4)
[Flask url_for generating http URL instead of https](https://stackoverflow.com/questions/14810795/flask-url-for-generating-http-url-instead-of-https)
[How does Cloudflare handle HTTP Request headers?](https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-)


### 通信がうまくいかない
firewallやselinuxの設定を確認してみてください。