導入:その「手軽さ」は、本番環境でも通用するか
Streamlitは、Pythonスクリプト数行でリッチなUIを持つAIアプリケーションを構築できるため、PoC(概念実証)フェーズにおいて非常に有効です。「まず動くものを作る」というプロトタイプ思考を体現する素晴らしいツールと言えるでしょう。そして、その勢いのままAWS App Runnerのようなフルマネージドなコンテナサービスへデプロイし、「本番環境完成」と考える方もいるかもしれません。
しかし、「動くこと」と「運用できること」の間には、考慮すべき点があります。
特にStreamlitとApp Runnerの組み合わせは、一見すると「サーバーレスで簡単なデプロイ」という理想的な構成に見えますが、アーキテクチャ上の相性において注意が必要です。ローカル環境では顕在化しない「ステート(状態)管理」と「WebSocket接続」の問題が、スケールアウトした際に課題となることがあります。
この記事では、多くの記事が触れない「不都合な真実」に焦点を当てます。なぜ画面をリロードするとデータが消えるのか、なぜ想定以上のコストがかかるのか、そしてプロジェクトを成功させるためにどのようなアーキテクチャ判断を下すべきか。技術の本質を見抜き、ビジネスへの最短距離を描くための現実解を共有します。
1. 幻想と現実:Streamlit × App Runnerの「手軽さ」の裏側
「Dockerfileを書いて、App Runnerにプッシュすれば終わり」という説明をよく耳にします。確かに、デモやごく小規模なツールであれば、その認識で問題ないかもしれません。しかし、エンタープライズレベルのAIアプリケーション、特に複雑なコンテキストを扱うシステムにおいては、この認識が大きな落とし穴となります。
なぜPoCアプリはそのまま本番で動かないのか
根本的な原因は、StreamlitとApp Runnerが前提としている設計思想の決定的な「ズレ」にあります。システムアーキテクチャの視点から見ると、両者は対極にあると言っても過言ではありません。
Streamlitは、本質的にステートフル(Stateful)なフレームワークです。ユーザーがスライダーを操作したり、チャットボットにプロンプトを入力したりするたびに、Pythonスクリプト全体が再実行されます。この際、会話履歴や変数の値を保持するために st.session_state を利用しますが、このデータはデフォルトでサーバー(コンテナ)の揮発性メモリ内に保存されます。
一方、AWS App Runnerをはじめとするモダンなコンテナ実行環境は、ステートレス(Stateless)であることを前提に設計されています。これはTwelve-Factor Appの原則にも通じるもので、リクエストはどのコンテナインスタンスで処理されても同じ結果になるべきであり、特定のインスタンス内部の状態に依存してはいけない、という考え方です。最新のクラウドネイティブ環境においても、この「コンテナは使い捨て(Ephemeral)」という原則は変わりません。
App Runnerが「銀の弾丸」ではない理由
ローカル開発環境(単一プロセス、単一ユーザー)では、この矛盾は表面化しません。しかし、App Runner上でオートスケーリングが発動し、複数のインスタンスが立ち上がった瞬間、アーキテクチャ上の矛盾が顕在化します。
例えば、以下のようなシナリオを想像してみてください。
- ユーザーAがリクエストを送信し、インスタンスXが処理を担当。
session_stateにデータが保存される。 - ユーザーAが次の操作を行う。ロードバランサーが負荷分散のため、リクエストをインスタンスYに振り分ける。
- インスタンスYのメモリには、ユーザーAのコンテキスト(
session_state)は存在しない。 - 結果として、ユーザーは突然「初期画面に戻される」か、予期せぬ「KeyError」を目撃することになる。
これはApp Runnerの不具合ではなく、スケーラブルなWebアプリケーション基盤としての仕様です。App Runnerはデプロイとスケーリングを劇的に簡素化しますが、Streamlitのような特殊なメモリ管理を行うフレームワークのために、自動的にセッション共有の仕組みを提供するわけではありません。この「アーキテクチャの不一致」を理解せずに本番環境へデプロイすることは、ユーザー体験を損なう重大なリスク要因となります。
2. 技術リスク評価:WebSocketとセッション維持の壁
さらに技術的な側面として、Streamlitの通信基盤であるWebSocketと、App Runnerのネットワーク仕様の衝突について分析します。
Sticky Session未対応によるユーザー体験の分断
Webアプリケーションでセッションを維持する手法に「Sticky Session(セッションアフィニティ)」があります。これは、特定のユーザーからのリクエストを、常に同じサーバーインスタンスにルーティングする機能です。
残念ながら、現在のAWS App Runnerは、Sticky Sessionをネイティブでサポートしていません。
App Runnerの前段にあるロードバランサーは、リクエストを配下のインスタンスに分散させます。通常のREST APIであればこれは望ましい挙動ですが、メモリ内に状態を持つStreamlitにとっては課題となることがあります。ユーザーがAIモデルと対話している最中に、ネットワークの瞬断などでWebSocketの再接続が発生した場合、接続先が変わってしまうリスクがあるのです。
Session State消失問題:メモリ内データの揮発性
具体的にどのようなシナリオで問題が発生するか、最新のAIアプリケーショントレンドを交えて見てみましょう。
高度化するRAG(検索拡張生成)アプリの例:
従来の単純なテキスト検索に加え、ナレッジグラフを活用したGraphRAGや、画像や図表を含むマルチモーダルRAGなど、RAGシステムは急速に進化しています。これに伴い、処理に必要なコンテキスト情報やインデックスデータは肥大化・複雑化しています。
これらをst.session_state(メモリ上)のみに依存して管理する設計は、スケーラビリティの観点から推奨されません。ユーザーが複雑なクエリを投げた瞬間、App Runnerがスケールアウトやリバランスを行い、リクエストが別のインスタンスに飛ぶとします。新しいインスタンスには構築済みのグラフ構造やベクトルデータが存在しないため、「データが見つかりません」というエラーになり、処理が中断されます。
解決策: インメモリ管理からの脱却が必要です。PineconeやWeaviateなどの外部ベクトルデータベース、あるいはDynamoDBやS3を利用して状態を外部化し、ステートレスな構成へ移行することが求められます。長い推論時間の問題:
App Runnerのリクエストタイムアウトは設定可能ですが、WebSocket接続の維持に関しては不安定な要素があります。特に、推論能力が向上した最新のLLMモデルや、Chain-of-Thought(思考の連鎖)を用いるエージェント型ワークフローでは、回答生成に時間がかかる傾向があります。その間にブラウザとサーバー間のハートビートが途切れると、Streamlitは「接続断」と判断します。画面上では "Please wait..." のままフリーズするか、"Connection lost" のトーストが表示され、リロードを促されることがあります。リロードすれば、セッションはリセットされます。
WebSocket接続断と再接続の挙動
Streamlitは、フロントエンド(Reactアプリ)とバックエンド(Pythonプロセス)をWebSocketで常時接続しています。この接続が重要です。App Runner環境下では、デプロイや設定変更時にコンテナが入れ替わりますが、この際、既存のWebSocket接続は切断されます。
ユーザーから見ると、作業中に突然アプリが反応しなくなることを意味します。商用サービスとして提供する場合、このリスクは考慮すべき点です。
3. コストとパフォーマンスのリスク:オートスケーリングの落とし穴
技術的な制約だけでなく、ビジネスインパクトに直結するコスト面のリスクも考慮する必要があります。経営者視点で見ると、AIアプリ特有のリソース消費パターンは、App Runnerの課金モデルと相性が良いとは限りません。
同時実行数(Concurrency)設定の難しさ
App Runnerのオートスケーリングは、主に「同時実行数(Concurrency)」をトリガーにします。これは「1つのインスタンスが同時に処理できるリクエスト数」です。
一般的なWeb APIなら、1インスタンスで複数のリクエストを処理することも可能です。しかし、Streamlitアプリ、特にAI推論を行うアプリはどうでしょうか?
- PythonのGIL(Global Interpreter Lock):Pythonは基本的にシングルスレッド性能に依存します。
- メモリ消費:LLMやDataFrameを扱う場合、1リクエストあたりのメモリ消費量は大きくなることがあります。
もし「同時実行数」をデフォルトに近い設定にしてしまうと、数人のユーザーが同時に重い推論を実行した瞬間、インスタンスのメモリが溢れ(OOM Kill)、そのインスタンスに接続している他のユーザーも影響を受ける可能性があります。
現実的には、AIアプリの場合、同時実行数を絞る必要があるかもしれません。その場合、ユーザーが増えるたびに新しいインスタンスが立ち上がることになります。
プロビジョニング済みインスタンスと待機コスト
App Runnerは「リクエストがない時はゼロ円」になるスケール・トゥ・ゼロを(完全な形では)サポートしていません。コールドスタートを防ぐため、最低でも「プロビジョニング済みインスタンス」を1つ維持する必要があります。
AIアプリは起動(モデルのロードなど)に時間がかかることがあります。コールドスタートが発生すると、最初のユーザーは待たされることになります。これを避けるために常にインスタンスを上げておく必要があり、結果としてコストが増加する可能性があります。
コールドスタートがAI UXに与える影響
通常のWebアプリであれば、数秒の遅延は許容されるかもしれません。しかし、対話型AIにおいて「反応がない」時間はユーザーの離脱に繋がる可能性があります。
App Runnerでスケールアウトが発生した際、新しいインスタンスが立ち上がり、ライブラリがロードされ、重いAIモデルがメモリに展開されるまで、リクエストは待機状態になります。この間、フロントエンドには何も表示されません。
4. 緩和策とアーキテクチャ:リスクを許容範囲に抑える構成案
App Runnerが全く使えないわけではありません。適切な「緩和策」を講じることで、リスクをコントロールし、運用可能なレベルに引き上げることが可能です。アジャイルかつスピーディーに解決策を実装していきましょう。
外部データストア(ElastiCache/DynamoDB)によるステート外出し
最も効果的な対策は、「Streamlitのメモリ(session_state)を過信しない」ことです。ステートレスなアーキテクチャへのリファクタリングを行います。
- Redis / Valkey (Amazon ElastiCache): セッション情報の保存先として有効です。Amazon ElastiCacheでは、従来のRedis OSSに加え、ライセンス変更(SSPL等)の影響を受けないオープンソースのValkeyもサポートされています。AWS環境においては、長期的な互換性とコスト効率を考慮し、Valkeyを選択するケースも増えています。どちらのエンジンを選択する場合でも、ユーザーの会話履歴や一時的なデータを外部KVSに保存し、Streamlit側では「セッションID」のみをCookie等で管理する設計は共通です。これにより、リクエストがどのインスタンスに振り分けられても継続性を保てます。
- Amazon S3 / DynamoDB: アップロードされたファイルや、永続化が必要なログデータは、外部ストレージに保存します。コンテナのローカルディスクへの保存は一時的なものであり、再起動でデータが消失するため避けるべきです。
実装アプローチとしては、streamlit-session などのサードパーティライブラリを活用するか、独自にRedis/Valkeyへの読み書きラッパーを実装することで、インスタンス非依存の状態管理を実現できます。
カスタムドメインとWAFによるセキュリティ補完
App RunnerはデフォルトでHTTPSエンドポイントを提供してくれますが、商用利用ならカスタムドメインの設定は推奨されます。さらに、AWS WAF をApp Runnerの前段に配置することも可能です。
AIアプリはBotによる攻撃やプロンプトインジェクションの標的になりやすいため、WAFによるレートリミット(過剰なリクエストの遮断)やIP制限を設定し、セキュリティリスクを防ぐことが重要です。
ヘルスチェック設定の最適化
デフォルトのヘルスチェック設定は、AIアプリには厳しすぎることがあります。モデルのロードに時間がかかる場合、App Runnerが「起動失敗」と判断してコンテナを停止させてしまうことがあります。
- Health Check Interval: 間隔を広げる。
- Timeout: 許容時間を延ばす。
- Healthy Threshold: 成功判定の回数を調整する。
apprunner.yaml 設定ファイルでこれらの値をチューニングし、安定して起動完了とみなされるよう調整が必要です。
5. 最終判断:ECS/Fargateへ移行すべき境界線
最後に、アーキテクトとして決断を下すための基準を提示します。「App Runnerで対応すべきか、ECS on Fargateへ移行すべきか」。ビジネスへの最短距離を描くための判断基準について説明します。
App Runnerで粘るべきか、ECSへ移行すべきか
以下のいずれかの条件に当てはまる場合、App Runnerの使用は推奨されない可能性があります。ECS on Fargate、あるいはEC2への移行を検討してください。
GPUが必要な場合:
App Runnerは依然としてCPUベースのワークロードに最適化されており、GPUインスタンスをネイティブにサポートしていません。ローカルLLMや画像生成モデルを高速に動かす必要があるなら、GPUが利用可能なECS、あるいはSageMaker AI(旧Amazon SageMaker)への移行が必要です。
特に最新のSageMaker AIでは、JumpStartを利用して最新の基盤モデルを迅速にデプロイできるほか、HyperPod機能による計算リソースの効率化が進んでいます。Streamlitアプリ自体は軽量なコンテナとしてApp Runnerに残しつつ、重い推論処理のみをSageMaker AIのエンドポイントにオフロードする構成も、システム思考に基づいたモダンなアーキテクチャとして推奨されます。厳密なSticky Sessionが必要な場合:
Redisによるステート外出し等の改修コストが高すぎる、あるいはライブラリの制約でメモリ内ステートが必須の場合、Application Load Balancer (ALB) でSticky Sessionを有効化できるECS構成の方が安全な場合があります。WebSocketの高度な制御が必要な場合:
双方向通信のタイムアウト値やバッファサイズなど、ロードバランサーレベルでの細かいチューニングが必要な場合、App Runnerのロードバランサーは制約となることがあります。コスト効率が逆転する規模感:
常時稼働するインスタンス数が多く、かつリクエストの波が予測可能な場合、Reserved InstancesやSavings Plansを適用したFargate/EC2の方が、トータルコストを下げられる可能性があります。
意思決定のためのチェックリスト
- アプリはステートレス(DB/Redis依存)に改修可能か? → 難しい場合はECS
- GPU推論が必要か? → 必要な場合はECSまたはSageMaker AI
- 許容できるコールドスタート時間は? → 短い場合は常時起動必須(コスト増)
- チームにインフラ専任者はいるか? → いない場合はApp Runnerのメリットが大きい
まとめ:ツールを使いこなす
AWS App Runnerは便利なサービスですが、Streamlitアプリにとって万能ではありません。ステート管理の課題とコスト構造を理解せずに本番投入すれば、問題が発生する可能性があります。
しかし、リスクを評価し、Redis等でアーキテクチャを補強できるのであれば、インフラ管理の手間を減らすことができる選択肢となります。
重要なのは、「サーバーレスだから簡単」という思考に陥らないこと。アプリケーションの特性(ステートフルか、メモリ消費が多いか)を見極め、適切なコンピュートサービスを選定することが重要です。
皆さんのAIプロジェクトが、PoCの段階から、堅牢な本番サービスとしてユーザーに価値を届けることを願っています。アーキテクチャの判断に迷ったら、まずは「ステートをどこに置くか」から検討してみてください。皆さんはどのようなアーキテクチャを選択しますか?ぜひ、実践の中で最適な解を見つけていってください。
コメント