🪟

【完全ガイド】Streamlit + WindowsコンテナをAzure(ACR, ACI, AKS)にデプロイする方法

に公開

はじめに

「StreamlitアプリをWindows環境で動かしたいけど、Macだから開発しにくい…」そんな悩みを抱えていませんか?
本記事は、ローカル環境の制約をAzureの力で乗り越える方法を解説します。
Azureやコンテナ技術の初学者から、特定の課題を持つ中級者まで、幅広い方におすすめです。

この記事では、Pythonの人気Webアプリケーションフレームワークである Streamlit で作成したアプリを、Windowsコンテナ化し、最終的にMicrosoft AzureのマネージドKubernetesサービスである AKS (Azure Kubernetes Service) にデプロイするまでの一連の手順を、具体的なコードやコマンドを交えて詳しく解説します。

この記事で構築する構成

この記事では、以下の流れで、Mac上のソースコードからAzure上のAKSにアプリケーションをデプロイします。

  1. ローカル (Mac): Dockerfile などのソースコードを準備します。
  2. ACR Task: Macからコマンドを実行し、Azure上でWindowsコンテナイメージをビルドします。
  3. ACI (任意): ビルドしたイメージを使い、ACIで簡単な動作確認を行います。
  4. AKS: 本番環境としてAKSのWindowsノードプールにコンテナをデプロイし、外部に公開します。

用語解説

Streamlit とは、Python だけで Web アプリを簡単に作成できるフレームワークです。
Streamlitの設計思想は「シンプルさと開発スピードの最大化」のため、高度な Web 開発には向いていません。

実行環境

  • 端末: MacBook Air M4
  • 環境: Docker Desktop for Mac

Mac OS で Windows イメージをビルドする方法

カーネルの違いにより、Mac OS ではローカル環境で Windows イメージのビルドはできません。
そのため、Azure の ACR タスク機能を使用してWindows イメージをビルドします。

ACR タスク機能を使用すると、タスクを実行するための専用 VM が内部的にデプロイされて、そこでタスクが実行されます。

まず、Azure にログインします。

# 自分のアカウントを使う場合
az login

# サービスプリンシパルを使う場合
az login --service-principal --username "クライアントID" --password "クライアントシークレットの値" --tenant "テナントID"

リソースグループと、Basic SKU の ACR を作成します。

az group create --resource-group "リソースグループの名称" --location japaneast
az acr create --resource-group "リソースグループの名称" --name "ACR の名称" --sku basic --location japaneast

ACR タスク機能を使ってイメージをビルドします。
このコマンドは、Dockerfile があるディレクトリで実行してください。
ビルドが完了すると、専用の VM から ACR に自動的にイメージがプッシュされます。

az acr build -t "ビルドするイメージ" -r "ACR の名称" --platform windows .

任意で、ACR タスク機能を使って、ACR 上にできたイメージを DockerHub にプッシュします。

az acr run \
  --registry "ACR の名称" \
  --platform windows \
  --cmd "docker login -u 'DockerHub のユーザー名' -p 'アクセストークン' ; docker pull 'イメージの名称' ; docker tag 'ACR 上のイメージを' 'どういう風にタグ付けするのか' ; docker push 'プッシュするイメージ'" \
  /dev/null

イメージのビルドに使ったファイル達

以下の 3 つのファイルを使いました。
詳細は、以下のタブをクリックしてご確認ください。

Dockerfile (ここをクリック)
Dockerfile
FROM mcr.microsoft.com/windows/servercore:ltsc2022

ADD https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe python-installer.exe

RUN start /w python-installer.exe /quiet InstallAllUsers=1 PrependPath=1

WORKDIR C:/app

COPY app.py .
COPY start.cmd .

RUN python -m pip install --upgrade pip
RUN python -m pip install streamlit

EXPOSE 8501

CMD ["C:\\app\\start.cmd"]
コマンドプロンプト (ここをクリック)
start.cmd
@echo off

:main_loop

echo Starting Streamlit process in headless mode...

:: Add --server.headless=true to completely disable interactive prompts.
python -m streamlit run app.py --server.port=8501 --server.address=0.0.0.0 --server.enableCORS=false --server.enableXsrfProtection=false --server.headless=true

echo.
echo Streamlit process exited at %TIME%. This should not happen. Restarting in 10 seconds...
echo.

:: Wait for 10 seconds using the stable ping command to avoid errors seen in logs.
ping 127.0.0.1 -n 11 > nul

:: Go back to the top of the loop to restart the process.
goto main_loop
Python スクリプト (ここをクリック)
app.py
# app.py
import streamlit as st

st.title('これは Windows コンテナー上の Streamlit アプリケーションです')

st.write('この画面が見れたってことは、テストは無事成功')

name = st.text_input('名前を入力してください', 'World')
st.write(f'こんにちは, {name}!')

各コードの詳細な解説は以下をご覧ください。(解説は生成 AI によるもの)

Dockerfile の解説 (ここをクリック)
Dockerfile
# === ベースイメージの指定 ===
# Dockerイメージを構築するための土台となるOSイメージを指定します。
# ここでは、Windows Server 2022 の Server Core エディションを使用しています。
# Server Coreは、GUIを持たないCUIベースのWindows Serverで、コンテナ用途に適しています。
# 以前の試行で、より軽量な "nanoserver" ではPythonの.exeインストーラーが動作しなかったため、
# 高い互換性を持つ "servercore" を選択しています。
#【重要】このOSバージョンは、デプロイ先となるAKSのWindowsノードプールのOSバージョンと一致させる必要があります。
# (例: 2022でビルドしたイメージは、2022のノードで動かす)
FROM mcr.microsoft.com/windows/servercore:ltsc2022

# === Python実行環境のセットアップ ===
# Pythonの公式インストーラーをインターネットからダウンロードし、イメージ内に配置します。
# ADD命令は、URLからファイルを直接ダウンロードしてコンテナ内にコピーする機能を持っています。
# ここでは、ダウンロードしたファイルを "python-installer.exe" という名前で保存しています。
ADD https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe python-installer.exe

# ダウンロードしたインストーラーを実行して、コンテナ内にPythonをインストールします。
# RUN命令は、イメージのビルド中にコマンドを実行するためのものです。
# "start /w" は、指定したプログラムが完全に終了するのを待つためのコマンドです。
# これがないと、インストールが終わる前に次のステップに進んでしまい、ビルドが失敗します。
# 各オプションの意味:
#   /quiet: サイレントインストールを実行し、UIを表示しません。
#   InstallAllUsers=1: このコンテナの全ユーザーが利用できるようにPythonをインストールします (例: C:\Program Files\Python39)。
#   PrependPath=1: Pythonの実行ファイルがあるフォルダへのパスを、システムの環境変数PATHの先頭に追加します。
#                  これにより、後のステップで "python" や "pip" といったコマンドを直接実行できるようになります。
RUN start /w python-installer.exe /quiet InstallAllUsers=1 PrependPath=1

# === アプリケーションのセットアップ ===
# これ以降の命令を実行する際の作業ディレクトリ(カレントディレクトリ)を C:\app に設定します。
# このディレクトリが存在しない場合は、自動的に作成されます。
WORKDIR C:/app

# ローカルマシン上にあるアプリケーションのソースコードと起動スクリプトを、イメージ内にコピーします。
# 1つ目の "." はコピー元のファイル名、2つ目の "." はコピー先(WORKDIRで指定した C:\app)を意味します。
COPY app.py .
COPY start.cmd .

# === 依存パッケージのインストール ===
# Pythonのパッケージ管理ツールであるpip自体を、最新バージョンにアップグレードします。
# これは、古いpipが原因で発生する可能性のある問題を回避し、ビルドログの警告を抑制するためのベストプラクティスです。
# "python -m pip" は、環境変数PATHに依存せず、現在有効なPythonのpipを確実に呼び出すための堅牢な方法です。
RUN python -m pip install --upgrade pip

# Streamlitライブラリと、それが依存するすべてのパッケージ(pandas, numpyなど)をインストールします。
RUN python -m pip install streamlit

# === コンテナの実行設定 ===
# このコンテナが実行時にリッスン(待ち受け)するポート番号を8501として定義します。
# これはあくまで「このポートを使います」というドキュメント的な意味合いが強いです。
# 実際に外部からアクセスできるようにするには、ACIの "--ports" オプションや、
# Kubernetesの "Service" リソースでポートを公開(マッピング)する設定が別途必要になります。
EXPOSE 8501

# このイメージからコンテナを起動したときに、デフォルトで実行されるコマンドを指定します。
# ここでは、コンテナ内にコピーした "start.cmd" スクリプトを実行するように設定しています。
# 複雑な起動ロジック(バックグラウンド実行や無限ループなど)はスクリプトにまとめることで、
# Dockerfile自体をシンプルに保ち、管理しやすくなります。
CMD ["C:\\app\\start.cmd"]
コマンドプロンプトの解説 (ここをクリック)
start.cmd
@echo off
:: このコマンドは、以降に実行されるコマンド自体をコンソールに表示しないようにするおまじないです。
:: これにより、ログ出力がすっきりと見やすくなります。

:: === メインループの開始 ===
:: この ":main_loop" は、スクリプト内の移動先を示す「ラベル」です。
:: 後の "goto main_loop" コマンドで、処理をここに戻すために使います。
:: これにより、Streamlitプロセスが何らかの理由で終了しても、自動的に再起動する無限ループが実現されます。
:main_loop

echo Starting Streamlit process in headless mode...

:: === Streamlitプロセスの起動 ===
:: "python -m streamlit run ..." コマンドで、Streamlitアプリケーションを起動します。
::
:: 各オプションの解説:
::   --server.port=8501
::     Streamlitがリクエストを待ち受けるポート番号を指定します。DockerfileのEXPOSE命令と一致させています。
::
::   --server.address=0.0.0.0
::     コンテナの外部(この場合はACIのパブリックIPアドレス)からの接続を受け付けるために必須の設定です。
::     デフォルトではローカルホストからしかアクセスできません。
::
::   --server.enableCORS=false と --server.enableXsrfProtection=false
::     クロスオリジンに関するセキュリティ機能を無効化します。ACIのようなクラウド環境からアクセスする際に発生した
::     ERR_CONNECTION_RESET エラーを回避するために追加しました。
::
::   --server.headless=true
::     【最重要】Streamlitを「ヘッドレスモード」で起動します。
::     これにより、初回起動時にメールアドレスの登録を求めるような、対話的なプロンプトが完全に無効化されます。
::     ACIのような非対話的なコンテナ環境では、プログラムがユーザーからの入力を待つと即座にクラッシュするため、
::     この設定がコンテナを安定稼働させるための鍵となります。
python -m streamlit run app.py --server.port=8501 --server.address=0.0.0.0 --server.enableCORS=false --server.enableXsrfProtection=false --server.headless=true

echo.
echo Streamlit process exited at %TIME%. This should not happen. Restarting in 10 seconds...
echo.

:: === 待機処理とループ ===
:: もし上記の python コマンドが何らかの予期せぬ理由で終了した場合、この行以降が実行されます。
::
:: "ping" コマンドを使い、10秒間スクリプトの実行を一時停止します。
:: 以前の試行で "timeout" コマンドがコンテナログにエラーを出力したため、
:: より安定して動作するpingを使った待機方法に変更しています。
::   127.0.0.1: 自分自身のアドレス(localhost)
::   -n 11: pingを11回送信する(Windowsでは1回あたり約1秒なので、合計で約10秒の待機時間になります)
::   > nul: pingの実行結果をコンソールに表示しないようにします。
ping 127.0.0.1 -n 11 > nul

:: "goto" コマンドで、処理をスクリプトの先頭にある ":main_loop" ラベルの位置まで戻します。
:: これにより、Streamlitプロセスが再起動されます。
goto main_loop

ビルドしたイメージは、DockerHub 上で公開していますので、必要に応じてご利用ください。

irongeneral21/streamlit-on-windows:1

Streamlit アプリを windows コンテナーとして ACI にデプロイする

AKS にデプロイする前に、まず ACI で動作確認します。
この操作は任意です。

ACI が ACR にアクセスするためのマネージド ID を作成する。

az identity create --resource-group "リソースグループの名称" --name "マネージド ID の名称"

今後の手順のために、以下 3 点を確認します。
・ACR のリソース ID
・マネージド ID のリソース ID
・マネージド ID のオブジェクト ID

az acr show --resource-group "リソースグループの名称" --name "ACR の名称" --query id --output tsv
az identity show --resource-group "リソースグループの名称" --name "マネージド ID の名称" --query id --output tsv
az identity show --resource-group "リソースグループの名称" --name "マネージド ID の名称" --query principalId --output tsv

マネージド ID に ACR からイメージをプルするための権限 (ACRPull) ロールを割り当てる。

az role assignment create --assignee "マネージド ID のオブジェクト ID" --scope "ACR のリソース ID" --role acrpull

ACR 上のイメージを使って、ACI をデプロイします。

az container create --name "ACI の名称" --resource-group "リソースグループの名称" --image "使うイメージ" --acr-identity "マネージド ID のリソースID" --assign-identity "マネージド ID のリソースID" --ports 8501 --os-type Windows --cpu 1 --memory 1 --dns-name-label streamlit-dns-label

ACI の料金は以下の公開情報からご確認ください。
https://azure.microsoft.com/ja-jp/pricing/details/container-instances/

Streamlit アプリを windows コンテナーとして AKS にデプロイする

AKS が ACR からイメージをプルできるように、AKS と ACR を統合する。

az aks update -n "AKSクラスタ名" -g "リソースグループ名" --attach-acr "ACR名"

ベースイメージが windows 2022 のため、AKS で Node OS が Windows 2022 のノードプールを追加する。

az aks nodepool add \
    --resource-group "リソースグループ名" \
    --cluster-name "AKSクラスタ名" \
    --name "winnp" \
    --node-count 1 \
    --os-type Windows \
    --os-sku Windows2022

その後、ビルドしたイメージを使う Deployment と、Pod を外部に公開するための、Type LoadBalancer の Service を作成する。

apiVersion: apps/v1
kind: Deployment
metadata:
  # デプロイメントの名前
  name: streamlit-windows-deployment
spec:
  # 同じコンテナを何個起動するか(レプリカ数)
  replicas: 1
  selector:
    matchLabels:
      # このラベルを持つPodを管理対象とする
      app: streamlit-windows
  template:
    metadata:
      # 作成されるPodに付けるラベル
      labels:
        app: streamlit-windows
    spec:
      # このPodをWindowsノードにスケジュールするための設定
      nodeSelector:
        "kubernetes.io/os": windows
      containers:
        - name: streamlit-container
          # ご自身でビルドしたACR上のイメージを指定してください
          # 例: "myacrname.azurecr.io/my-streamlit-app:latest"
          image: "【あなたのACR名】.azurecr.io/【ビルドしたイメージ名】:【タグ】"
          resources:
            requests:
              memory: "1Gi"
              cpu: "1"
            limits:
              memory: "1Gi"
              cpu: "1"
          ports:
            # コンテナがリッスンしているポート
            - containerPort: 8501
---
apiVersion: v1
kind: Service
metadata:
  # サービスの名前
  name: streamlit-loadbalancer-service
spec:
  # Azureのロードバランサーをプロビジョニングし、パブリックIPを割り当てる
  type: LoadBalancer
  ports:
    - protocol: TCP
      # ロードバランサーが外部に公開するポート (80はHTTPの標準ポート)
      port: 80
      # コンテナ内の転送先ポート (Streamlitが待っているポート)
      targetPort: 8501
  selector:
    # このラベルを持つPodにトラフィックを転送する
    # 上記Deploymentのラベルと一致させる
    app: streamlit-windows

なお、Pod の起動には 5 分程度時間を要します。
Pod が起動したかどうかについては、以下のコマンドよりご確認ください。

kubectl get pod -o wide -w

Pod の Running 状態が確認できましたら、Type LoadBalancer の Service の外部 IP アドレスを確認して、ブラウザから疎通確認します。

kubectl get svc -o wide

ドキュメント

https://learn.microsoft.com/ja-jp/cli/azure/run-azure-cli-docker?view=azure-cli-latest
https://learn.microsoft.com/ja-jp/azure/container-registry/container-registry-tutorial-quick-task
https://learn.microsoft.com/ja-jp/cli/azure/acr?view=azure-cli-latest#az-acr-build

まとめ

本記事では、Streamlitで作成したPythonアプリケーションをWindowsコンテナ化し、Azure Kubernetes Service (AKS) 上で公開するまでの一連のプロセスを解説しました。

Mac環境では直接ビルドできないという課題を Azure Container Registry (ACR) のACR Task機能で解決し、Azure Container Instances (ACI) での動作確認を経て、最終的にAKSにデプロイする流れを追いました。

今回の構成における重要なポイントは以下の3点です。

  1. ACR Taskの活用: --platform windows を指定することで、ローカルのOSに依存せずクラウド上でWindowsコンテナイメージをビルドしました。
  2. コンテナを安定稼働させる起動スクリプト: 非対話環境でのクラッシュを避けるため、--server.headless=true オプションを付けたStreamlitを、無限ループ処理を施したstart.cmdで実行しました。
  3. AKSとACRの連携: プライベートなコンテナレジストリから安全にイメージをプルするため、az aks update --attach-acr コマンドでAKSにACRへのアクセス権を付与しました。

おわりに

もし、少しでもこの記事がお役に立てましたら、ぜひ "いいね" をお願いします。
また、今後も、AKS や ACI 関連の記事を書いていきますので、Zenn と X のフォローをどうぞよろしくお願いいたします。

Discussion