Python Flask アプリを Windows IIS で公開する 2 つの方法

# はじめに
ASP.NET(.NET Framework)が稼働している社内 IIS (Internet Information Service)サーバに Python Flask で開発した REST API を立てることになりそうだったので、ホストする方法の調査・検証を行いました。メモしておきます。

# 検証環境
Windows Server 2019
サーバマネージャーはこんな↓状態で、Python のインストールは済んでいるものとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/323a6089-8bf0-0379-8a74-138d8f97cdc4.png)

# 調査結果(概要)
まず Python Flask の内蔵サーバはそのままプロダクション環境で利用すべきではないとされているので、(社内用だけど一応) WSGI 対応サーバを利用、Windows に対応している Waitress を利用することにします。

[Deploying to Production](https://flask.palletsprojects.com/en/3.0.x/deploying/#)
[Waitress](https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/deploying/waitress.html)

IIS 側は以下のいずれかで対応できそうなので両方検証し、どちらもうまくいきました。

a. IIS に HttpPlatformHandler をインストールして、Web.config に Python で Web アプリを起動するコマンド類を書くことで、IIS で受けたリクエストを転送してくれる

https://learn.microsoft.com/ja-jp/visualstudio/python/configure-web-apps-for-iis-windows?view=vs-2022

b. IIS をリバースプロキシにして WSGI サーバへフォワードする

(WSGI サーバへ直接着信すればよいのでは、、、という話は横においておきます。)


# 検証1 (上記 a. のパターン)
事前に HttpPlatformHandler をインストールしたうえで、
[HttpPlatformHandler](https://www.iis.net/downloads/microsoft/httpplatformhandler)

適当なプロジェクトフォルダを作り(C:\WebSites\HelloWorldPyW1 とした)、venv 下で flask, waitress をインストールする。
```
cd C:\WebSites\HelloWorldPyW1
python -m venv venv
.\venv\Scripts\Activate
pip install flask waitress
```
  
Python Flask のコードをプロジェクトフォルダ直下に書いて動作確認する。
```python_iis_w.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return '<p>Hello World! with Waitress1</p>'

if __name__ == '__main__':
    app.run(debug=True)
```

```
python python_iis_w.py
```

Waitress から呼び出すように、以下のファイルを同じ場所に作成する。これはデプロイ環境にのみ必要なファイル。

```waitress_server.py
from waitress import serve
import python_iis_w

# HttpPlatformHandler でプロセスが起動する時に、待受ポート値が
# HTTP_PLATFORM_PORT という環境変数で与えられるので、
# この値を利用して waitress を起動する。
import os
PORT=os.environ.get("HTTP_PLATFORM_PORT")

# Web.config 側でポートを起動パラメータに指定して、args からとる方法でも OK
# import sys
# PORT=sys.argv[1]

serve(python_iis_w.app, host='127.0.0.1', port=PORT)
```

Web.config も同じ場所に作成する。(一応 %HTTP_PLATFORM_PORT% 環境変数を起動パラメータでも指定)

```
﻿<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="PythonHandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified"/>
    </handlers>
    <httpPlatform processPath="C:\WebSites\HelloWorldPyW1\venv\Scripts\python.exe"
                  arguments="waitress_server.py %HTTP_PLATFORM_PORT%"
                  stdoutLogEnabled="true"
                  stdoutLogFile="c:\home\LogFiles\pythong.log"
                  startupTimeLimit="60"
                  processesPerApplication="16">
      <environmentVariables>
      </environmentVariables>
    </httpPlatform>
  </system.webServer>
</configuration>
```

* ログパスへはとりあえず Users の変更権限を付与しておいたが IIS_USRS の変更権限を与えておくのがよいと思う
* 動作確認したところ stdoutLogFile に指定したファイル部分は主ファイル名として利用され、その後ろに  "\_" + プロセスID + "\_" + タイムスタンプ + ".log" が付与される形でログ出力された

次に IIS マネージャで別サイトを作成し、対象フォルダを指定して、ブラウザで確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/d6ff4238-f8ad-4dcb-7732-d2406baa2393.png)

![Screenshot from 2024-07-15 19-30-34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/a0265661-55dd-dd40-aa11-a2786d071a40.png)

# 検証2 (上記 b. のパターン)
事前に URL Rewrite module v2.1 と ARR(Application Request Routing) v3.0 をインストールしたうえで、
[URL Rewrite](https://www.iis.net/downloads/microsoft/url-rewrite)
[Application Request Routing](https://www.iis.net/downloads/microsoft/application-request-routing)

検証1 と同じソース2つ(*.py)を別フォルダ(C:\PythonPJ\PythonIISw2 とした)にコピーして、同様に venv を行い、ソースを調整する。
* 出力文言は識別のために "Hello World! with Waitress2" に変更しておいた。
* waitress は IIS 経由で起動されず、検証時は手動起動。稼働環境ではタスクスケジューラなどからスタートアップ起動することになる。

```python_iis_w.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return '<p>Hello World! with Waitress2</p>'

if __name__ == '__main__':
    app.run(debug=True)
```

```waitress_server.py
from waitress import serve
import python_iis_w

serve(python_iis_w.application, host='127.0.0.1', port=8220)
```

* 待ち受けポートは環境変数から読み込んでもよいがここでは決め打ちとし(8220)、IIS 側でも同じ値で転送する必要がある。強いて一元化したいなら、後述する Web.config の action タグから読み込むように Python 側を作り込めばできそう。

事前にリバースプロキシ設定のベースとなる空のフォルダ(C:\WebSites\HelloWorldPyW2 とした)を作っておき、IIS で Web サイトを追加する時に指定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/ac00f625-201c-dcdc-ee19-c752fcaf2cf2.png)

次にサイトをクリックして、中央ペインから [URL 書き換え] をWクリックして、[規則の追加...] > [リバース プロキシ] と進む。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/5ce3e4f9-98ef-7581-4a8d-da362ed9b0c6.png)

受信規則に IP アドレスと Waitress の待ち受けポートを入力し、[OK]。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/d5abcf13-8ca5-1fe2-74c1-c7b3028d58dd.png)

以下でアプリを起動し、ブラウザで動作確認。
```
.\venv\Scripts\Activate
python waitress_server.py
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/04696318-04be-8c1b-c1f1-eb5a77c5e7f6.png)

C:\WebSites\HelloWorldPyW2 には Web.config だけが存在し、IIS マネージャで構成したリバースプロキシの情報が書き込まれていることが確認できる。

```
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <rule name="ReverseProxyInboundRule1" stopProcessing="true">
                    <match url="(.*)" />
                    <action type="Rewrite" url="http://127.0.0.1:8220/{R:1}" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>
```


# うまくいかなかったこと
IIS の Default Web Site 下のサブフォルダに ASP.NET のサイトが構築されているので、同じ要領で並列化したかったが無理のようでした。
よく考えると、異なるプロセスでポートを共有するのがそもそも NG なのだろうと思います。