Ollama: 本番デプロイ
本番デプロイはローカルAIのラボから戦場への最後の跳躍——高可用、スケーラブル、災害耐性。
💡 ヒント: Nginxリバースプロキシ + ロードバランシングが本番デプロイの標準アーキテクチャ——NginxがSSL終端、API Key認証、レート制限、トラフィック分配を担当し、Ollamaノードは127.0.0.1にバインドするのみ。マルチノードクラスタでは
least_conn(最小接続数)戦略が推奨——推論リクエストの所要時間が大きく異なるため。
📋 前提条件: まず以下を習得していること
- レッスン15: Dockerコンテナデプロイ
- レッスン20: セキュリティ強化
1. 学べること
- 本番アーキテクチャ設計:ロードバランシング + マルチノードクラスタ
- 高可用性:ヘルスチェック + 自動フェイルオーバー
- スケーリング戦略:キューベースの水平スケーリング
- Kubernetesデプロイ:Helm ChartsとGPUスケジューリング
- 災害復旧とロールバック:モデルバージョン管理と設定のコード化
2. SaaS起業家のリアルな事例
💡 ヒント: 本番環境ではNginxロードバランシングに
least_conn(最小接続数)戦略を使用すべき。デフォルトのround_robinではない。Ollama推論リクエストの所要時間は大きく異なる(短いQ&Aは1秒、長文は30秒)ため、least_connの方が負荷を均等に分配する。
ℹ️ 情報: Ollamaはクラスタモードをネイティブにサポートしない(マスタースレーブ同期なし、分散推論なし)。マルチノードクラスタのモデル一貫性とセッションアフィニティは上位層(Nginx/FastAPI)で実装する必要がある。これはKubernetesデプロイにも当てはまる。
(1) ペインポイント:単一障害点がサービス全体を停止させる
AliceのSupportBotは単一Ollamaサーバーで稼働。サーバーメンテナンスやGPU OOMでサービスが中断し、カスタマーサービスシステムが2時間ダウン、500人以上の顧客に影響。
(2) ソリューション:マルチノード高可用クラスタ
3ノードOllamaクラスタ + Nginxロードバランサーをデプロイし、任意ノードがダウンしても自動フェイルオーバー:
flowchart TD
A[Nginxロードバランサー] --> B[Ollamaノード1<br/>GPU 1]
A --> C[Ollamaノード2<br/>GPU 2]
A --> D[Ollamaノード3<br/>GPU 3]
3. 本番アーキテクチャ設計
⚠️ 警告: マルチノードクラスタでは、各Ollamaノードが独立してモデルと状態を管理する——データベースのようなマスタースレーブレプリケーションはない。ノードAでプルしたモデルは自動的にノードBに同期されない。統一初期化スクリプトまたは共有ストレージで全ノードのモデル一貫性を確保すること。
(1) 単一ノード vs マルチノード
| 次元 | 単一ノード | マルチノードクラスタ |
|---|---|---|
| 可用性 | 単一障害点 | N-1フォールトトレランス |
| スループット | 制限あり | リニアスケーリング |
| コスト | 低 | 高(3倍以上) |
| 運用複雑さ | 低 | 中 |
| ユースケース | 開発/小規模 | 本番 |
(2) 本番アーキテクチャ概要
flowchart TD
A[インターネット] --> B[WAF / CDN]
B --> C[Nginx<br/>SSL + 認証 + LB]
C --> D[Ollamaノード1<br/>GPU + モデル]
C --> E[Ollamaノード2<br/>GPU + モデル]
C --> F[Ollamaノード3<br/>GPU + モデル]
D --> G[NFS / S3<br/>共有モデルストレージ]
E --> G
F --> G
D --> H[Chromaクラスタ]
E --> H
F --> H
I[Prometheus] --> D
I --> E
I --> F
I --> J[Grafanaダッシュボード]
(3) コンポーネント一覧
| コンポーネント | 数 | スペック | 用途 |
|---|---|---|---|
| Nginx LB | 1 | 2 vCPU、4GB RAM | ロードバランシング + SSL |
| Ollamaノード | 3 | 8 vCPU、16GB RAM、RTX 4090×1 | モデル推論 |
| Chroma | 1 | 4 vCPU、8GB RAM、SSD | ベクトル保存 |
| NFS/S3 | 1 | 500GB以上 | モデル共有ストレージ |
| Prometheus | 1 | 2 vCPU、4GB RAM | モニタリング |
4. 高可用性
⚠️ 注: 本番ではSSL証明書設定が必須——HTTPSは通信を暗号化するだけでなく、API Key認証の前提条件でもある(HTTP平文でAPI Keyを送信するのはセキュリティ上全く意味がない)。無料のLet's Encrypt証明書はcertbotで取得、またはCaddyで自動HTTPSを使用。
(1) ヘルスチェックとフェイルオーバー
| メカニズム | チェック方法 | 間隔 | タイムアウト |
|---|---|---|---|
| Nginxパッシブチェック | リクエスト失敗時に利用不可とマーク | 各リクエスト | 5秒 |
| アクティブヘルスチェック | 定期的にGET /api/tags | 10秒 | 3秒 |
| アプリケーション層チェック | カスタム推論テスト | 30秒 | 10秒 |
(2) ▶ サンプル:Nginxロードバランサー設定
NGINX
# /etc/nginx/conf.d/ollama-lb.conf
upstream ollama_cluster {
least_conn; # Route to least busy node
server ollama-node1:11434 max_fails=3 fail_timeout=30s;
server ollama-node2:11434 max_fails=3 fail_timeout=30s;
server ollama-node3:11434 max_fails=3 fail_timeout=30s;
}
server {
listen 443 ssl;
server_name ai.example.com;
ssl_certificate /etc/ssl/certs/ai.example.com.crt;
ssl_certificate_key /etc/ssl/private/ai.example.com.key;
location /v1/ {
# API Key authentication
if ($http_x_api_key != "your-secret-key") {
return 401 '{"error": "Unauthorized"}';
}
proxy_pass http://ollama_cluster;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 120s;
# Rate limiting
limit_req zone=api burst=20 nodelay;
}
# Health check endpoint
location /health {
proxy_pass http://ollama_cluster/api/tags;
}
}
出力:
TEXT
// Execution successful
(3) ▶ サンプル:自動フェイルオーバースクリプト
BASH
#!/bin/bash
# Ollama cluster health monitor
NODES=("ollama-node1:11434" "ollama-node2:11434" "ollama-node3:11434")
HEALTH_URL="/api/tags"
ALERT_EMAIL="ops@example.com"
check_node() {
local node=$1
if curl -sf --connect-timeout 3 "http://$node$HEALTH_URL" > /dev/null; then
echo "UP"
else
echo "DOWN"
fi
}
while true; do
for node in "${NODES[@]}"; do
status=$(check_node "$node")
if [ "$status" = "DOWN" ]; then
echo "ALERT: $node is DOWN at $(date)" >> /var/log/ollama_health.log
# Send alert
echo "Ollama node $node is DOWN" | mail -s "OLLAMA ALERT" "$ALERT_EMAIL" 2>/dev/null
fi
done
sleep 10
done
出力:
TEXT
NAME ID SIZE
llama3.2:latest a80... 2.0 GB
mistral:latest 61... 4.1 GB
5. スケーリング戦略
(1) スケーリングトリガー条件
| 指標 | スケールアップ閾値 | スケールダウン閾値 | 待機時間 |
|---|---|---|---|
| リクエストレイテンシP95 | > 5秒 | < 2秒 | 5分 |
| 同時接続数 | 容量の80%以上 | 容量の30%以下 | 5分 |
| GPU使用率 | > 85% | < 40% | 10分 |
| キュー長 | > 10 | = 0 | 3分 |
(2) スケーリング方法比較
| 方法 | 速度 | コスト | 複雑さ |
|---|---|---|---|
| 手動スケーリング | 遅い(時間) | 制御可能 | 低 |
| 自動スクリプト | 中程度(分) | 制御可能 | 中 |
| K8s HPA | 高速(秒) | 自動 | 高 |
(3) ▶ サンプル:レイテンシベース自動スケーリング
PYTHON
import subprocess
import time
from typing import Optional
class AutoScaler:
def __init__(self, scale_up_threshold: float = 5.0,
scale_down_threshold: float = 2.0,
cooldown: int = 300):
self.scale_up_threshold = scale_up_threshold
self.scale_down_threshold = scale_down_threshold
self.cooldown = cooldown
self.last_scale_time = 0
def get_avg_latency(self) -> Optional[float]:
try:
result = subprocess.run(
["curl", "-sf", "http://localhost:11434/api/chat", "-d",
'{"model":"qwen2.5","messages":[{"role":"user","content":"hi"}],"stream":false}'],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
import json
data = json.loads(result.stdout)
duration_ns = data.get("total_duration", 0)
return duration_ns / 1e9
except Exception:
pass
return None
def check_and_scale(self):
latency = self.get_avg_latency()
if latency is None:
return
now = time.time()
if now - self.last_scale_time < self.cooldown:
return
if latency > self.scale_up_threshold:
print(f"High latency ({latency:.1f}s), scaling up...")
self._scale_up()
self.last_scale_time = now
elif latency < self.scale_down_threshold:
print(f"Low latency ({latency:.1f}s), scaling down...")
self._scale_down()
self.last_scale_time = now
def _scale_up(self):
# Add new Ollama node (e.g., via Docker or cloud API)
print("Adding Ollama node...")
def _scale_down(self):
# Remove idle Ollama node
print("Removing idle Ollama node...")
# Usage
# scaler = AutoScaler()
# while True:
# scaler.check_and_scale()
# time.sleep(60)
出力:
TEXT
Adding Ollama node...
Removing idle Ollama node...
6. Kubernetesデプロイ
(1) K8sデプロイアーキテクチャ
flowchart TD
A[Ingress<br/>nginx-ingress] --> B[Service<br/>ollama-svc]
B --> C[Deployment<br/>ollama-deploy<br/>3レプリカ]
C --> D[Pod 1<br/>GPU + モデル]
C --> E[Pod 2<br/>GPU + モデル]
C --> F[Pod 3<br/>GPU + モデル]
G[PVC<br/>model-storage] --> D
G --> E
G --> F
(2) GPUスケジューリング主要設定
| リソース | 設定 | 説明 |
|---|---|---|
| nvidia.com/gpu (requests) | リソースリクエスト | Podごとに1 GPU |
| nvidia.com/gpu (limits) | リソース制限 | 最大1 GPU |
| nodeSelector | GPUノード選択 | GPUノードを指定 |
| tolerations | GPUテイント許容 | GPUノードへのスケジューリングを許可 |
(3) ▶ サンプル:Ollama K8sデプロイ
YAML
# ollama-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ollama
labels:
app: ollama
spec:
replicas: 3
selector:
matchLabels:
app: ollama
template:
metadata:
labels:
app: ollama
spec:
nodeSelector:
gpu: "true"
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: ollama
image: ollama/ollama
ports:
- containerPort: 11434
resources:
requests:
nvidia.com/gpu: 1
memory: "8Gi"
limits:
nvidia.com/gpu: 1
memory: "16Gi"
env:
- name: OLLAMA_HOST
value: "0.0.0.0:11434"
- name: OLLAMA_NUM_PARALLEL
value: "4"
- name: OLLAMA_KEEP_ALIVE
value: "30m"
volumeMounts:
- name: model-storage
mountPath: /root/.ollama
livenessProbe:
httpGet:
path: /api/tags
port: 11434
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/tags
port: 11434
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: ollama-models-pvc
---
apiVersion: v1
kind: Service
metadata:
name: ollama-svc
spec:
selector:
app: ollama
ports:
- port: 11434
targetPort: 11434
type: ClusterIP
出力:
TEXT
K8s Deployment created successfully, Pods running normally
出力:
TEXT
HPA auto-scaling configured successfully, cluster resource monitoring enabled
(1) ▶ サンプル:K8s HPA自動スケーリング
YAML
# ollama-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ollama-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ollama
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: nvidia.com/gpu
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 1
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 300
出力:
TEXT
# HPA created successfully
kubectl get hpa ollama-hpa
# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
# ollama-hpa Deployment/ollama 45%/80% 2 6 3 5m
7. 災害復旧とロールバック
(1) 災害復旧戦略
| 戦略 | RTO | RPO | コスト |
|---|---|---|---|
| アクティブ/パッシブ | 5〜15分 | 0 | ハードウェア2倍 |
| アクティブ/アクティブ | 0(自動フェイルオーバー) | 0 | ハードウェア3倍 |
| コールドスタンバイ | 1〜4時間 | 損失あり | 1倍 + ストレージ |
(2) 設定のコード化
| リソース | バージョン管理方法 |
|---|---|
| Modelfile | Gitリポジトリ |
| Docker Compose | Gitリポジトリ |
| K8s Manifests | Git + ArgoCD |
| Nginx設定 | Gitリポジトリ |
| モデルバージョン | Ollamaタグ + ドキュメント |
8. 総合サンプル:本番デプロイチェックリスト
PYTHON
# ============================================
# Comprehensive: Production deployment checklist
# Full verification before going live
# ============================================
PRODUCTION_CHECKLIST = {
"infrastructure": [
("Ollama nodes: 3+ replicas running", "CRITICAL"),
("GPU drivers: installed and verified", "CRITICAL"),
("Model storage: shared NFS/S3 mounted", "CRITICAL"),
("Network: internal VLAN for Ollama traffic", "HIGH"),
("DNS: ai.example.com resolves to LB", "HIGH"),
],
"security": [
("Ollama bound to 127.0.0.1 on each node", "CRITICAL"),
("Nginx SSL certificate valid", "CRITICAL"),
("API Key authentication enabled", "CRITICAL"),
("Rate limiting configured (60 req/min)", "HIGH"),
("WAF rules in place", "MEDIUM"),
],
"high_availability": [
("Health checks configured (Nginx + app layer)", "CRITICAL"),
("Failover tested: kill 1 node, service continues", "CRITICAL"),
("Load balancer: least_conn algorithm", "HIGH"),
("Session affinity: disabled (stateless)", "HIGH"),
],
"monitoring": [
("Prometheus scraping all nodes", "HIGH"),
("Grafana dashboards created", "HIGH"),
("Alert rules: GPU OOM, high latency, service down", "CRITICAL"),
("Log aggregation: Loki or ELK", "MEDIUM"),
],
"disaster_recovery": [
("Config in Git (Modelfile, Compose, K8s)", "HIGH"),
("Model backup: GGUF files on S3/NFS", "HIGH"),
("Chroma data backup: daily snapshot", "HIGH"),
("Recovery drill: tested within last 30 days", "MEDIUM"),
],
"performance": [
("Baseline benchmark recorded", "HIGH"),
("OLLAMA_NUM_PARALLEL configured", "HIGH"),
("OLLAMA_KEEP_ALIVE=30m set", "MEDIUM"),
("num_ctx per endpoint optimized", "MEDIUM"),
]
}
def run_checklist() -> str:
report = ["# Production Deployment Checklist\n"]
total = 0
checked = 0
for category, items in PRODUCTION_CHECKLIST.items():
report.append(f"\n## {category.replace('_', ' ').title()}")
for item, priority in items:
total += 1
report.append(f"- [ ] [{priority}] {item}")
report.append(f"\n---\nTotal items: {total}")
return "\n".join(report)
print(run_checklist())
❓ よくある質問
Q 3ノードクラスタの最低GPU数は?
A 最低3(ノードごとに1)。予算が限られている場合、2ノード構成(1プライマリ + 1バックアップ)も可能だが、フェイルオーバー時のスループットは半減。
Q 各マシンでモデルファイルをプルする必要がある?
A 共有ストレージ(NFS)を使用する場合は1回のみ。ローカルストレージの場合は各マシンでプルが必要。NFS共有 + ローカルキャッシュが推奨。
Q KubernetesとDocker Composeはどちらを選ぶ?
A 5ノード未満はDocker Compose(シンプル)。5ノード以上または自動スケーリングが必要な場合はKubernetes。ほとんどのシナリオではDocker Composeで十分。
Q フェイルオーバーをテストするには?
A Ollamaノードを手動で停止(docker stopまたはsystemctl stop)、Nginxが自動的に他のノードにトラフィックをルーティングし、ユーザーへの影響がないことを確認。
Q Ollamaに組み込みクラスタモードはある?
A ない。Ollamaは単一インスタンスサービス;クラスタには外部ロードバランサーの組み合わせが必要。各Ollamaインスタンスは独立動作;Nginxがリクエスト分配を担当。
Q モデルバージョンはどう管理する?
A Modelfile + Gitで設定管理。モデルファイルはタグ(qwen2.5:v1.0)でバージョン識別。本番では固定タグを使用し、:latestは避ける。
📖 まとめ
- 本番アーキテクチャ:Nginx LB + 複数Ollamaノード + 共有ストレージ + Chroma
- 高可用性:Nginx least_conn + ヘルスチェック + 自動フェイルオーバー
- スケーリング:レイテンシ/GPU使用率でトリガー;Docker Compose手動またはK8s HPA自動
- Kubernetes:GPUスケジューリング + PVC永続化 + HPA自動スケーリング
- 災害復旧:設定のコード化(Git)+ モデルバックアップ(NFS/S3)+ 定期訓練
- 本番チェックリストは6領域をカバー:インフラ、セキュリティ、高可用性、モニタリング、災害復旧、パフォーマンス
📝 練習問題
- 基本(難易度 ⭐):単一マシンの本番デプロイ計画を設計——Nginx + 単一Ollama + Chroma。アーキテクチャ図を描き、Docker Compose設定を記述する。
- 中級(難易度 ⭐⭐):2ノードOllamaクラスタ + Nginxロードバランサーを構築し、手動フェイルオーバーをテストする。
- 上級(難易度 ⭐⭐⭐):完全な本番デプロイドキュメントを作成——アーキテクチャ図、Docker Compose/K8s設定、セキュリティ強化、モニタリング/アラート、災害復旧計画——し、フェイルオーバー訓練を完了する。