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

  1. Qué aporta un orquestador
  2. Kubernetes mínimo para ML engineers: Pod, Deployment, Service, HPA
  3. Los manifiestos del servicio churn-api
  4. Configuración y secretos: ConfigMap y Secret
  5. Rollout y rollback: dos niveles distintos de "volver atrás"
  6. Autoescalado con HPA
  7. La alternativa serverless: escala a cero y cold starts
  8. Serving gestionado y especializado, en una pincelada
  9. 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.yaml declara 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.-> D

Los 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 reiniciar

Puntos que merecen explicación:

  • requests y limits: los requests son la reserva con la que Kubernetes decide en qué máquina cabe el Pod; los limits, 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 con OOMKilled justo 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 /salud verifica 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 contenedor

Desde 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éplicas

Có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 requests están bien puestos: es la vara de medir del porcentaje.
  • CPU es un buen proxy para nuestra inferencia (el predict del 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_features del 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-api se despliega en el Kubernetes gestionado del cloud de CineClick: Deployment con 3 réplicas (imagen cineclick/churn-api:0.1.0), Service churn-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/CrashLoopBackOff justo 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 @champion en el registry y reiniciando los Pods. Y viceversa.
  • Error: liveness demasiado agresiva. Un initialDelaySeconds corto 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 requests realistas 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 @champion de v2 a v1 en el registry MLflow, y a continuación kubectl rollout restart deployment/churn-api para que los Pods recarguen el modelo en su lifespan (verificable en GET /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.

© Copyright 2026. Todos los derechos reservados