Tenemos la imagen cineclick/churn-api:0.1.0 corriendo en un contenedor. Es un gran avance, pero un contenedor único no es producción: si el proceso muere de madrugada, se queda muerto; si el tráfico se multiplica durante una noche de estrenos, nadie añade capacidad; si la nueva versión resulta estar rota, nadie la retira. Todo eso — reiniciar, replicar, escalar, actualizar sin cortar el servicio — es el trabajo de un orquestador, y el estándar de la industria es Kubernetes. En esta lección aprenderás los cuatro conceptos de Kubernetes que un ML engineer necesita (Pod, Deployment, Service, HPA), escribiremos los manifiestos reales del servicio de churn — con probes apuntando a nuestro /salud, configuración por ConfigMap y secretos por Secret —, practicaremos el rollout y el rollback de una nueva imagen, y evaluaremos la alternativa serverless. No es un curso de Kubernetes (el portal tiene uno dedicado si quieres profundizar); es exactamente la porción que necesitas para desplegar y operar tu modelo.
Contenido
- Qué aporta un orquestador
- Kubernetes mínimo para ML engineers: Pod, Deployment, Service, HPA
- Los manifiestos del servicio churn-api
- Configuración y secretos: ConfigMap y Secret
- Rollout y rollback: dos niveles distintos de "volver atrás"
- Autoescalado con HPA
- La alternativa serverless: escala a cero y cold starts
- Serving gestionado y especializado, en una pincelada
- La decisión de CineClick
Qué aporta un orquestador
Un orquestador convierte "tengo una imagen" en "tengo un servicio operado". Sus aportaciones, en el orden en que las echarías de menos:
- Réplicas: ejecuta N copias idénticas del contenedor y reparte el tráfico entre ellas. Más capacidad y, sobre todo, tolerancia a fallos: una réplica caída no tumba el servicio.
- Self-healing: vigila cada contenedor (con las probes que veremos) y reinicia automáticamente el que muere o deja de responder. El "¿quién lo reinicia a las 3 de la madrugada?" tiene respuesta: la máquina.
- Rollout y rollback: despliega una versión nueva sustituyendo réplicas gradualmente sin cortar el servicio, y permite volver a la anterior con un comando.
- Service discovery y balanceo: da al conjunto de réplicas un nombre DNS estable y una IP única; los consumidores no saben (ni deben saber) cuántas réplicas hay ni dónde están.
- Gestión declarativa: describes el estado deseado en YAML ("quiero 3 réplicas de esta imagen con estos recursos") y Kubernetes trabaja continuamente para que la realidad coincida. Esto encaja con la filosofía del curso: igual que
dvc.yamldeclara el pipeline, los manifiestos declaran el despliegue — todo en Git, todo auditable.
Kubernetes mínimo para ML engineers: Pod, Deployment, Service, HPA
Cuatro conceptos bastan para nuestro caso:
| Objeto | Qué es | Para churn-api |
|---|---|---|
| Pod | La unidad mínima de ejecución: uno (o pocos) contenedores con red y almacenamiento compartidos. Efímero: puede morir y ser reemplazado en otra máquina. | Un contenedor de cineclick/churn-api:0.1.0 |
| Deployment | Declara "quiero N réplicas de este Pod, con esta imagen y estos recursos" y las mantiene. Gestiona rollouts y rollbacks. | 3 réplicas del servicio |
| Service | Nombre DNS e IP estables que balancean el tráfico entre los Pods vivos del Deployment. | churn-api — a donde llamará el frontend |
| HPA (Horizontal Pod Autoscaler) | Ajusta automáticamente el número de réplicas según una métrica (CPU, memoria...). | Entre 3 y 10 réplicas según CPU |
La regla mental: nunca creas Pods a mano — creas un Deployment que crea y cuida los Pods, y un Service que les da puerta de entrada. El Pod es ganado, no mascota: no le pongas cariño individual, porque Kubernetes lo matará y recreará cuando le convenga (y eso es bueno: por eso el modelo se descarga del registry al arrancar y no depende de nada local).
flowchart LR
C[Frontend CineClick] --> S[Service churn-api]
S --> P1[Pod réplica 1]
S --> P2[Pod réplica 2]
S --> P3[Pod réplica 3]
D[Deployment<br/>replicas: 3<br/>imagen: 0.1.0] -.crea y vigila.-> P1 & P2 & P3
H[HPA<br/>CPU > 60% ⇒ más réplicas] -.ajusta replicas.-> DLos manifiestos del servicio churn-api
Los manifiestos viven en el repo, en deploy/k8s/. Primero el Deployment, el fichero central:
# deploy/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: churn-api
labels:
app: churn-api
spec:
replicas: 3 # 3 copias: capacidad + tolerancia a fallos
selector:
matchLabels:
app: churn-api
template: # la plantilla del Pod que se replicará
metadata:
labels:
app: churn-api
spec:
containers:
- name: churn-api
image: cineclick/churn-api:0.1.0 # el tag semántico de 04-03
ports:
- containerPort: 8000
resources:
requests: # lo que el Pod tiene GARANTIZADO (sirve para planificar)
cpu: "250m" # 0.25 CPUs
memory: "512Mi"
limits: # techo: por encima, throttling (CPU) o kill (memoria)
cpu: "1"
memory: "1Gi"
env:
- name: MLFLOW_TRACKING_URI
valueFrom:
configMapKeyRef:
name: churn-api-config
key: mlflow_tracking_uri
- name: UMBRAL_ABANDONO
valueFrom:
configMapKeyRef:
name: churn-api-config
key: umbral_abandono
- name: API_KEY
valueFrom:
secretKeyRef:
name: churn-api-secretos
key: api_key
readinessProbe: # ¿puede recibir tráfico? (si no: se le retira del Service)
httpGet:
path: /salud
port: 8000
initialDelaySeconds: 15 # margen para descargar el modelo del registry
periodSeconds: 10
livenessProbe: # ¿está vivo? (si no: Kubernetes lo reinicia)
httpGet:
path: /salud
port: 8000
initialDelaySeconds: 30
periodSeconds: 20
failureThreshold: 3 # 3 fallos seguidos antes de reiniciarPuntos que merecen explicación:
requestsylimits: losrequestsson la reserva con la que Kubernetes decide en qué máquina cabe el Pod; loslimits, el techo. Para un servicio de ML, dimensiona la memoria mirando cuánto ocupa el modelo cargado (nuestro RandomForest de 300 árboles ocupa unos cientos de MB en memoria; 512Mi de request con límite 1Gi da margen). Un límite de memoria demasiado justo produce el clásico Pod que muere conOOMKilledjusto al cargar el modelo.- Las dos probes apuntan a
/salud, el endpoint que escribimos en 04-02 — y ahora cobra sentido su diseño: como/saludverifica que el modelo está en memoria, la readinessProbe garantiza que un Pod recién creado no recibe tráfico hasta que ha terminado de descargar al campeón del registry. La livenessProbe detecta procesos colgados y los reinicia. Y aquí se cierra el círculo del "fallar rápido" de 04-02: si el registry está caído, el Pod nuevo muere en el arranque, Kubernetes lo reintenta con espera creciente (CrashLoopBackOff), y mientras tanto las 3 réplicas viejas siguen sirviendo — el riesgo que aceptamos en 04-03, mitigado exactamente como prometimos. replicas: 3: mínimo razonable en producción — aguanta la caída de una réplica y los reinicios de rollout sin quedarse a cero.
El Service, mucho más corto:
# deploy/k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: churn-api
spec:
selector:
app: churn-api # enruta a todos los Pods con esta etiqueta
ports:
- port: 80 # puerto "público" dentro del clúster
targetPort: 8000 # puerto del contenedorDesde cualquier Pod del clúster, el frontend llama a http://churn-api/predecir — nombre estable, balanceo automático entre las réplicas ready. (La exposición fuera del clúster — Ingress, load balancer del cloud — depende de la plataforma de cada empresa y la dejamos indicada.)
Aplicar y comprobar:
kubectl apply -f deploy/k8s/ kubectl get pods -l app=churn-api # 3 pods Running y READY 1/1 kubectl logs -l app=churn-api --tail=5 # "Modelo churn-cineclick v2 cargado." x3
Configuración y secretos: ConfigMap y Secret
El principio de 04-03 — misma imagen para todos los entornos, la configuración entra por fuera — se materializa en dos objetos:
# deploy/k8s/configmap.yaml — configuración NO sensible, por entorno apiVersion: v1 kind: ConfigMap metadata: name: churn-api-config data: mlflow_tracking_uri: "http://mlflow.mlops.svc.cluster.local:5000" umbral_abandono: "0.5" --- # deploy/k8s/secret.yaml — sensible: NUNCA en Git en claro apiVersion: v1 kind: Secret metadata: name: churn-api-secretos type: Opaque stringData: api_key: "CAMBIAR-fuera-de-git"
- El ConfigMap de producción apunta al MLflow del clúster; el de staging, a otro. La imagen no cambia.
- El Secret guarda la API key de 04-02. Ojo: el YAML del ejemplo es ilustrativo — en un repo real los Secrets no se commitean en claro; se crean con
kubectl create secret, o con herramientas de la plataforma (Sealed Secrets, External Secrets, el gestor de secretos del cloud). La regla de 04-03 sigue vigente un nivel más arriba: los secretos no viven ni en la imagen ni en Git.
Rollout y rollback: dos niveles distintos de "volver atrás"
Supón que publicamos la versión 0.2.0 del servicio (nuevo endpoint /predecir-lote del ejercicio de 04-02). El despliegue es cambiar la imagen del Deployment:
kubectl set image deployment/churn-api churn-api=cineclick/churn-api:0.2.0 kubectl rollout status deployment/churn-api # observa el reemplazo gradual
Kubernetes ejecuta un rolling update: crea Pods con la imagen nueva, espera a que su readinessProbe pase (modelo descargado, /salud OK), les envía tráfico y solo entonces retira Pods viejos — uno a uno, sin interrupción del servicio. Si la 0.2.0 resulta defectuosa:
kubectl rollout undo deployment/churn-api # vuelve a la revisión anterior (0.1.0) kubectl rollout history deployment/churn-api # historial de revisiones
Y aquí conviene detenerse, porque CineClick tiene ahora dos niveles de rollback que no hay que confundir:
| Rollback de imagen | Rollback de modelo | |
|---|---|---|
| Qué revierte | Código del servicio, dependencias, entorno | El modelo que se sirve |
| Herramienta | kubectl rollout undo |
Mover el alias @champion en MLflow (03-02) |
| Cuándo usarlo | Bug en la API, dependencia rota, imagen defectuosa | El modelo nuevo predice peor en el mundo real |
| Requiere redesplegar | Sí (rolling update inverso) | No la imagen — pero sí reiniciar Pods (kubectl rollout restart) para que el lifespan recargue |
| Quién decide | Ingeniería | Ingeniería + negocio (flujo con revisión humana de 03-02) |
Esta separación es fruto directo de la decisión de 04-03 (modelo fuera de la imagen): cada problema se revierte con la herramienta de su nivel, sin arrastrar al otro. Si el modelo estuviera horneado en la imagen, ambos rollbacks serían el mismo y siempre pagarías el precio completo. Nota final: el rolling update es la estrategia estándar de Kubernetes; las estrategias avanzadas de release — shadow, canary, A/B, que deciden cuánto tráfico ve la versión nueva antes de confiar en ella — son materia de la lección 05-04.
Autoescalado con HPA
Con 3 réplicas fijas, la noche de estrenos con pico de cancelaciones satura el servicio, y la madrugada de un martes desperdicia dinero. El HPA ajusta las réplicas a la demanda:
# deploy/k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: churn-api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: churn-api
minReplicas: 3 # nunca menos del mínimo de producción
maxReplicas: 10 # techo de gasto
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # si la media supera el 60% del request, añade réplicasCómo razona: el objetivo es mantener el uso medio de CPU en el 60% del request (aquí, el 60% de 250m). Si las réplicas van al 90%, el HPA calcula cuántas harían falta para volver al 60% y las crea; si van al 20%, retira (sin bajar de 3). Dos matices para servicios de ML:
- El HPA solo funciona bien si los
requestsestán bien puestos: es la vara de medir del porcentaje. - CPU es un buen proxy para nuestra inferencia (el
predictdel RandomForest es CPU puro), pero recuerda que cada réplica nueva tarda en estar ready lo que tarde en descargar el modelo — el HPA reacciona a picos en decenas de segundos, no instantáneamente. Para picos previsibles (la campaña de un estreno), escalar preventivamente (kubectl scale --replicas=6) sigue siendo legítimo.
La alternativa serverless: escala a cero y cold starts
Kubernetes exige tener (o pagar) un clúster y a alguien que lo entienda. La alternativa serverless de contenedores (Cloud Run, Azure Container Apps, AWS App Runner / Lambda con imágenes) toma tu misma imagen Docker y se ocupa de todo: aprovisionar, escalar según peticiones, cobrar solo por uso... incluyendo escalar a cero cuando no hay tráfico.
El precio de escalar a cero es el cold start: la primera petición tras un periodo de silencio debe esperar a que se arranque un contenedor — y en nuestro caso el arranque incluye descargar el modelo del registry. Para churn-api serían varios segundos; para un modelo de deep learning de gigabytes, decenas. Un cold start de 5 s contra un presupuesto de 300 ms es un incumplimiento de 16x.
| Criterio | Serverless encaja | Serverless NO encaja |
|---|---|---|
| Tráfico | Esporádico, con valles largos o impredecible | Sostenido 24/7 (a volumen constante, sale más caro por petición) |
| Latencia | Tolerante a segundos ocasionales (o pagas instancias mínimas calientes) | Presupuesto estricto de ms en TODAS las peticiones |
| Tamaño del modelo | Pequeño (arranque rápido) | Grande (cold starts de decenas de segundos) |
| Equipo | Sin experiencia/ganas de operar Kubernetes | Ya opera un clúster con más servicios |
| Ejemplo | API interna de scoring usada unas veces al día; entornos de demo | El flujo de cancelación de CineClick |
Apunte que retomaremos en 04-05: el patrón batch de CineClick, que corre 10 minutos a la semana, es un candidato ideal a cómputo efímero/serverless — pagar por un job que corre, no por un servidor que espera.
Serving gestionado y especializado, en una pincelada
Existe un tercer camino que debes conocer aunque no lo usemos: plataformas de serving específicas de ML.
- KServe y Seldon Core: capas sobre Kubernetes que entienden de modelos — les das una URI de modelo (soportan MLflow) y generan el servicio, con extras como autoescalado por peticiones, canary nativo o explicadores. A cambio, otra pieza de plataforma que operar.
- Endpoints gestionados del cloud (SageMaker Endpoints, Vertex AI Prediction, Azure ML Endpoints): el proveedor opera todo; tú subes el modelo. Máxima comodidad, a cambio de acoplamiento al proveedor, coste y menos control sobre el contenedor (nuestro requisito de importar
construir_featuresdel paquete exige contenedor propio o imágenes personalizadas).
Son opciones razonables en organizaciones ya casadas con un cloud o con decenas de modelos que servir. Para un primer servicio, entenderlo hasta el fondo — como estamos haciendo — vale más que delegarlo en una caja negra.
La decisión de CineClick
Apliquemos los criterios: el flujo de cancelación tiene tráfico sostenido durante todo el día (la gente cancela a cualquier hora), presupuesto estricto de 300 ms que descarta cold starts, y la plataforma de CineClick ya opera un clúster Kubernetes gestionado en su cloud para el resto de microservicios — hay equipo, monitorización y costumbre. Decisión, documentada junto a la de 04-01:
El servicio
churn-apise despliega en el Kubernetes gestionado del cloud de CineClick: Deployment con 3 réplicas (imagencineclick/churn-api:0.1.0), Servicechurn-api, probes contra/salud, configuración por ConfigMap (MLFLOW_TRACKING_URI,UMBRAL_ABANDONO) y API key en Secret, HPA de 3 a 10 réplicas al 60% de CPU. Serverless queda descartado para este servicio por los cold starts frente al presupuesto de 300 ms, pero anotado como opción para el job batch semanal.
Los manifiestos quedan en deploy/k8s/ dentro del repo; hoy los aplicamos con kubectl apply a mano, y esa frase — a mano — ya debería incomodarte. Es el hueco que el módulo 5 viene a llenar.
Errores Comunes y Consejos
- Error: desplegar sin readinessProbe (o con una que no comprueba el modelo). Kubernetes enviaría tráfico a Pods que aún están descargando el modelo del registry: errores 500 en cada rollout y en cada escalado. Nuestra probe contra
/salud(que verifica modelo en memoria) lo evita — el diseño de 04-02 trabajando. - Error: límites de memoria por debajo de lo que ocupa el modelo cargado. Síntoma inconfundible: Pods en
OOMKilled/CrashLoopBackOffjusto tras arrancar. Mide la memoria del proceso con el modelo cargado y deja margen. - Error: confundir los dos rollbacks. "El modelo nuevo va mal" no se arregla con
kubectl rollout undo(eso revierte la imagen): se arregla moviendo@championen el registry y reiniciando los Pods. Y viceversa. - Error:
livenessdemasiado agresiva. UninitialDelaySecondscorto más un registry lento = Kubernetes matando Pods que estaban a punto de estar listos, en bucle. Da margen al arranque (nosotros: 30 s) y deja la exigencia fina a la readiness. - Error: tratar el HPA como magia. Sin
requestsrealistas no escala bien, y no es instantáneo: cada réplica nueva paga el arranque completo (contenedor + descarga del modelo). Para picos conocidos, escala preventivamente. - Consejo: los manifiestos viven en el repo (
deploy/k8s/), versionados como el código y el pipeline. En 05-02 será el CD quien los aplique; que ya estén en Git es media batalla. - Consejo: si quieres profundizar en Kubernetes (Ingress, namespaces, RBAC, operadores), el portal tiene un curso dedicado; aquí nos quedamos deliberadamente en la porción que un ML engineer opera a diario.
Ejercicios
Ejercicio 1
Marketing lanza una campaña de TV el sábado a las 21:00 y se espera que las visitas (y cancelaciones) se tripliquen entre las 21:00 y las 23:00. Con el HPA configurado (3–10 réplicas, 60% CPU), ¿qué harías el viernes? Razona por qué el HPA solo podría no bastar y da el comando concreto.
Ejercicio 2
Tras desplegar la imagen 0.2.0, los logs muestran que /predecir-lote devuelve errores 500 por un bug de serialización, aunque /predecir funciona. Paralelamente, Laura sospecha que el modelo v2 está degradándose y quiere volver a v1. Indica las dos acciones, con sus comandos/pasos, y explica por qué son independientes.
Ejercicio 3
Un Pod nuevo lleva 10 minutos en CrashLoopBackOff. kubectl logs del Pod muestra: KeyError: 'MLFLOW_TRACKING_URI'. Diagnostica la causa más probable y los pasos para confirmarla y corregirla. ¿Por qué las otras réplicas siguen funcionando?
Soluciones
Solución 1
El HPA reacciona después de que la CPU suba, y cada réplica nueva tarda en estar lista (programar el Pod + arrancar el contenedor + descargar el modelo + pasar la readinessProbe): en un pico vertical a las 21:00, los primeros minutos se servirían con capacidad insuficiente. Para un pico previsible, escala preventivamente: el viernes (o el sábado por la tarde), kubectl scale deployment/churn-api --replicas=8. Matiz importante: con HPA activo, este escalado manual convive con él — el HPA no bajará de su cálculo, pero sí podría reducir si la CPU está baja antes del pico; la opción robusta es subir temporalmente minReplicas a 8 en el HPA (editando hpa.yaml y aplicándolo) y restaurarlo el domingo. Lo esencial: autoescalado reactivo para lo imprevisible, planificación para lo anunciado.
Solución 2
- Bug de la imagen 0.2.0:
kubectl rollout undo deployment/churn-api— rolling update inverso a la 0.1.0, sin corte de servicio. Después, arreglar el bug, publicar 0.2.1 y redesplegar. - Degradación del modelo v2: es el flujo de 03-02, no de Kubernetes — con la evidencia en la mano y revisión humana, mover el alias
@championde v2 a v1 en el registry MLflow, y a continuaciónkubectl rollout restart deployment/churn-apipara que los Pods recarguen el modelo en su lifespan (verificable enGET /version, que pasará a responder"1").
Son independientes porque en 04-03 separamos deliberadamente los ciclos de vida: la imagen gobierna el código; el registry gobierna el modelo. Puedes revertir cualquiera de los dos sin tocar el otro — y en este escenario haces ambos, cada uno con su herramienta y su proceso de decisión.
Solución 3
La causa más probable: el Pod arranca sin la variable MLFLOW_TRACKING_URI — recuerda que en 04-02 la leemos con os.environ[...] precisamente para fallar rápido si falta. Como la variable viene de un ConfigMap, los sospechosos son: el ConfigMap churn-api-config no existe en ese namespace, la clave se llama distinto (mlflow_tracking_uri), o el Deployment referencia mal el nombre. Confirmación: kubectl describe pod <pod> (mostrará errores de referencia al ConfigMap) y kubectl get configmap churn-api-config -o yaml (verificar existencia y clave). Corrección: crear/corregir el ConfigMap y dejar que el Deployment recree el Pod (o kubectl rollout restart). Las otras réplicas siguen funcionando si arrancaron antes del problema (por ejemplo, alguien borró o renombró el ConfigMap después): las variables se inyectan al crear el Pod, así que los vivos conservan su valor — otro ejemplo de por qué "funciona en los Pods viejos" no prueba que el despliegue esté sano.
Conclusión
El servicio de churn ya es un ciudadano de producción: 3 réplicas (hasta 10 con el HPA) del contenedor cineclick/churn-api:0.1.0 detrás del Service churn-api, con Kubernetes reiniciando lo que muere, reteniendo el tráfico hasta que cada Pod ha cargado al campeón (readinessProbe contra /salud), la configuración entrando por ConfigMap y los secretos por Secret, y dos palancas de rollback bien separadas: kubectl rollout undo para la imagen, mover @champion para el modelo. Sabes además cuándo esta maquinaria sobra — tráfico esporádico y tolerancia a cold starts: serverless — y que existen plataformas de serving especializadas para cuando los modelos se cuenten por decenas. Queda una pregunta incómoda antes de cerrar el módulo: este servicio, ¿es rápido? ¿Cuánto tarda de verdad una predicción, dónde se va el tiempo, cuánto cuesta cada mil llamadas, y qué se puede ganar antes de pagar más infraestructura? La siguiente lección mide primero — percentiles, no medias — y optimiza después, por orden de rentabilidad.
Curso de MLOps
Módulo 1: Fundamentos de MLOps
- Qué es MLOps y por qué los modelos mueren en el notebook
- El ciclo de vida de un modelo de ML en producción
- Niveles de madurez MLOps y roles del equipo
- El proyecto del curso: del notebook a producción
Módulo 2: Del notebook al código reproducible
- Estructura de un proyecto de ML: del notebook al paquete
- Entornos reproducibles y gestión de dependencias
- Versionado de datos con DVC
- Pipelines de entrenamiento reproducibles
Módulo 3: Experimentos y registro de modelos
- Tracking de experimentos con MLflow
- Model registry: versionar y promocionar modelos
- Feature stores: cuándo y para qué
Módulo 4: Servir modelos en producción
- Patrones de despliegue: batch, online y streaming
- Un servicio de predicción con FastAPI
- Empaquetado con Docker
- Escalado y despliegue: Kubernetes y serverless
- Optimización de la inferencia: latencia y coste
Módulo 5: Automatización: CI/CD y orquestación
- CI para ML: tests de código, datos y modelos
- CD: automatizar el despliegue del modelo
- Orquestación de pipelines de ML
- Estrategias de release: shadow, canary y A/B
