Snowflake CLIを使ってStreamlitアプリをデプロイする

## やること
Snowflake CLIをセットアップしてStreamlitアプリをSnowflake上にデプロイします。

## 準備
まずはSnowflake CLIをインストールします。
インストーラーの使用が推奨されていますが、私は環境の都合でpipを使用しました。

https://docs.snowflake.com/ja/developer-guide/snowflake-cli/installation/installation#install-with-pip-pypi

インストールが完了したら接続情報を更新します。
以下のようなtomlファイルを作成します。
私はVScodeのSnowflake拡張機能を使用しており、そちらのtomlファイルを流用しました。
↓こちらにありました。
"C:/Users/hogehoge/.snowflake/connections.toml"

```toml
# connections.toml
[MY_CONNECTION]
account = "XXXXXXX.japan-east.azure"
user = "MY_USER"
authenticator = "snowflake"
password = "xxxxxxxxxx"
database = "MY_DATABASE"
schema = "MY_SCHEMA"
warehouse = "WH_XSMALL"
role = "MY_ROLE"

# 複数の接続情報を保持できる
[ANOTHER_ACCOUNT]
account = "yyyyy"
user = "yyyyy"
authenticator = "snowflake"
password = "yyyyy"
```

コマンドプロンプトを立ち上げ、tomlファイルが格納してあるディレクトリまで移動します。
以下のコマンドを実行するとSnowflake CLIで使用する接続情報をデフォルト値として設定できます。これにより、コマンド実行の度に接続情報の引数を与える必要がなくなります。

```bash
snow connection set-default MY_CONNECTION
```
これで準備は完了です。

## やってみた
### テンプレートを作成する
今回はデスクトップ配下にstreamlit_appというフォルダを作成し、そこにテンプレートを格納します。ターミナルでデスクトップまで移動し、以下のコマンドを入力します。
```bash
snow init new_streamlit_project --template example_streamlit
```
**new_streamlit_project**はこれから作成するstreamlitアプリのプロジェクト名です。お好きなものを入力してください。
`--template`はテンプレートのソースです。今回は省略していますが、`--template-source`と一緒に指定することで、ローカルフォルダや自作のリポジトリからテンプレートをインポートすることができます。デフォルトでは以下のリポジトリからテンプレートをインポートします。

https://github.com/snowflakedb/snowflake-cli-templates

コマンドを実行すると立て続けに、streamlitアプリを保管するステージとアプリの実行に使うウェアハウス名、作成するstreamlitアプリ名を聞かれます。適当に入力してください。

コマンド実行が完了すると以下のようなフォルダが作られました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634883/602f6657-a2db-41e5-8008-47bfc58ad22e.png)

- common
モジュールとして利用するpyファイルの置き場。
- paged
複数ページのアプリを作る場合はこのフォルダにpyファイルを置きます。
- environment.yml
アプリで使用するpythonモジュールのリスト。
condaのsnowflakeチャンネルからインポートするもののリストです。
- snowflake.yml
アプリの環境設定ファイル。
- streamlit_app.py
アプリのエントリポイント用pyファイル。


### 環境を設定する
snowflake.ymlを編集します。
データベースとスキーマを設定して以下のようになりました。
```diff_yaml
# snoflake.yml
definition_version: '2'
entities:
  my_st_app:
    type: streamlit
    identifier:
      name: my_st_app
+      schema: MY_SCHEMA
+      database: MY_DATABASE
    main_file: streamlit_app.py
    pages_dir: pages
    query_warehouse: WH_XSMALL
    stage: STREAMLIT_DEV
    artifacts:
      - streamlit_app.py
      - environment.yml
      - pages
      - common/hello.py
```
`my_st_app`は`snow init`時に設定したアプリ名です。`identifier`の`name`にも同じものが入っています。
テンプレートに入っていないフォルダやファイルを追加したい場合は、`artifacts`に追記します。

ちなみに、このsnowflake.ymlには複数のstreamlitアプリを記載することができるようです。

### デプロイする
デプロイには以下コマンドを利用します。
```bash
snow streamlit deploy --replace -p new_streamlit_project --dbname MY_DATABASE --schema MY_SCHEMA
```
`-p`はプロジェクト名、`--dbname`はデータベース名、`--schema`はスキーマ名を渡します。冒頭で設定した接続情報にデータベース名やスキーマ名を含めている場合は省略できます。
`--replace`を指定するとアプリが既に存在する場合に上書きすることができます。
デフォルトはfalseですが、CI/CDで利用する場合は書いておくと良いと思います。

```text
Creating MY_DATABASE.MY_SCHEMA.STREAMLIT_DEV stage
Deploying files to @MY_DATABASE.MY_SCHEMA.STREAMLIT_DEV/my_st_app
Creating MY_DATABASE.MY_SCHEMA.my_st_app Streamlit
Streamlit successfully deployed and available under https://~
```
こんなログが出て完了しました。
アプリへのURLも出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634883/7b979bd6-4b0a-47bb-91f9-b7fbaf158247.png)
アプリも無事立ち上げることができました。

### コードに修正を加えて再デプロイする
ソースコードに変更を加えてもう一度デプロイしてみます。
pages配下に2つ目のページを作成しました。
```python
# pages/second_page.py
import streamlit as st

options = ["orange", "banana", "apple"]
selected_option = st.sidebar.radio("Please select:", options)
st.title("Hello world")
```
先程と同じコマンドを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634883/433a4ce7-1e01-4fc9-b5c6-0a19bbedbe70.png)
デプロイできました。ちゃんと新規追加したページが表示されています！

## 細かいことを検証する
### ステージの他ファイルに影響はあるのか？
デプロイコマンドを実行すると`Creating MY_DATABASE.MY_SCHEMA.STREAMLIT_DEV stage`というログが毎回出ます。実行の度に`CREATE OR REPLACE`が走っていたら、他のファイルが消えてしまうかも？と思い、試してみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634883/a13b3138-4f3f-40e2-b040-f0d6a607b663.png)
streamlitアプリをデプロイしているステージに`memo.md`を追加しました。
この状態で再度デプロイしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634883/27f576da-cce0-4a75-815d-4c8b3986bd01.png)
無事でした。
おそらく、デプロイ時には`CREATE STAGE IF NOT EXIST`として処理されているものと考えられます。

:::note info
（追記）
ステージに同名のファイルがある場合は上書きとなりますが、ファイル名を変更した場合は古い方のファイルは残り続けます。
これはプロジェクト内のファイルについても同様です。
:::

### importを利用する
condaからインポートするモジュールは`environment.yml`に書きますが、外部ステージなどに置いた自作のモジュールを記載することはできません。自作モジュールをstreamlitアプリで使用する場合は`snowflake.yml`に記載する必要があります。
```diff_yaml
# snowflake.yml
definition_version: '2'
entities:
  my_st_app:
    type: streamlit
    identifier:
      name: my_st_app
      schema: MY_SCHEMA
      database: MY_DATABASE
    main_file: streamlit_app.py
    pages_dir: pages
    query_warehouse: WH_XSMALL
    stage: STREAMLIT_DEV
+   imports:
+     - "@MY_STAGE/my_module.py"
    artifacts:
      - streamlit_app.py
      - environment.yml
      - pages
      - common/hello.py
```
```diff_python
# streamlit_app.py
import streamlit as st
from common.hello import say_hello
+ from my_module import hello_world

st.title(f"Example streamlit app. {say_hello()}")
+ st.write(hello_world())
```
```python
# my_module.py
def hello_world ():
    return 'Hello World'
```
my_module.pyはMY_STAGEというステージに配置しています。
再度デプロイしたところ、モジュールの読み込みができていることを確認できました。
プロジェクト外のコードを流用したい場合に使えそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634883/3aeaf12d-fbc6-4af4-a20d-854806625aa9.png)

### 権限を付与する
デプロイしたアプリを閲覧する権限は別のコマンドで実行する必要があります。
今回はPUBLICロールに閲覧権限を付与したいと思います。
```bash
snow streamlit share my_st_app PUBLIC
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634883/25a43df7-96bd-4a31-972c-ca7d7a5fc513.png)

View Onlyの権限が付与されていることを確認できました！

## 感想
Streamlit in SnowflakeをCI/CDしていくにあたって、様々な手法がありますが、Snowflake　CLIはコマンドがシンプルなので利用しやすそうだなと思いました。
Gitと連携したデプロイについても今後考えていきたいところです。