Práctica: Despliega un servicio de predicción con múltiples réplicas
En esta lección práctica, pasarás de los conceptos teóricos a la implementación concreta. Aprenderás a definir, configurar y desplegar un servicio de inferencia de Machine Learning en Kubernetes, escalándolo mediante múltiples réplicas para garantizar alta disponibilidad y capacidad de respuesta ante cargas variables. Esta es la piedra angular del despliegue de modelos en producción, donde la resiliencia y la escalabilidad son no solo deseables, sino obligatorias.
Construiremos sobre la base de tener un modelo de Machine Learning ya contenerizado en una imagen Docker. Nuestro objetivo será crear los manifiestos de Kubernetes necesarios para orquestar este contenedor, exponerlo como un servicio estable a la red y asegurarnos de que múltiples instancias (pods) estén ejecutándose simultáneamente, gestionadas automáticamente por el controlador de Kubernetes. Este ejercicio simula un escenario real donde tu API de predicción debe soportar picos de tráfico sin caerse.
Concepto Clave: Réplicas, Deployments y Servicios
Para entender cómo Kubernetes maneja la escalabilidad, debemos dominar tres recursos fundamentales que trabajan en conjunto. Primero, un Pod es la unidad más pequeña y desplegable en Kubernetes, que encapsula uno o más contenedores. Sin embargo, gestionar Pods individuales es inviable en producción. Aquí es donde entra el Deployment. Un Deployment es un objeto de alto nivel que declara el estado deseado de tu aplicación: qué imagen usar, cuántas réplicas (copias idénticas) de los Pods deben ejecutarse, y cómo actualizarlas. El controlador del Deployment se encarga de que la realidad (Pods ejecutándose) coincida con tu declaración, creando o eliminando Pods según sea necesario.
El tercer pilar es el Service. Cuando tienes múltiples réplicas de un Pod, necesitas una forma estable de acceder a ellas, independientemente de qué Pod específico atienda la solicitud. Un Service actúa como un balanceador de carga interno (y a veces externo) y un punto de acceso de red permanente. Asigna un nombre de DNS estable (por ejemplo, mi-servicio-de-prediccion.namespace.svc.cluster.local) y un IP virtual, y distribuye el tráfico entrante entre todos los Pods sanos que coincidan con su selector de etiquetas. En esencia, el Deployment gestiona el "ejército" de réplicas, y el Service es el "mostrador de atención" único al que llegan todas las peticiones.
Tip: Piensa en un Deployment como el plano del arquitecto y el capataz de obra para tu aplicación. El plano (el manifiesto YAML) dice "quiero tres casas idénticas". El capataz (el controlador) se asegura de que siempre haya tres casas en pie. Si una se cae, él construye otra inmediatamente. El Service es el número de teléfono de emergencia único que la gente marca; la centralita (Service) decide a qué casa conectar la llamada, distribuyendo el trabajo equitativamente.
Cómo funciona en la práctica: El ciclo de una petición
Imaginemos que has desplegado un modelo de clasificación de imágenes con tres réplicas. El proceso comienza cuando defines tu estado deseado en un archivo YAML, aplicándolo con kubectl apply -f deployment.yaml. El controlador de la API de Kubernetes recibe esta definición y el controlador del Deployment se activa. Su misión es crear tres Pods (Réplicas). Para ello, crea un objeto secundario llamado ReplicaSet, cuyo único trabajo es contar Pods. El ReplicaSet crea los tres Pods programándolos en los nodos disponibles del clúster. Cada Pod descarga la imagen Docker especificada y ejecuta el servidor de predicción (por ejemplo, una app Flask que carga el modelo).
Paralelamente, defines un Service en otro archivo YAML y lo aplicas. El Service consulta la API para encontrar todos los Pods que tengan las etiquetas que él está configurado para seleccionar (por ejemplo, app: modelo-clasificador). A medida que los Pods se crean, el Service los detecta y los añade a su pool de endpoints. Cuando una aplicación cliente, ya sea dentro o fuera del clúster, necesita hacer una predicción, envía una petición HTTP al nombre DNS o IP del Service. El proxy de red de Kubernetes (kube-proxy) intercepta esta petición y, utilizando reglas de iptables o IPVS, la redirige de manera aleatoria o round-robin a la IP de uno de los tres Pods. Si un Pod falla, el ReplicaSet lo detecta y crea uno nuevo, y el Service automáticamente deja de enviarle tráfico, manteniendo la disponibilidad del sistema.
Código en acción: Manifiestos completos para un modelo de predicción
A continuación, presentamos un ejemplo completo y funcional para desplegar un servicio de predicción simple. Asumimos que tienes una imagen Docker llamada mi-usuario/modelo-clasificador:v1.0 que expone un endpoint /predict en el puerto 5000. El primer archivo define el Deployment con tres réplicas.
# deployment-modelo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-modelo-clasificador
labels:
app: modelo-clasificador
tier: backend
spec:
replicas: 3 # Número clave de réplicas deseadas
selector:
matchLabels:
app: modelo-clasificador # Debe coincidir con las labels de los Pods
template:
metadata:
labels:
app: modelo-clasificador # Etiqueta crucial para que el Service encuentre estos Pods
version: "v1.0"
spec:
containers:
- name: contenedor-modelo
image: mi-usuario/modelo-clasificador:v1.0
ports:
- containerPort: 5000 # Puerto que el contenedor escucha
env:
- name: MODELO_UMBRAL
value: "0.75"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 5000
initialDelaySeconds: 5
periodSeconds: 5
El segundo archivo define el Service que expone nuestro Deployment. Usaremos un Service de tipo ClusterIP para hacerlo accesible internamente dentro del clúster. Para pruebas locales con Minikube o si necesitas acceso externo, podrías usar NodePort o LoadBalancer.
# service-modelo.yaml
apiVersion: v1
kind: Service
metadata:
name: servicio-modelo-clasificador
spec:
type: ClusterIP # Tipo de servicio. ClusterIP crea una IP interna.
selector:
app: modelo-clasificador # ¡DEBE COINCIDIR con las labels del Pod en el Deployment!
ports:
- protocol: TCP
port: 80 # Puerto en el que el Service escuchará INTERNAMENTE
targetPort: 5000 # Puerto del contenedor al que redirige el tráfico
Para desplegar estos recursos, ejecuta los siguientes comandos en tu terminal, asumiendo que tienes configurado un contexto de Kubernetes (como Minikube o un clúster en la nube).
# Aplicar el Deployment
kubectl apply -f deployment-modelo.yaml
# Aplicar el Service
kubectl apply -f service-modelo.yaml
# Verificar el estado del Deployment
kubectl get deployments
# Verificar los Pods creados (deberías ver 3)
kubectl get pods -l app=modelo-clasificador
# Verificar el Service y su IP interna (CLUSTER-IP)
kubectl get service servicio-modelo-clasificador
Una vez desplegado, puedes probar el servicio desde dentro del clúster. Una forma rápida es crear un Pod temporal con curl y hacer una petición al nombre del servicio.
kubectl run curl-test --image=radial/busyboxplus:curl -i --tty --rm
# Una vez dentro del shell del Pod temporal:
curl http://servicio-modelo-clasificador/predict -X POST -H "Content-Type: application/json" -d '{"data": "tus_datos_aqui"}'
Errores comunes y cómo evitarlos
Al comenzar con Kubernetes, es fácil tropezar con ciertos errores que pueden detener tu despliegue. Aquí te presentamos los más frecuentes y cómo solucionarlos.
1. Selector de etiquetas (Label Selector) no coincide: Este es el error más común. Las etiquetas definidas en spec.selector.matchLabels del Deployment y en spec.template.metadata.labels deben coincidir exactamente. Además, el selector del Service (spec.selector) debe coincidir con estas mismas etiquetas. Si no coinciden, el Service no encontrará ningún Pod y no se enrutará tráfico. Solución: Verifica meticulosamente la ortografía y los pares clave-valor en tus archivos YAML. Usa kubectl get pods --show-labels para ver las etiquetas reales de tus Pods.
2. Falta de recursos (CPU/Memoria) en los nodos: Especialmente en entornos locales como Minikube, puedes solicitar más recursos de los disponibles. Si los resources.requests de tu contenedor exceden la capacidad del nodo, el Pod quedará en estado Pending. Solución: Usa kubectl describe pod <nombre-pod> para ver eventos que indiquen "Insufficient cpu/memory". Ajusta tus requests y limits a valores realistas o añade recursos a tu clúster.
3. Puertos incorrectos o no expuestos: El containerPort en el Deployment es principalmente informativo. El error real viene cuando el targetPort en el Service no coincide con el puerto en el que tu aplicación escucha realmente dentro del contenedor. Solución: Asegúrate de que tu aplicación dentro del contenedor Docker escucha en el puerto que especificas (por ejemplo, 5000) y que el Service apunta a ese mismo puerto con targetPort.
4. Imagen de contenedor no encontrada o problemas de pull: Si usas una imagen privada sin configurar correctamente los imagePullSecrets, o si el nombre/tag de la imagen es incorrecto, el Pod fallará. Solución: Revisa los eventos del Pod con kubectl describe pod. Busca errores como "ErrImagePull" o "ImagePullBackOff". Verifica el nombre de la imagen y, si es privada, configura un secret para Docker Registry.
5. Sondas (Probes) mal configuradas: Un error sutil pero crítico. Si tu contenedor tarda más de lo esperado en iniciarse, pero la initialDelaySeconds de tu livenessProbe es muy corta, Kubernetes matará el contenedor pensando que está en un bucle de error, creando un ciclo de reinicios infinito. Solución: Ajusta los tiempos de las sondas según el tiempo real de arranque de tu aplicación. Usa kubectl logs <nombre-pod> para depurar el inicio de tu aplicación.
Checklist de dominio
Antes de considerar esta lección completamente dominada, asegúrate de poder verificar cada uno de los siguientes puntos de forma práctica.
- Puedo escribir un manifiesto de Deployment YAML desde cero que especifique una imagen de contenedor, el número de réplicas, y límites de recursos.
- Comprendo la diferencia entre
port,targetPort, ynodePorten la definición de un Service y puedo configurarlos correctamente. - Sé cómo escalar manualmente un Deployment usando
kubectl scale deployment/<nombre> --replicas=5y entiendo el impacto inmediato en los Pods. - Puedo diagnosticar un problema donde un Service no está enrutando tráfico, verificando selectores de etiquetas, endpoints (
kubectl get endpoints), y el estado de los Pods. - Sé cómo exponer mi servicio de predicción fuera del clúster, ya sea cambiando el tipo de Service a NodePort o LoadBalancer, o usando un Ingress Controller.
- Puedo definir y configurar correctamente livenessProbe y readinessProbe para una aplicación web de ML, entendiendo su propósito distinto.
- Entiendo cómo Kubernetes distribuye el tráfico entre las réplicas y por qué la aplicación debe ser stateless o manejar el estado externamente (por ejemplo, en una base de datos) para que este esquema funcione.
- Puedo realizar una actualización rodante (rolling update) de mi modelo cambiando la versión de la imagen en el Deployment y observando cómo Kubernetes reemplaza gradualmente los Pods antiguos por los nuevos.