Ollama: 本番デプロイ

本番デプロイはローカルAIのラボから戦場への最後の跳躍——高可用、スケーラブル、災害耐性。

💡 ヒント: Nginxリバースプロキシ + ロードバランシングが本番デプロイの標準アーキテクチャ——NginxがSSL終端、API Key認証、レート制限、トラフィック分配を担当し、Ollamaノードは127.0.0.1にバインドするのみ。マルチノードクラスタではleast_conn(最小接続数)戦略が推奨——推論リクエストの所要時間が大きく異なるため。

📋 前提条件: まず以下を習得していること

1. 学べること


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ロードバランサーをデプロイし、任意ノードがダウンしても自動フェイルオーバー:

100%
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) 本番アーキテクチャ概要

100%
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デプロイアーキテクチャ

100%
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は避ける。

📖 まとめ


📝 練習問題

  1. 基本(難易度 ⭐):単一マシンの本番デプロイ計画を設計——Nginx + 単一Ollama + Chroma。アーキテクチャ図を描き、Docker Compose設定を記述する。
  2. 中級(難易度 ⭐⭐):2ノードOllamaクラスタ + Nginxロードバランサーを構築し、手動フェイルオーバーをテストする。
  3. 上級(難易度 ⭐⭐⭐):完全な本番デプロイドキュメントを作成——アーキテクチャ図、Docker Compose/K8s設定、セキュリティ強化、モニタリング/アラート、災害復旧計画——し、フェイルオーバー訓練を完了する。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%