Nginx+gunicorn構成でFlaskを使う【ローカル環境編】

# 概要
Flaskアプリケーションを立ち上げる手順や関連する設定について記載します。
本記事の内容はローカル環境にDockerなど介さずに直接プロセスを立ち上げるケースとなります。
Dockerを利用したケースについては[Docker編](https://qiita.com/mintak21/items/d956389ee9338e6c0fe0)を参照ください。

# 前提
- ローカル環境でpythonが実行可能であること
- Pythonバージョンが3.8以上であること
- MacOS向けの記載となっている

# 全体像

![全体像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552343/88d08abc-701b-59b6-926a-f03ff5a5257d.png)

# 記事早見表
- 似たような記事を読んだとき、よくなんで？どういう意味？と感じる -> [○○ってどういうもの/こと？](#ってどういうものこと)
- 動けばいいや -> [動かしてみる](#動かしてみる)
- gunicornやNginxの設定ファイルについて知りたい -> [設定ファイル、起動/終了コマンドを紐解く](#設定ファイル起動終了コマンドを紐解く)

# 動かしてみる
まずは実際に上記構成で動かしてみましょう。
必要となるパッケージをインストールしておきます。

```bash:Console
brew install nginx
pip3 install gunicorn
```

## プロジェクトの構成と各ファイルの内容

```text
PROJECT
├── config
│   ├── gunicorn_settings.py # アプリケーションサーバの設定ファイル
│   └── nginx.conf           # Webサーバの設定ファイル
└── flask_app.py             # メインとなるアプリケーション
```
- アプリケーションファイル
<br>今回、アプリケーションの挙動については重要視しないので、文字列を返却するルートを2つだけ用意することとします。

```python:flask_app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index() -> str:
    return 'Index Page!'

@app.route('/health')
def health() -> str:
    return 'Health Check OK!!'
```
- Webサーバ設定ファイル

```python:gunicorn_settings.py
import os

bind = '127.0.0.1:' + str(os.getenv('PORT', 9876))
proc_name = 'Infrastructure-Practice-Flask'
workers = 1
```
- Nginx設定ファイル

```nginx:nginx.conf
# 詳細は設定ファイルの項を参照
worker_processes 1;

events {
  worker_connections 512;
}

http {
  server {
    listen 9123;
    server_name INFRA-PRACTICE-NGINX;
    charset UTF-8;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Host $host;

    location / {
      proxy_pass http://127.0.0.1:9876;
    }

    location /health {
      proxy_pass http://127.0.0.1:9876/health;
    }
  }
}
```

## プロセスを起動する
```bash:Terminal
# Webサーバの起動
nginx -c ${PROJECT_PATH}/config/nginx.conf

# アプリケーションサーバの起動
gunicorn flask_app:app -c config/gunicorn_settings.py
```

## 疎通確認する
```bash:Terminal
curl http://localhost:9123/
# -> Index Page!と返ってくればOK
curl http://localhost:9123/health/
# -> Health Check OK!!と返ってくればOK
```

# 設定ファイル、起動/終了コマンドを紐解く
## gunicorn
### 設定ファイル
[公式ドキュメント](http://docs.gunicorn.org/en/latest/settings.html)では、オプション形式で記載されています。
設定ファイルを利用する場合、設定項目名をkeyとした`key=value`形式で表記します(*e.g.* `loglevel = 'debug'`)。設定ファイルの拡張子は任意です。
設定の一部を以下に示します。

|key|意味|型&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|デフォルト値|
|:-----|:-----|:-----|:-----|
|reload|コードを変更したとき自動的に読み直して再起動する|bool|False
|accesslog|サーバへのアクセスログ出力先<br>ファイルを指定、「-」だと標準出力を意味する<br>*e.g.* `accesslog = logs/gunicorn_access.log`|string|None
|errorlog|エラーログ出力先<br>ファイルを指定、「-」だと標準出力を意味する<br>*e.g.* `errorlog = logs/gunicorn_err.log`|string|'-'
|proc_name|psコマンドで表示されるプロセス名称<br>特に指定しないとdefault_proc_nameである「gunicorn」となる|string|None
|limit_request_line|リクエストの最大サイズ、DDos対策で指定<br>同様の項目にlimit_request_fields|int|4094
|bind|接続ホストおよびポート<br>HOST:PORT形式で指定<br>*e.g.* `bind = os.getenv('HOST') + ':' + os.getenv('PORT')`|string|127.0.0.1:8000
|daemon|gunicornプロセスをデーモン起動する|bool|False
|workers|リクエストをさばくWorkerプロセスの数<br>親プロセスが1つ起動し、workersの数だけ子プロセスを立てこちらでハンドルする<br>並列を考慮できていない場合に2以上を指定すると、不具合のもととなる|int|1
|worker_connections|最大接続数|int|1000

### 起動/終了コマンド
【起動】

```bash:Terminal
gunicorn {Flaskアプリケーションファイル名}:{ファイル内のFlaskインスタンス変数名} -c {設定ファイルのパス}
```
※設定ファイルの項で述べたとおり、Settingは起動時オプションでも指定可能。

【終了】
プロセス起動しているTerminalにおいて、Ctrl+C

## Nginx
### 設定ファイル
[公式ドキュメント](https://nginx.org/en/docs/)を見てみると、gunicornの`key=value`形式とは異なり、
Module内部にDirective(命令)をコンテキスト形式(*curly bracket:* `{}` で囲まれるブロック)で記述することがわかります。

#### `Module`
|Module名|記載する内容
|:----|:----|
|core|ログ、プロセス制御<br>最も外側のコンテキストがこれにあたる
|events|イベント系処理
|**http**|Webサーバにかかわる処理<br>おそらく最も多く記述するのがこのモジュール

※そのほか、mailやstreamが存在。

#### `Directive Syntax in http context`
httpコンテキスト内にはserverコンテキスト、serverコンテキスト内にはさらにlocationコンテキスト
といったように、コンテキストinコンテキスト形式で記述していきます。
ここではhttpブロックの設定例を示します。例を見ることで、冒頭の設定ファイルが何を意味しているか理解が進むのではないでしょうか。

- 基本の設定

```nginx:config
http {
    server {
        listen        80;                          # 80番ポートで接続を待つ
        server_name   MY_SERVER;                   # サーバ名はMY_SERVER
        charset       UTF-8;                       # レスポンスヘッダのContent-typeをUTF-8に指定
        error_page    404  /404_not_found.html;    # ステータスコードが404のときに404_not_found.htmlへ内部的にリダイレクト
    }
}
```

- リバースプロキシ

```nginx:config
http {
    server {
        # /indexへのアクセスを
        location /index {
            proxy_pass          http://127.0.0.1:8080;  # 127.0.0.1の8080ポートへリバースプロキシする
            proxy_set_header    Host    $host;          # プロキシされたサーバへ渡すリクエストヘッダのうち、Hostに$hostを設定し直す
　　　　　　  proxy_read_timeout  60s;                    # プロキシされたサーバからの応答が60秒なかったらタイムアウトとする
        }
    }
}
```

- アクセス制御/IPフィルタリング

```nginx:config
http {
    server {
        location / {
            allow 192.168.0.1;    # 192.168.0.1からのアクセスを許可して
            deny  all;            # それ以外のネットワークアクセスを拒否する
        }
    }
}
```

- BASIC認証

```nginx:config
http {
    server {
        location / {
            auth_basic             "Basic Auth closed site";　　# BASIC認証が必須の旨を明示し、
            auth_basic_user_file   conf/htpasswd;               # .htpasswdファイルパスを指定する
        }
    }
}
```

- Referrerチェック

```nginx:config
http {
    server {
        # リクエストヘッダにRefererがない(none)、ヘッダにあるがhttp://またはhttps://で始まらない(blocked)、
        # mintak.comで終わらない(server_names) ルートからのアクセスの場合はinvalidと判定 ->$invalid_refererに1をセット
        valid_referers none blocked server_names *mintak.com
        if ($invalid_referer) {
           return 403;  # invalidと判定した場合は403 Forbiddenで返却
        }
    }
}
```

#### `Embbed Variable`
Nginxサーバにおける環境変数のようなもの。詳しくは[こちら](https://nginx.org/en/docs/http/ngx_http_core_module.html#variables)。
一部を以下に示します。

|variable|内容
|:----|:----|
|$host|リクエストヘッダのHost、ない場合はサーバ名
|$remote_addr|接続クライアントのアドレス
|$remote_port|接続クライアントのポート
|$status|レスポンスのステータス
|$server_name|サーバの名称
|$http_*name*|HTTPリクエストヘッダのフィールド、nameの部分にヘッダフィールド名を小文字、underscoreで設定<br>*e.g.* http_host、http_referer

#### 設定のバリデーションチェック
`-t`をつけることで設定ファイルのバリデーションを行うことが可能です。

```bash:Terminal
nginx -c {設定ファイルの絶対パス} -t
```

問題ない場合は`syntax is ok`/`test is successful`と表示され、設定誤りがあると以下のように問題のある箇所が表示されます。

```text
nginx: [emerg] "proxy_pass" directive is not allowed here in /Users/mintak/infra-server-env-build-practice/config/nginx.conf:29
nginx: configuration file /Users/mintak/infra-server-env-build-practice/config/nginx.conf test failed
```

### 起動・終了コマンド
【起動】

```bash:Terminal
nginx -c {設定ファイルの絶対パス}
```

- 相対パスの場合、nginxのルートからのパスとなるため、`nginx: [emerg] open() "{RELATIVE PATH}" failed (2: No such file or directory)`とメッセージが出ることがあります。

【終了】

```bash:Terminal
nginx -s stop

# 上記に失敗する場合はプロセスを特定してkill
ps ax | grep nginx
kill -9 ${PID}
```

【起動確認】

```bash:Terminal
ps aux | grep nginx    # ここでnginx: master processと出てきた場合はnginxが動いている。

# PORTの確認ができるコマンド
lsof -i -P | grep nginx
#=> nginx     41845 mintak    6u  IPv4 0x595e7f947462ada5      0t0  TCP *:9123 (LISTEN)
```


# ○○ってどういうもの/こと？
ここまでのファイルを用意してコマンドをたたけば目的は達成できます。しかし、自分のような初学者には難しい言葉や概念が多く出ています。ここからは、そうしたキーワードに関して自分なりの解釈を記載していきます。

## どうしてこのような構成になるのか
有識者曰く、かつてはWebサーバとアプリケーションサーバの分割はされていなかったとのこと。この構成が問題となったのは、インターネットの普及に伴い、一般公開しているサービスに対するアクセス数が増えたことでサーバへの負荷が高まり、全体的にパフォーマンスが落ちるという自体に直面したときでした。
そこで、負荷を分散させることで安定稼働することを目指し、後述のように役割に応じてサーバを分割する流れになりました。この思想が現代においては当たり前で、一般的にWebサービスは`Webサーバ<-->アプリケーションサーバ<-->DBサーバ`という三層構造で作られているそうです。
**役割ごとにプロセス(サーバ)を分ける**という根本の考え方は、コーディングの際の設計思想(1クラス/1関数ごとに明確な役割は1つずつ)と通じるところがあると思います。

### でも`python flask_app.py`で動くじゃん
これはFlaskというフレームワークがWebサーバ/アプリケーションサーバを兼任した状態で立ち上げることのできるフレームワークであるためです。
**開発時の簡単な検証や動作確認をする際**など、わざわざWebサーバとアプリケーションサーバを別で立ち上げるのがメンドウな場合に有用です。
このコマンドでの起動時に、`WARNING: This is a development server. Do not uses it in a production development.` とログが出てきます[^1]が、あくまで開発向けであり、この構成だと大量アクセス処理でパンクするなどのリスクが大きいから**本番ではやめてね**、という意味です。

※flask_app.pyに`app.run(host='0.0.0.0, port=9876)`を追記すると上記コマンドでも立ち上がる

### Webサーバとアプリケーションサーバの主な役目
それぞれ一言でまとめます。詳しくはReferenceのリンクページを参照ください。

- Webサーバ　　　　　　：要求に対して、だれがアクセスしても変わらないもの(静的処理)を対応。TOPページなどがこれにあたりやすい。
- アプリケーションサーバ：アクセスに応じて返答が変わるもの(動的処理)を対応。会員Myページなど(会員によって表示内容が異なる)。

## リバースプロキシってなんでリバース？
リバースがあるってことはフォワードがあるか、というと**あります**。
通常我々がプロキシと呼称しているものが、フォワードプロキシにあたります。

### フォワードプロキシとリバースプロキシの違いはなにか
互いにプロキシであることには変わりありません。クライアント->インターネット間での処理がフォワードプロキシ(サーバ)、インターネット->ターゲットWebサーバ間での処理がリバースプロキシ(サーバ)です。[^2]

![Flask_Nginx_unicorn_diagramdrawio-Step1-Porxy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552343/9e28e1ec-c5a2-cda4-cee6-56188282eeee.png)


直接目的のサーバにつなげばいいじゃん！と思われるかもしれませんが、プロキシサーバをかませることによるメリット(下記参照)を受けることができます。

### フォワードプロキシの用途
|用途|詳細
|:-----|:-----|
|省力化|1度アクセスしたサイトのデータを一時的にキャッシュしておくことで2回目以降のアクセスでは、Webにアクセスせずにキャッシュからデータを取得するフローとしてデータ取得までの時間を短縮することが可能。
|コンテンツフィルタリング|特定のサイトに対するアクセスをここで遮断する。<br>**~~会社PCでえっちなサイトが見られないのは大体こいつのせい~~**

### リバースプロキシの用途
|用途|詳細|
|:-----|:-----|
|セキュリティ強化|特定のアクセス方法(http://、https://など)ではない場合や特定のIPアドレス以外からのアクセスを遮断するなどの機構を持たせることが出来る。
|負荷分散(ロードバランサ)|一度クッションとなることで、複数台あるアプリケーションサーバに対してうまいことアクセスを分配する。
|高速化|静的搾取。画像、htmlファイルなどの参照用のコンテンツをキャッシュで保持することでサーバの負荷を軽減したりより速いレスポンスを返したりすることが可能。

## WSGI(ウィズギー)とは
Web Server Gateway Interfaceの略。~~通称上杉くん。~~
DjangoやFlaskはWSGI対応しているフレームワークで、gunicornやuWEGIはWSGI対応のサーバなんだよね、くらいの認識で問題ないかと思います。
登場の経緯は次のような感じ。[^3]

>・フレームワークAちゃん > 私イケてるからイケてるWebサーバのαくん以外ムリ
>・フレームワークBちゃん > 僕は手堅いβくんがいいな♡

みたいな感じ(？？)で、過去、PythonWebフレームワークごとに使用できるWebサーバが制限(逆も然り)されるということがままありました。実はαくんは、Bちゃんに使ってもらいたいのでしたが、ここでAちゃんとの許嫁的関係が足を引っ張ることになります。
この問題を解決しようと登場したのが~~上杉くんこと~~WSGIです。WSGIは「サーバとフレームワークは自由にお互いを選んで~~恋愛~~接続できるようにしようよ」ということで、アプリケーションフレームワーク<-->Webサーバの関係において、どのようにやりとりをするかというルールを定めました。
WSGIとWSGI対応しているサーバ、フレームワークのおかげで、今日のワタシたちは自由にサーバとフレームワークの組み合わせを選ぶことができるようになったのであります。めでたしめでたし。

# References
- [gunicorn公式ドキュメント](http://docs.gunicorn.org/en/latest/index.html)
- [Nginxドキュメント(原本)](https://nginx.org/en/docs/)
- [Nginxドキュメント(日本語訳)](http://mogile.web.fc2.com/nginx/index.html)
- [Nginx設定のまとめ(Qiita)](https://qiita.com/syou007/items/3e2d410bbe65a364b603)
- [WebサーバとAPサーバの違い(Think IT)](https://thinkit.co.jp/article/11837)

[^1]: `FLASK_ENV=development`と設定していない場合。
[^2]: 今回はNginxがプロキシサーバとWebサーバを兼任している形です。
[^3]: こんなアホな説明の仕方はここにしかないと思います。。。