🥱

格安でSteamlitのDockerコンテナをデプロイする方法

2024/06/30に公開

動機:何も考えずにStreamlitアプリをデプロイしたい

Streamlitは,Pythonのみで簡単にWebアプリケーションを構築できるライブラリです.社内ツールやプロトタイプを手軽に作ることができるため,非常に重宝しています.

しかし,作成したアプリケーションのデプロイ段階になると,意外と面倒なことがわかります.調査すると様々な選択肢が存在しますが,それぞれに一長一短があり,理想的な解決策をなかなか見つけることができずにいました.

私が求めているのは,主に以下の3点です:

  • セキュリティ:社内からのみアクセス可能にすること
  • 簡便性:インフラの知識をあまり必要としない方法であること
  • コスト効率:特に,使用されていない時間帯のコストを抑えられること

IP制限やSSOログインなどで社内限定のアクセスを実現したいです.また,開発や運用においてインフラ関連の作業を最小限に抑え,Streamlitアプリケーションの作成に集中できる環境が理想的です.さらに,アプリケーションの維持費用を可能な限り抑えることで,使用頻度が低い期間のコストを軽減したいと思います.一方で,可用性やパフォーマンスについてはそこまで重視していません.あくまでも少人数が使う内部ツールやプロトタイプ向けだと割り切っています.

解決策:Fly.io & Caddy

Fly.io:簡便性とコスト効率

Fly.ioは,コンテナ化されたアプリケーションを簡単にデプロイできるプラットフォームです.

Fly.ioは,コスト効率と簡便性の要求を満たしてくれます.scale to zero により,アプリケーションが使用されていない時間帯には自動的にリソースがゼロにスケールダウンされ,コストが発生しません.つまり,使用していない時間帯のランニングコストを完全に削減できます.またDockerfileがあればアプリケーションを簡単にデプロイできるので,インフラの知識がなくてもアプリを作成できます.

https://fly.io/docs/about/pricing/

設定を絞れば,全く使われない月でも$0.15,30日間毎日24時間稼働し続けても$3.19という格安構成も実現可能です.

Caddy:セキュリティ要件

残るセキュリティの要求を満たすために,Caddyを利用します.CaddyはWebサーバーの一種で,Nginxと同じような機能(リバースプロキシ,ロードバランサなど)を持っています.

Caddyの大きな特徴はSSL証明書の自動発行です.何も設定しなくてもLet's Encryptで証明書を発行してくれます.Nginxの場合はSSL証明書発行のためにcertbotのインストール等の作業が必要でしたが,この作業が不要になります.

しかし今回の構成ではCaddyのSSL証明書自動発行機能は利用しません.実はFly.ioがSSL証明書を発行してくれるため,CaddyはIP制限やBasic認証などのアクセス制御とリバースプロキシのためだけに利用しています.Caddyを利用したのは単純にCaddyを使ってみたかったからというだけであり,おそらくNginxでも全く同じことが実現できます.

コード

GitHubにあります.
https://github.com/ikumasudo/fly-caddy-streamlit

重要な部分を説明します.

fly.toml

fly.toml
...
[http_service]
  internal_port = 80
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']
...

Fly.ioの設定ファイルであるfly.tomlです.[http_service]のセクションでauto_stop_machines auto_start_machines min_machines_running を設定することで,リクエストがない間は起動中のマシンの台数を0にまで減らし,リクエストの数に応じてマシンを起動させるという動作にしています.利用頻度が少ないアプリであればかなりのコスト削減になります.またinternal_portは,コンテナがリクエストを受け付けているポートを指定します.ここではCaddyが80番で待っているので80を設定します.

Caddyfile

Caddyfile
http://{$FLY_APP_NAME}.fly.dev {
  @allowed_ip {
        header Fly-Client-Ip {$ALLOWED_IP}
  }
  @denied_ip {
        not header Fly-Client-Ip {$ALLOWED_IP}
  }
  basic_auth {
		# Username "Bob", password "hiccup"
		Bob $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
	}
  reverse_proxy @allowed_ip localhost:8501
  respond @denied_ip "Access Denied" 403
  log {
        output file /var/log/caddy/access.log
  }
}

ここでIP制限やBasic認証を設定しています.ここではIP制限とBasic認証を設定していますが,もちろんどちらか片方だけ設定することもできます.

Fly.ioの仕様で,クライアントのIPアドレスはFly-Client-Ipというヘッダーに格納されます.なのでCaddyではこのヘッダーを見てIP制限を行います.最初,Caddyのドキュメントを参考んしてremote_ipを使っていたためうまく動きませんでした.アクセスログを見てremote_ipにクライアントのIPが入っていないことに気づき解決しました.

Fly.ioの環境変数は fly secrets コマンドから設定できます.ALLOWED_IPには許可したいIPアドレスを設定します.FLY_APP_NAMEはFly.ioが提供してくれる環境変数なので設定は不要です.

デプロイ手順

初回

Fly.ioを使ったことがない方は,Fly.ioのアカウント作成・CLIインストールを行ってください.無料枠が存在しますが,アプリを作成するためにはクレジットカードの登録が必要だったと思います.

git clone https://github.com/ikumasudo/fly-caddy-streamlit

Fly.ioにアプリケーションを作成します.

cd fly-caddy-streamlit
fly app create

アプリの名前を聞かれるので適当に設定します.ここではfly-stに設定します.

output
be4rr@be4rr:~/projects/fly-st$ fly app create
? Choose an app name (leave blank to generate one): fly-st
? Select Organization: Ikuma Sudo (personal)
New app created: fly-st

ここで設定したアプリ名をfly.tomlで設定します.

fly.toml
- app = 'fly-streamlit'
+ app = 'fly-st'

環境変数ALLOWED_IPを設定します.

fly secrets set ALLOWED_IP=[IPアドレス]

デプロイします.

fly deploy

デプロイが完了したあと,https://[アプリ名].fly.dev(例えばfly-st.fly.dev)にアクセスするとBasic認証でユーザー名Bobとパスワードhiccupを求められるので入力します.ALLOWED_IPに設定されたIPから接続していればStreamlitアプリが表示されます.

変更の反映

コードや設定の変更を反映させたい場合は,fly deployを実行するだけです.簡単ですね.

参考文献

Discussion