開発時にFlask組込みサーバー、運用時にWSGIサーバー(Waitress)を起動するスクリプト

#### Flaskアプリ起動用シェルスクリプトの引数によって起動するサーバーを選択するスクリプトの作成方法

Qiitaの過去の投稿にWSGIサーバー(waitress)に関するものが何件か有りましたが、運用面から切り込んだ記事が見当たらなかったので、今回は実際に運用する場合に使うスクリプト作成方法を解説いたします。

### 想定する実行環境

+ OS: Linux (Raspberry Pi OS 含む)
+ **Python仮想環境を作成し、仮想環境の python で実行すること**
    + **Pythonバージョン: 3.8 ※本番環境は Raspberry Pi OS (Desktop)を想定**
    + **ライブラリ: Falsk, Waitress がインスール済みであること**
+ 各動作環境サーバーのホストとポート
    + 開発時: localhost(127.0.0.1), ポート: 5000
    + 運用時: 外部に公開可能なホスト名 (例) raspi-4-dev.local (192.168.0.20), ポート: 12345  
 ※1 起動時に環境変数に設定することを想定  
 ※2 **以降、Raspberry Pi OS (Desktop) をラズパイ４(開発機) とします。**

以下はこの記事のスクリプトを実行した開発PC(Ubuntu 22.04)の/etc/hosts の抜粋

```bash
# これらはOSインストール時に設定されたもの
127.0.0.1	  localhost
127.0.1.1	  Dell-T7500
# これはFlaskアプリケーション用に設定したもの
192.168.0.101  dell-t7500.local
```

### 参考URL

Flask: [(ja.readthedocs.io) クイックスタートQuickstart](https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/quickstart.html)
Waitress: [Pylons Waitress 2.1.2 documentation >> Usage](https://docs.pylonsproject.org/projects/waitress/en/stable/usage.html)

>The following will run waitress on port 8080 on all available IPv4 addresses, but not IPv6.
```python
from waitress import serve
serve(wsgiapp, host='0.0.0.0', port=8080)
```
>By default Waitress binds to any IPv4 address on port 8080. You can omit the host and port arguments and just call serve with the WSGI app as a single argument:
```python
from waitress import serve
serve(wsgiapp)
```

### リソース一覧

```
Flask_to_waitress/
├── flask_to_waitress
│   ├── app_main.py     # Flaskアプリケーション
│   └── templates
│       └── index.html
├── run.py   # 開発時にFlask組込みサーバー、運用時Waitressサーバーを起動するpythonスクリプト
└── start.sh # Webアプリケーションサーバー起動シェルスクリプト
```

### 1. Webサーバー起動シェルスクリプト

**```start.sh```**

(1) 環境変数の設定
+ 運用環境の引数が指定された場合: "prod" or "production"  
    + ホスト名を /etc/hostname から取得
    + **Flaskアプリのホスト名として ".local"を付与したあとに小文字化する**
    + **Flaskアプリホスト名を環境変数に設定**
    + **ポート番号を環境変数に設定**

+ FLASK_ENV環境変数に動作環境文字列を設定: "development" or "production"  

```shell
#!/bin/bash

# ./start.sh                    -> development
# ./start.sh prod | production  ->production

env_mode="development"
if [ $# -eq 0 ]; then
   :
else
   if [[ "$1" = "prod" || "$1" = "production" ]]; then
      env_mode="production"
   fi
fi

if [[ $env_mode = "production" ]]; then
   host_name="$(/bin/cat /etc/hostname)"
   IP_HOST_ORG="${host_name}.local"
   export IP_HOST="${IP_HOST_ORG,,}" # to lowercase
   # 通常はここでは指定しない
   export FLASK_PROD_PORT=12345
fi

export FLASK_ENV=$env_mode
echo "$IP_HOST with $FLASK_ENV"
```

(2) Flaskアプリ実行
+ 環境変数 "PATH_FLASK_WAITRESS" の有無チェック
    + 指定されている場合: 環境変数を実行パスに設定 ※開発PCのソースディレクトリを想定
    + 設定されていない場合: ユーザーホームの "Flask_to_waitress" ※本番環境を想定 
+ **python仮想環境 (py38_raspi4) に切り替える (activate)**
+ **python仮想環境のpythonで ```run.py``` を実行する**

```bash
EXEC_PATH=
if [ -n "$PATH_FLASK_WAITRESS" ]; then
   EXEC_PATH=$PATH_FLASK_WAITRESS
else
   EXEC_PATH="$HOME/Flask_to_waitress"
fi

. ${HOME}/py_venv/py38_raspi4/bin/activate

python ${EXEC_PATH}/run.py

deactivate
```

### 2. 実行環境に対応するサーバーを起動するpythonスクリプト

#### 2-1. Flaskアプリケーション

(1) HTMLテンプレート

```html
<!DOCTYPE html>
<html lang="ja">
<body>
<h2>Flask to Waitress on {{ server_host }}</h2>
<hr/>
<p> {{ current_date }}</p>
</body>
</html>
```

**```app_main.py```**

**Flask クラスのインスタンス作成**

```python

from datetime import datetime

from flask import Flask, render_template

app = Flask(__name__)


@app.route('/')
def index():
    server_host: str = app.config["SERVER_HOST"]
    now_dt: datetime = datetime.now()
    now: str = now_dt.strftime("%Y年%m月%d日 %H時%M分%S秒") 
    return render_template("index.html",
                           server_host=server_host, 
                           current_date=now
                           )
```

#### 2-2. 環境に応じたサーバーを起動するpythonスクリプト

**```run.py```**

+ 起動シェルスクリプトから環境変数を取得する
    + Flask実行環境 (**FLASK_ENV**) の取得: **運用("production")** | **開発("development")**
    + ホスト名 ("**IP_HOST**") の取得  
    **(A) 設定されている場合: 指定されたホスト名を使用する ※ "0.0.0.0" は使いません**  
    (B) 設定されていない場合: "localhost" を設定
+ Flask実行環境
    + 運用: waitress の serve関数をインポートし、ホストとポートを設定して起動する
    + 開発: Flask組込みサーバーを起動する 

```python
import os

from flask_to_waitress.app_main import app

if __name__ == "__main__":
    has_prod: bool = os.environ.get("FLASK_ENV", "development") == "production"
    IP_HOST: str = os.environ.get("IP_HOST", "localhost")
    PORT: str = os.environ.get("FLASK_PROD_PORT", "5000")
    app.config["SERVER_HOST"] = f"{IP_HOST}:{PORT}"
    port_num: int = int(PORT)
    if has_prod:
        # Production mode
        try:
            # Prerequisites: pip install waitress
            from waitress import serve

            serve(app, host=IP_HOST, port=port_num)
        except ImportError:
            print("Production Waitress server not found!")
            exit(1)
    else:
        # Development mode
        app.run(host=IP_HOST, port=port_num, debug=True)
```


### 3. 起動スクリプト実行

#### 3-1 (1). 開発PC: 開発環境として起動

(1) **予め開発PCのアプリケーションパスを環境変数に設定**してから実行

```bash
(py38_raspi4) $ export PATH_FLASK_WAITRESS=~/project/Qiita/flask_apps/Flask_to_waitress
(py38_raspi4) $ ./start.sh
with development
 * Serving Flask app 'flask_to_waitress.app_main'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://localhost:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 972-050-977
```

[ブラウザで確認] **```http://localhost:5000```**

![exec_localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/187d04c6-c6e5-cb00-cc34-0cd303622d6e.jpeg)

(2) 実行結果

```bash
127.0.0.1 - - [26/Nov/2023 17:04:05] "GET / HTTP/1.1" 200 -
```

#### 3-1 (2) 開発PC: 本番環境として起動

(1) 起動引数に **prod** を指定して起動

```bash
(py38_raspi4) $ ./start.sh prod
dell-t7500.local with production
```

[ブラウザで確認] **```http://dell-t7500.local:12345```**

![exec_production.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/80bd5ab9-3603-acb6-12ae-8ca2448aa04f.jpeg)

**Waitressサーバーを起動した場合、waitressのコンソールログは出力されません。**  
※ ```dell-t7500.local with production``` はシェルスクリプトで出力したもの


#### 3-2. ラズパイ４(開発機): 本番環境として起動

```bash
pi@raspi-4-dev:~/Flask_to_waitress $ ./start.sh prod
raspi-4-dev.local with production
```

![raspi4-dev_exec_production.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/40515e2e-6e58-8661-3cc2-324a2636f9f3.png)

**```netstat```** で待受けを確認

```bash
pi@raspi-4-dev:/etc $ sudo netstat -tp
稼働中のインターネット接続 (w/oサーバ)
Proto 受信-Q 送信-Q 内部アドレス            外部アドレス            状態        PID/Program name    
tcp        0      0 raspi-4-dev.local:12345 192.168.0.101:39602     ESTABLISHED 1272/python         
tcp        0      0 raspi-4-dev.local:ssh   192.168.0.101:50592     ESTABLISHED 747/sshd: pi [priv] 
tcp        0      0 raspi-4-dev.local:12345 192.168.0.101:39610     ESTABLISHED 1272/python         
tcp        0      0 raspi-4-dev.local:ssh   192.168.0.101:48736     ESTABLISHED 1077/sshd: pi [priv 
```

#### [補足] serve(app, host='0.0.0.0', port=12345) とするとどうなるか

ラズパイ側の /etc/hosts に **```192.168.0.20  raspi-4-dev```** の定義がなくとも、プライベート内のPCのブラウザから下記でアクセスできます
**※ブラウザアクセスするPC側では /etc/hosts に 上記 IPアドレスとホスト名の定義が必要です。**
```
http://rasi-4-dev:12345
```

#### ホスト名に".local"をつけている理由

ラズパイ４(本番機)がモバイル接続のAndroid端末向けにホスト名が".local"が付与されている前提でリバースプロキシを構築してるからになります。

ラズパイで稼働するFlaskアプリをインターネットにリバースプロキシ経由で公開する方法について下記GitHubで公開しております。

[IoT初歩の初歩 Androidアプリからラズパイに繫ぐ: 3.Webサーバによるリバースプロキシの構築](https://github.com/pipito-yukio/weather_data_viewer)


### 4. 結論

**Flaskアプリケーションの起動シェルスクリプトを作成することにより、Webアプリケーションのシステムサービス化が容易になります。**

本番環境ではサーバーが起動したら自動でアプリケーションが立ち上がりますよね。

Qiitaでの類似の投稿でこのような内容のものが見つからなかったのてあえて紹介させていただきました。

#### 参考として起動シェルスクリプトを使ったシステムサービス化のコード例を以下に示します

(1) 環境変数ファイル: **```/etc/default/webapp-flask-to-waitress```**  
```
FLASK_PROD_PORT=12345
```

(2) サービス設定ファイル: **```/etc/systemd/system/webapp-flask-to-waitress.service```** 

※1 コメント部分は通常はいれません。データベースサーバーの後などのサービスを設定します

```
[Unit]
Description=Flask webapp PlotWeather service
# わざとコメントにしていま ※本番環境ではデータベースサーバの起動後などの設定をするのが一般的
# After=postgres-12-docker.service

[Service]
Type=idle
# FLASK_PROD_PORT
EnvironmentFile=/etc/default/webapp-flask-to-waitress
ExecStart=/home/pi/Flask_to_waitress/start.sh prod >/dev/null
User=pi

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

システムサービス化に関しては下記Qiita記事もご覧ください。

[(Qiita) RaspberryPi Pythonアプリケーションをシステムサービス化する](https://qiita.com/pipito-yukio/items/ac2657adc45c453eea55)


**今回の記事の元になった起動シェルスクリプトは下記GitHubリポジトリでご覧になれます。**
[GitHub(pipito-yukio) UDP Weather Sensor packet monitor for Raspberry pi 4　src/installer](https://github.com/pipito-yukio/raspi4_apps/tree/master/src/installer)