Streamlit を Azure App Serviceで動かす！

# はじめに
データサイエンティストな皆さんに人気が出始めている [Streamlit](https://streamlit.io/) という Webフレームワークがあります。

とても簡易的に作れて便利なフレームワークですが、これを Azure App Service で動かす手順をあまり見かけないので書き記しておきます。
ここで解説する手順ですが、先にやり方をまとめておくと次のようになります。

1. ローカルで開発（VSCode の devContainerを使う）
1. Azure App Serviceを作成
    - Python 3.10で作る
    - SCMデプロイ時にビルドが走るようにアプリケーション設定に値をセット
    - Startupコマンドに streamlit 用のコマンドをセット
1. GitHub にRepositoryを作成し、ソースコードをPush
1. Azure App ServiceからGitHub 連携をセットアップ
1. デプロイ結果を確認

GitHub+App　Service　に慣れている人からすれば、ほぼいつも通りのCI/CDですが、2の設定がポイントです。そこだけ知りたい、という方はそこだけご確認ください。

# 1. ローカルで Streamlit を動かす

仮想環境を作り、そこに Streamlit をインストールする手順が推奨だそうですが、Visual Studio Code の devContainer で環境を作る方が好みなので今回はそっちでやります。

- ローカル環境
    - Visual Studio Code
        - devContainerプラグイン
    - Docker Desktop

## devContainer で Python 環境を作る

適当なフォルダを作り、そこに Dockerfile を次のように作成します。

```dockerfile
FROM python:3.10-bullseye

RUN pip install streamlit numpy pandas matplotlib
```

2022/12/19の時点で Streamlit は Python 3.10までがバージョンの上限、と公式に記載があるので最新である 3.11ではなく3.10を指定しています。 pip でインストールするのは　Streamlit だけでいいのですが、どうせデータ分析するならnumpy,pandas, matplotlibとか使うよね〜と思って追記しています。不要なら削除、他に必要なものがあるなら後ろに追加しましょう。

# フォルダをコンテナとして開く

Visual Studio Code 立ち上げて、左下の緑アイコンをクリックします。このアイコンは devContainerプラグインが VSCode にインストールされていないと表示されません。表示されていない人はインストールしましょう。

https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers

Open Folder in Containerを選択します。フォルダ選択ダイアログが立ち上がるので Dockerfile が存在するフォルダを指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/12922b15-057c-fbaa-6fde-3145cca674d2.png)

From 'Dockerfile" を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/a434df7a-ae43-4aa4-8373-37f00417827a.png)

コンテナに追加する アプリを選びます。後で GitHub へプッシュするので git を入れておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/64bbc874-fc19-2b30-96d8-e28e17c62078.png)

すると .devContainerフォルダが新規作成され、その中にdevContainer.jsonという設定ファイルも新規作成されます。そして、Dockerfileを使ってコンテナが立ち上がり、そのコンテナにVSCodeから接続している状態になります。接続した状態は、このように左下の緑アイコンが変化します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/1770d9c3-22d5-32c5-efab-3dd67bbc0efa.png)

もし立ち上げに失敗した場合は、フォルダをVSCodeで開き直してください。するとフォルダをコンテナで開き直しますか？と聞かれるのでOKを選択すれば再度接続してくれるはずです。

では、稼働確認です。ターミナルをVSCodeで立ち上げて次のコマンドを叩きます。

```bash
streamlit hello
```

ポートフォワードについてのこんなダイアログが上がってくるので、ブラウザーで開くを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8340aabf-0b58-77aa-dfeb-0397ee75665c.png)

ブラウザーでこんなデモ画面が表示されれば、ローカルの環境構築はOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/03ac4137-6fcd-021c-4ea6-21640fa2a877.png)

## 実装する

適当にテスト用のページを実装しましょう。フォルダ内に main.py というファイルを新規作成して次のように実装します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/baf587f9-ff42-7fc8-4718-ba99b9095fda.png)

```python
import streamlit as st

st.title('Hello!')
```

先ほど streamlit hello を入力したターミナルを CTRL+Cで停止して、次のコマンドを入力します。

```bash
streamlit run main.py
```

先ほどのデモ画面が表示されているブラウザをリロードすると、こんな感じになるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c57d89ac-e1ed-3df1-d90c-d46370aeba90.png)

### App Service　デプロイ後に pip でインストールしたいパッケージを requirements.txtに書く

Dockerfile に記載した環境に必要なパッケージは、このままでは App Service には存在しないのでうまく動きません。 App Serviceは立ち上げ時に requirements.txt に記載されたパッケージを pip でインストールしてくれる機能があります。なので、requirements.txtファイルを新規作成して、そこにDockerfileに記載したパッケージを記載します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/d03a3460-f4ac-e183-a632-3c283f7c6c6a.png)

```text:requirements.txt
streamlit
numpy
pandas
matplotlib
```

# 2. Azure App Serviceを作成

次の情報をベースに Azureで App Serviceを作成してください。他の値はご自由に。

- 公開: コード
- ランタイム スタック: Python 3.10

### Streamlit 用に App Service を構成する

この後、GitHubからソースをデプロイさせるのですが、デプロイされた App Service側では　　requirements.txtを読んで pip でパッケージをインストールするセットアップをしてもらう必要があります。この動作は構成で行います。アプケーション設定にSCM_DO_BUILD_DURING_DEPLOYMENTというKEYを作り、値にtrueまたは1をセットします。最後に保存ボタンをクリックすることをお忘れなく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/bf2a0065-cfc8-47c8-04b6-78b08643aafe.png)

次にスタートアップスクリプトを設定します。streamlit はWebサーバー機能を含んでいますので、streamlit をスタートアップで動かさなければなりません。ローカルで実装したのはmain.pyでしたのでコマンドは次のようになります。

```bash
python -m streamlit run main.py   --server.port 8000 --server.address 0.0.0.0
```

これを、全般設定のスタートアップ コマンドに設定します。くどいようですが最後に保存ボタンをクリックすることをお忘れなく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/17c6d644-4ecb-a04a-5592-1f46ed386f30.png)

# 3. GitHub にRepositoryを作成し、ソースコードをPush

特に説明は不要でしょう。この操作は純粋なGitの操作になります。作成ずみのローカル環境をGitHubなどのRemoteリポジトリに紐づけるのは一般的に次のようなコマンドを叩きます。

```bash
git init
git remote add origin Path_To_Remote_Repository
git commit --allow-empty -m "first commit"
git push -u origin main
```

Path_To_Remote_Repositoryは、GitHubで作成したリポジトリのPathに置き換えてください。最初に空のコミットをPushするコマンドになっているのは単に私の趣味です。

# 4. Azure App ServiceからGitHub 連携をセットアップ

メニューのデプロイセンターでソースを GitHub とし、必要ならログインしてリポジトリ、ブランチを選択します。自分でGitHubに作ったリポジトリなので特に不明点はないはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ca4dcdf7-40a9-f906-42cb-cbc209556445.png)

保存ボタンをクリックして少し経つと、設定が終わります。GitHub に作成したレポジトリを開き、Actionを見てみましょう。パイプラインが稼働中となっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/4ffda723-fcdd-0bf8-01f0-7098be0f99d3.png)

しばらく待つと、このパイプラインが終了します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/1d60b528-6259-52cb-0fd3-73a978c876b4.png)


# 5. デプロイ結果を確認

App Service の概要にあるURLを開いて、デプロイ結果を確認しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/b3770719-3352-ddab-0ed1-a68a151d9249.png)

無事にデプロイされました。

# まとめ

Streamlit の App Serviceへのデプロイはコンテナを使った方法は見かけますが、このように直接デプロイすることもできます。（裏では結局コンテナ使っているんですけど） シンプルなのでオススメです。

AppServiceでホストしたんだから、AzureADによる認証をつけたい！という方がいらっしゃると思います。すごく簡単にできます。AzurePortalで次の操作をします。

1. 認証クリック
1. IDプロバイダーを追加をクリック
1. Microsoft を選択
1. 追加

これだけでAzureAD認証が追加されます。アクセスしたユーザーが誰なのかを知りたい、など Claimにアクセスする場合は　Request Header から値を取得することができます。 streamlit を使って Request Header にアクセスするサンプルソースを作りましたので参考にしてください。（Streamlit 1.16で動作確認済み）

```py
import streamlit as st
from streamlit.web.server.websocket_headers import _get_websocket_headers


st.title('Hello! This streamlit app is on Azure AppService.')

headers = _get_websocket_headers()
principal_name = headers.get("X-Ms-Client-Principal-Name")
principal_id = headers.get("X-Ms-Client-Principal-Id")
access_token = headers.get("X-Ms-Token-Aad-Access-Token")

st.header("AzureAD's auth results")

if principal_name is not None:
    st.markdown('X-Ms-Client-Principal-Name: ' + principal_name)
if principal_id is not None:
    st.markdown('X-Ms-Client-Principal-Id: ' + principal_id)
if access_token is not None:
    st.markdown('X-Ms-Token-Aad-Access-Token: ' + access_token)

st.header('http headers count')
st.text(len(headers))

st.header('List http headers')
if headers is not None:
    for key, value in headers.items():
        text = 'key={}, value={}'
        st.text(text.format(key, value))
```