Contenido

Ollama en Kubernetes: Despliega tu LLM local en un cluster real

Antes de empezar

En el post anterior construimos una app tipo ChatGPT que corre 100% en tu máquina con Ollama, FastAPI y streaming en tiempo real. Al final dejamos una pregunta abierta: ¿y si metemos esto en un cluster de Kubernetes?

Hoy vamos a responder esa pregunta. Vamos a desplegar Ollama como un servicio dentro de Kubernetes, conectar nuestra app de chat, configurar recursos, autoescalado y hablar de algo fundamental: la GPU.

Y al final, vamos a ser honestos sobre lo que falta para que esto sea “producción de verdad” — porque desplegar un LLM es solo la mitad del camino.


Arquitectura

Esto es lo que vamos a construir:

  • Ingress expone la app al exterior.
  • Chat App es nuestra FastAPI del post anterior, con HPA para escalar según demanda.
  • Ollama corre el modelo LLM, idealmente en un nodo con GPU.
  • PVC persiste los modelos descargados para que no se re-descarguen en cada reinicio.

Pre-requisitos

HerramientaVersiónPara qué
OrbStack1.9+Kubernetes local en macOS
kubectl1.28+Gestión del cluster
Helm3.xInstalación de charts (opcional)
Ollama local0.18+Para probar antes de desplegar
¿Por qué OrbStack?
Usamos OrbStack porque ofrece un cluster de Kubernetes integrado en macOS con soporte nativo de contenedores Linux, consumo bajo de recursos y startup casi instantáneo. Es perfecto para desarrollo local. Si prefieres otra opción, más adelante explico las alternativas.

Paso 1: Preparar el cluster local con OrbStack

OrbStack incluye un cluster de Kubernetes que puedes activar desde la app o por terminal:

# Verificar que Kubernetes esté activo en OrbStack
kubectl cluster-info

Deberías ver algo como:

Kubernetes control plane is running at https://127.0.0.1:26443

Si usas otra herramienta, estos son los equivalentes:

HerramientaComando para crear clusterPlataforma
OrbStackActivar en Settings → KubernetesmacOS
Rancher DesktopActivar en Preferences → KubernetesmacOS, Windows, Linux
minikubeminikube start --memory=8g --cpus=4macOS, Windows, Linux
kindkind create clustermacOS, Windows, Linux
Docker DesktopActivar en Settings → KubernetesmacOS, Windows
Memoria
Para correr un LLM como Llama 3.1 (8B) necesitas al menos 8 GB de RAM disponibles para el cluster. Modelos más grandes como 70B necesitan mucha más. Asegúrate de asignar suficiente memoria a tu herramienta de Kubernetes local.

Paso 2: Crear el namespace y el PVC

Primero, un namespace dedicado y un volumen persistente para los modelos:

# ollama-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: llm
  labels:
    app: ollama
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ollama-models
  namespace: llm
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
kubectl apply -f ollama-namespace.yaml

¿Por qué un PVC? Porque los modelos de Ollama pesan varios GB. Sin persistencia, cada vez que el pod se reinicie, tendría que descargar el modelo de nuevo. Con el PVC, el modelo se descarga una vez y queda guardado.


Paso 3: Desplegar Ollama

Ahora el deployment de Ollama:

# ollama-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ollama
  namespace: llm
  labels:
    app: ollama
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ollama
  template:
    metadata:
      labels:
        app: ollama
    spec:
      containers:
        - name: ollama
          image: ollama/ollama:latest
          ports:
            - containerPort: 11434
          resources:
            requests:
              cpu: "2"
              memory: 4Gi
            limits:
              cpu: "4"
              memory: 8Gi
          volumeMounts:
            - name: models
              mountPath: /root/.ollama
          readinessProbe:
            httpGet:
              path: /
              port: 11434
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /
              port: 11434
            initialDelaySeconds: 30
            periodSeconds: 10
      volumes:
        - name: models
          persistentVolumeClaim:
            claimName: ollama-models
---
apiVersion: v1
kind: Service
metadata:
  name: ollama
  namespace: llm
spec:
  selector:
    app: ollama
  ports:
    - port: 11434
      targetPort: 11434
  type: ClusterIP
kubectl apply -f ollama-deployment.yaml

Sobre los recursos: ¿por qué estos valores?

Aquí vale la pena detenernos y hablar de los números:

RecursoRequestLimitPor qué
CPU2 cores4 coresLa inferencia de un LLM es CPU-intensiva. 2 cores es el mínimo para que no se arrastre; 4 le da espacio para picos
Memoria4 Gi8 GiLlama 3.1 (8B) necesita ~4.7 GB solo para cargar el modelo en memoria. El request garantiza que el scheduler le asigne un nodo con suficiente RAM
Requests vs Limits
  • Request = lo mínimo que el pod necesita para funcionar. El scheduler usa esto para decidir en qué nodo colocar el pod.
  • Limit = el máximo que puede consumir. Si lo supera, Kubernetes lo mata (OOMKilled).

Para Ollama, el request de memoria es crítico: si es muy bajo, el scheduler puede colocar el pod en un nodo sin suficiente RAM y el modelo no carga. Si el limit es muy bajo, el pod muere en cuanto empiece a procesar un prompt largo.

Tabla de referencia por modelo

ModeloParámetrosRAM mínimaCPU mínimaGPU recomendada
Llama 3.18B4.7 GB2 cores6 GB VRAM
Llama 3.170B40 GB8 cores48 GB VRAM
Mistral7B4.1 GB2 cores6 GB VRAM
Phi-414B8.5 GB2 cores10 GB VRAM
Gemma 312B7.5 GB2 cores10 GB VRAM

Paso 4: Descargar el modelo

Una vez que Ollama esté corriendo, descarga el modelo:

# Ejecutar dentro del pod de Ollama
kubectl exec -it -n llm deploy/ollama -- ollama pull llama3.1

Esto descarga el modelo al PVC. La próxima vez que el pod se reinicie, el modelo ya estará ahí.

Verifica que funcione:

kubectl exec -it -n llm deploy/ollama -- ollama run llama3.1 "Hola, ¿estás corriendo en Kubernetes?"

Paso 5: Desplegar la Chat App

Ahora desplegamos la app de chat del post anterior:

# chat-app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: chat-app
  namespace: llm
  labels:
    app: chat-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: chat-app
  template:
    metadata:
      labels:
        app: chat-app
    spec:
      containers:
        - name: chat-app
          image: ghcr.io/your-org/ollama-chat-python:latest
          ports:
            - containerPort: 8000
          env:
            - name: OLLAMA_HOST
              value: "http://ollama.llm.svc.cluster.local:11434"
          resources:
            requests:
              cpu: 200m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 5
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: chat-app
  namespace: llm
spec:
  selector:
    app: chat-app
  ports:
    - port: 80
      targetPort: 8000
  type: ClusterIP

Fíjate en la variable de entorno OLLAMA_HOST: apunta al Service de Ollama usando el DNS interno de Kubernetes (ollama.llm.svc.cluster.local). La app no necesita saber dónde corre Ollama físicamente.

kubectl apply -f chat-app-deployment.yaml

Paso 6: Configurar el HPA

La app de chat puede escalar horizontalmente. Ollama no — un modelo cargado en memoria no se puede “partir” entre pods fácilmente. Pero la app sí:

# chat-app-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: chat-app-hpa
  namespace: llm
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: chat-app
  minReplicas: 2
  maxReplicas: 5
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Pods
          value: 1
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 120
      policies:
        - type: Pods
          value: 1
          periodSeconds: 120
kubectl apply -f chat-app-hpa.yaml

¿Por qué NO escalamos Ollama con HPA?

Buena pregunta. Hay varias razones:

  1. Carga del modelo: Cada nueva réplica de Ollama necesita cargar el modelo en memoria (~5 GB para Llama 3.1 8B). Eso toma tiempo y recursos.
  2. Estado: Ollama mantiene el modelo en memoria. No es stateless como una API REST típica.
  3. GPU: Si usas GPU, cada réplica necesita su propia GPU. No puedes compartir una GPU entre dos pods de Ollama eficientemente.
  4. Costo: Más réplicas = más GPUs = más dinero. Mucho más dinero.

La estrategia correcta para escalar Ollama es escalar verticalmente (más CPU/RAM/GPU) o usar una solución como vLLM o TGI (Text Generation Inference) que están diseñadas para servir modelos de forma eficiente con batching de requests.


La GPU: el elefante en la habitación

Hablemos de lo más importante para producción: la GPU.

Sin GPU (CPU-only)

Esto es lo que estamos haciendo en nuestro ejemplo con OrbStack. Funciona, pero es lento. Un prompt simple puede tomar 10-30 segundos en generar la respuesta completa con CPU. Para desarrollo y pruebas está bien, pero para producción con usuarios reales es inaceptable.

Con GPU

Con una GPU, el mismo prompt toma 1-3 segundos. La diferencia es brutal.

Para usar GPU en Kubernetes necesitas:

  1. Nodos con GPU (NVIDIA es el estándar).
  2. NVIDIA Device Plugin instalado en el cluster.
  3. Drivers NVIDIA en los nodos.
# Agregar a la spec del container de Ollama:
resources:
  requests:
    cpu: "1"
    memory: 4Gi
    nvidia.com/gpu: "1"    # Solicitar 1 GPU
  limits:
    cpu: "2"
    memory: 8Gi
    nvidia.com/gpu: "1"    # Limitar a 1 GPU

Referencia rápida de GPUs

GPUVRAMModelos que soportaPrecio cloud (aprox/hora)
NVIDIA T416 GBHasta 13B parámetros~$0.50
NVIDIA A10G24 GBHasta 30B parámetros~$1.00
NVIDIA A100 (40GB)40 GBHasta 70B parámetros~$3.00
NVIDIA A100 (80GB)80 GB70B+ parámetros~$5.00
NVIDIA H10080 GB70B+ (más rápido)~$8.00
OrbStack y GPU
OrbStack actualmente no soporta passthrough de GPU al cluster de Kubernetes. Para desarrollo local con GPU, puedes correr Ollama directamente en tu Mac (aprovechando Metal/Apple Silicon) y conectar tu app en K8s al host con host.docker.internal. Para producción con GPU real, necesitas un cluster en la nube (GKE, EKS, AKS) con nodos GPU.

Probar el setup completo

Expón la app localmente y pruébala:

# Port-forward para probar
kubectl port-forward -n llm svc/chat-app 8000:80

Abre http://localhost:8000 en tu navegador y listo — tu chat de IA corriendo en Kubernetes.

Para verificar el estado de todo:

# Ver todos los recursos
kubectl get all -n llm

# Ver los logs de Ollama
kubectl logs -n llm deploy/ollama -f

# Ver el HPA en acción
kubectl get hpa -n llm -w

Lo que falta: el camino a producción

Ok, ya tenemos Ollama corriendo en Kubernetes. ¿Significa que estamos listos para producción? Ni de cerca. Desplegar un LLM es solo el primer paso. Para que sea “producción de verdad” necesitas cubrir varias áreas que a menudo se pasan por alto.

1. Observabilidad de prompts (Langfuse)

¿Qué prompts están enviando tus usuarios? ¿Cuánto tarda cada respuesta? ¿Qué modelo genera mejores respuestas? Sin observabilidad, estás volando a ciegas.

Langfuse es una plataforma open source de observabilidad para LLMs que te permite:

  • Ver cada prompt y su respuesta en un dashboard.
  • Medir latencia, tokens consumidos y costos.
  • Evaluar la calidad de las respuestas.
  • Hacer tracing de cadenas complejas (RAG, agents, etc.).

2. Protección de datos sensibles (Presidio)

Este es uno de los riesgos más grandes y menos discutidos. ¿Qué pasa si un usuario envía su número de tarjeta de crédito en un prompt? ¿O datos personales como su RUT, DNI, dirección, historial médico?

Microsoft Presidio es un framework open source de detección y anonimización de datos personales (PII). Lo pones como middleware entre tu app y Ollama para:

  • Detectar PII antes de que llegue al modelo (nombres, emails, tarjetas, documentos de identidad).
  • Anonimizar o redactar la información sensible.
  • Loggear qué tipo de datos intentaron pasar.

Ejemplo del flujo:

3. Métricas y monitoreo

Necesitas saber:

  • Latencia por request: ¿cuánto tarda la inferencia? (p50, p95, p99)
  • Tokens por segundo: ¿qué tan rápido genera texto?
  • Memoria GPU/CPU: ¿estás cerca del límite?
  • Queue depth: ¿cuántos requests están esperando?
  • Error rate: ¿cuántos requests fallan?

Esto se resuelve con Prometheus + Grafana (si ya leíste el post de Prometheus, ya sabes cómo).

4. Guardrails de contenido

¿Qué pasa si alguien le pide a tu LLM que genere contenido ofensivo, instrucciones peligrosas o desinformación? Necesitas guardrails que filtren tanto la entrada como la salida.

Herramientas como NVIDIA NeMo Guardrails o Guardrails AI permiten definir reglas que:

  • Bloquean prompts que intentan hacer jailbreak del modelo.
  • Filtran respuestas que contengan contenido inapropiado.
  • Mantienen la conversación dentro del dominio esperado.

5. Costos y optimización

Correr un LLM en Kubernetes no es barato. Una GPU A100 puede costar $3/hora. Si la tienes corriendo 24/7, son **$2,160 al mes**. Para optimizar:

  • Cuantización: Modelos cuantizados (Q4, Q5) usan menos memoria y son más rápidos, con pérdida mínima de calidad.
  • Apagar cuando no se usa: Si tu LLM solo recibe tráfico en horario laboral, apágalo de noche.
  • Batching: Herramientas como vLLM agrupan múltiples requests para procesar en paralelo, maximizando el uso de la GPU.
  • Modelos más pequeños: ¿Realmente necesitas un modelo de 70B? Para muchos casos, Phi-3 (3.8B) o Llama 3.1 (8B) son más que suficientes.

Resumen

TemaEstadoHerramienta
Despliegue de Ollama en K8s✅ Cubierto hoyKubernetes + OrbStack
Recursos y HPA✅ Cubierto hoyRequests/Limits + HPA
GPU✅ ExplicadoNVIDIA Device Plugin
Observabilidad de prompts🔜 Próximo postLangfuse
Protección de datos🔜 Próximo postPresidio
Métricas del LLM🔜 Próximo postPrometheus + Grafana
Guardrails🔜 Próximo postNeMo Guardrails
Optimización🔜 Próximo postvLLM, cuantización

Este post es el primero de una serie sobre LLMs en Kubernetes para producción. En los próximos posts vamos a ir cubriendo cada una de las piezas que faltan, empezando por Langfuse para observabilidad y Presidio para protección de datos.

Porque desplegar un LLM es fácil. Lo difícil es hacerlo bien.


Recursos