Módulo 2: Pods Deployments And Services
8. Proyecto: `andes-cargo-status-api` con N réplicas
Descripción
Este proyecto junta, en un solo sistema verificado de punta a punta, las tres primitivas que construiste en este módulo: escalas el Deployment de andes-cargo-status-api a tres réplicas, confirmas que las tres responden a través del mismo status-api-service —el balanceo de carga que la lección 6 explicó en teoría, ahora con evidencia real—, y repites, una última vez, el experimento central de este módulo: borrar un Pod a propósito y verlo reponerse solo, esta vez con el sistema completo funcionando junto. Todo lo que sigue corrió de verdad contra andes-cargo-cluster.
Conexión con el módulo
Cada lección de este módulo construyó una pieza: el concepto de Pod (lecciones 2 y 4), el concepto de Deployment/ReplicaSet (lecciones 3 y 5), el concepto de Service (lecciones 6 y 7). Este proyecto es la primera vez que ves las tres funcionando juntas, bajo una condición que ninguna lección individual probó todavía: más de dos réplicas, con tráfico real repartido entre todas.
Paso 1 — Escala el Deployment a 3 réplicas
Modifica deployment.yaml (el mismo archivo de la lección 5), cambiando replicas: 2 a replicas: 3:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: andes-cargo-status-api
namespace: andes-cargo
labels:
app: andes-cargo-status-api
spec:
replicas: 3
selector:
matchLabels:
app: andes-cargo-status-api
template:
metadata:
labels:
app: andes-cargo-status-api
spec:
containers:
- name: andes-cargo-status-api
image: andes-cargo-status-api:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
kubectl apply -f deployment.yaml
Qué esperar (configured, no created — el Deployment ya existía desde la lección 5, esta es una actualización de un campo, no una creación):
deployment.apps/andes-cargo-status-api configured
Este único cambio —un número, de 2 a 3— es todo lo que hizo falta. No recreaste el Service, no tocaste el namespace, no reconstruiste la imagen. El bucle de reconciliación de la lección 3 hace el resto solo.
Paso 2 — Verifica: tres réplicas, repartidas entre los nodos disponibles
kubectl get deployments -n andes-cargo
Qué esperar (AGE es tu valor variable):
NAME READY UP-TO-DATE AVAILABLE AGE
andes-cargo-status-api 3/3 3 3 119s
kubectl get pods -n andes-cargo -o wide
Qué esperar (sufijos hash, IPs y AGE son tus valores variables — el prefijo del nombre y el patrón de dos nodos son literales para esta arquitectura de clúster):
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
andes-cargo-status-api-56856576d4-7ztk9 1/1 Running 0 106s 10.244.1.3 andes-cargo-cluster-worker2 <none> <none>
andes-cargo-status-api-56856576d4-fqn9j 1/1 Running 0 6s 10.244.2.5 andes-cargo-cluster-worker <none> <none>
andes-cargo-status-api-56856576d4-vjc9k 1/1 Running 0 119s 10.244.2.4 andes-cargo-cluster-worker <none> <none>
Tres Pods, repartidos entre andes-cargo-cluster-worker y andes-cargo-cluster-worker2 — el control-plane no recibe ninguno, exactamente lo que predijiste en el Ejercicio 2 de la lección 5. Con solo dos nodos worker disponibles para tres réplicas, uno de los dos necesariamente termina con dos Pods — en este caso, andes-cargo-cluster-worker.
Paso 3 — Confirma el balanceo: las tres réplicas responden
Aquí está la verificación central de este proyecto: ¿de verdad el Service reparte tráfico entre las tres réplicas, no solo entre dos, ni siempre hacia la misma? kubectl port-forward (lección 7) no sirve para esto — mantiene la conexión fija a un solo Pod. En su lugar, lanza un Pod temporal dentro del clúster, y desde ahí hazle varias solicitudes al Service por su nombre DNS interno:
kubectl run curl-client --image=curlimages/curl:latest --restart=Never -n andes-cargo -- sleep 3600
Qué esperar:
pod/curl-client created
Espera unos segundos a que arranque, y confirma:
kubectl get pod curl-client -n andes-cargo
Qué esperar:
NAME READY STATUS RESTARTS AGE
curl-client 1/1 Running 0 5s
Ahora, desde dentro de ese Pod, haz nueve solicitudes seguidas contra el nombre DNS completo del Service —el mismo patrón <service>.<namespace>.svc.cluster.local que la lección 6 explicó—:
kubectl exec curl-client -n andes-cargo -- sh -c 'for i in $(seq 1 9); do curl -s http://status-api-service.andes-cargo.svc.cluster.local/health; echo; done'
Qué esperar (literal, ejecutado — las nueve respuestas son idénticas en contenido, porque /health no depende de qué Pod específico responda, pero eso no significa que las nueve las haya atendido el mismo Pod):
{"service":"andes-cargo-status-api","status":"ok"}
{"service":"andes-cargo-status-api","status":"ok"}
{"service":"andes-cargo-status-api","status":"ok"}
{"service":"andes-cargo-status-api","status":"ok"}
{"service":"andes-cargo-status-api","status":"ok"}
{"service":"andes-cargo-status-api","status":"ok"}
{"service":"andes-cargo-status-api","status":"ok"}
{"service":"andes-cargo-status-api","status":"ok"}
{"service":"andes-cargo-status-api","status":"ok"}
Nueve respuestas 200, todas con el mismo cuerpo — como esperabas. La pregunta real es cuál Pod atendió cada una, y la respuesta no está en el curl, está en los logs de cada Pod. Cuenta cuántas líneas de GET /health tiene cada uno:
for p in $(kubectl get pods -n andes-cargo -l app=andes-cargo-status-api -o jsonpath='{.items[*].metadata.name}'); do
echo "--- $p ---"
kubectl logs "$p" -n andes-cargo | grep -c "GET /health"
done
Qué esperar (los nombres de Pod son tu valor variable; los conteos exactos también varían según cuántas veces hayas repetido el curl —el mecanismo de reparto de kube-proxy no garantiza una distribución perfectamente pareja en pocas solicitudes—, pero el patrón de fondo —ningún Pod en cero— es lo que confirma el balanceo):
--- andes-cargo-status-api-56856576d4-7ztk9 ---
3
--- andes-cargo-status-api-56856576d4-fqn9j ---
4
--- andes-cargo-status-api-56856576d4-vjc9k ---
3
Ahí está la confirmación: los tres Pods recibieron solicitudes (3, 4, 3 — suman las nueve del curl), no solo uno o dos. kube-proxy, el componente que ya nombró la lección 6, repartió las solicitudes entre las tres IPs de la lista de Endpoints, sin que nadie le dijera explícitamente cómo hacerlo — el mismo mecanismo de la lección 6, ahora confirmado con tres réplicas en vez de dos.
Paso 4 — Borra un Pod a propósito, una última vez
Repite, por última vez en este módulo, el experimento central: borrar un Pod y observar la reposición — pero ahora con tres réplicas y tráfico real fluyendo por el Service.
kubectl get pods -n andes-cargo -l app=andes-cargo-status-api -o wide
Qué esperar (tus valores de nombre/IP/AGE van a diferir):
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
andes-cargo-status-api-56856576d4-7ztk9 1/1 Running 0 2m16s 10.244.1.3 andes-cargo-cluster-worker2 <none> <none>
andes-cargo-status-api-56856576d4-fqn9j 1/1 Running 0 36s 10.244.2.5 andes-cargo-cluster-worker <none> <none>
andes-cargo-status-api-56856576d4-vjc9k 1/1 Running 0 2m29s 10.244.2.4 andes-cargo-cluster-worker <none> <none>
kubectl delete pod andes-cargo-status-api-56856576d4-fqn9j -n andes-cargo
Sustituye por el nombre exacto de uno de tus propios Pods.
Qué esperar:
pod "andes-cargo-status-api-56856576d4-fqn9j" deleted from andes-cargo namespace
kubectl get pods -n andes-cargo -l app=andes-cargo-status-api -o wide
Qué esperar (andes-cargo-status-api-56856576d4-2slnk es un Pod nuevo, con IP nueva; los otros dos, sin cambios):
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
andes-cargo-status-api-56856576d4-2slnk 1/1 Running 0 36s 10.244.2.6 andes-cargo-cluster-worker <none> <none>
andes-cargo-status-api-56856576d4-7ztk9 1/1 Running 0 2m52s 10.244.1.3 andes-cargo-cluster-worker2 <none> <none>
andes-cargo-status-api-56856576d4-vjc9k 1/1 Running 0 3m5s 10.244.2.4 andes-cargo-cluster-worker <none> <none>
kubectl get deployment andes-cargo-status-api -n andes-cargo
Qué esperar (nunca cayó de 3/3):
NAME READY UP-TO-DATE AVAILABLE AGE
andes-cargo-status-api 3/3 3 3 3m5s
Paso 5 — Confirma que el sistema completo sigue sano después del reemplazo
Una última verificación: con el Pod nuevo ya corriendo, confirma que el Service lo incluyó automáticamente en su lista de Endpoints —sin que nadie se lo pidiera— y que sigue balanceando correctamente:
kubectl exec curl-client -n andes-cargo -- sh -c 'for i in $(seq 1 6); do curl -s -o /dev/null -w "%{http_code}\n" http://status-api-service.andes-cargo.svc.cluster.local/health; done'
Qué esperar:
200
200
200
200
200
200
for p in $(kubectl get pods -n andes-cargo -l app=andes-cargo-status-api -o jsonpath='{.items[*].metadata.name}'); do
echo "--- $p ---"
kubectl logs "$p" -n andes-cargo | grep -c "GET /health"
done
Qué esperar (los conteos son acumulados desde que cada Pod arrancó — el Pod de reemplazo, más joven, puede mostrar un número distinto a los otros dos; lo que importa es que ninguno quede en cero):
--- andes-cargo-status-api-56856576d4-2slnk ---
3
--- andes-cargo-status-api-56856576d4-7ztk9 ---
3
--- andes-cargo-status-api-56856576d4-vjc9k ---
6
El Pod nuevo (2slnk) ya está recibiendo tráfico, con nada especial que hayas tenido que configurar — el selector del Service lo encontró en cuanto nació, exactamente el mecanismo automático que la lección 6 describió.
Limpia el Pod temporal de prueba, ya no lo necesitas:
kubectl delete pod curl-client -n andes-cargo
Qué esperar:
pod "curl-client" deleted from andes-cargo namespace
El checklist final: el sistema completo de este módulo
kubectl get all -n andes-cargo
Qué esperar (nombres de Pod e IPs son variables; la forma general —un Deployment, un ReplicaSet, tres Pods, un Service— es literal):
NAME READY STATUS RESTARTS AGE
pod/andes-cargo-status-api-56856576d4-2slnk 1/1 Running 0 48s
pod/andes-cargo-status-api-56856576d4-7ztk9 1/1 Running 0 3m4s
pod/andes-cargo-status-api-56856576d4-vjc9k 1/1 Running 0 3m17s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/status-api-service ClusterIP 10.96.78.1 <none> 80/TCP 2m1s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/andes-cargo-status-api 3/3 3 3 3m17s
NAME DESIRED CURRENT READY AGE
replicaset.apps/andes-cargo-status-api-56856576d4 3 3 3 3m17s
Cuatro tipos de objeto, una sola jerarquía: el Deployment que declaraste (lección 5, escalado en el Paso 1 de este proyecto), el ReplicaSet que creó por debajo (lección 3, nunca lo tocaste directamente), los tres Pods que mantiene vivos (confirmados sanos y balanceados en este proyecto), y el Service que les da una dirección estable (lección 7). Ningún objeto de este sistema depende de que recuerdes ningún IP ni ningún nombre de Pod específico — esa es, en una sola pantalla, la tesis completa de este módulo.
Errores comunes
Verificar el balanceo con curl directo desde tu máquina en vez de desde dentro del clúster, y no ver nada distribuido (de flujo, específico de este proyecto). Qué pasa: alguien intenta repetir la verificación del Paso 3 usando kubectl port-forward (el patrón de la lección 7) en un bucle, y nota que todas las solicitudes las atiende el mismo Pod, sin importar cuántas veces repita el curl. Por qué pasa: kubectl port-forward mantiene una conexión fija hacia un Pod específico durante toda la vida del túnel — no reparte solicitudes individuales entre los Pods disponibles, a diferencia de una solicitud real hecha directamente contra la IP del Service desde dentro del clúster. Cómo detectarlo: si ves el mismo Pod respondiendo a GET /health en sus logs, una y otra vez, mientras otros Pods sanos quedan en cero. Cómo corregirlo: para verificar balanceo real entre réplicas, la solicitud tiene que llegar directo a la IP del Service (o a su nombre DNS) desde dentro del clúster —el patrón exacto de este proyecto, con un Pod temporal (curl-client) haciendo las solicitudes—, no a través de un túnel de port-forward, que fija la conexión a un solo backend.
Esperar una distribución perfectamente pareja (3/3/3) en solo nueve solicitudes, y sospechar de un problema si no la ves (conceptual). Qué pasa: alguien corre la verificación del Paso 3, ve un reparto como 3/4/2 en vez de 3/3/3, y asume que algo está mal con el Service o con kube-proxy. Por qué pasa: "balanceo de carga" suena a "reparto matemáticamente exacto", pero el mecanismo real de kube-proxy (basado en reglas de iptables por defecto) no garantiza una distribución perfecta en una muestra pequeña — sí garantiza, con un volumen de tráfico suficiente, que ningún Pod sano queda permanentemente sin recibir solicitudes. Cómo detectarlo: si tu propio reparto de nueve solicitudes no salió exactamente 3/3/3. Cómo corregirlo: lo que importa verificar no es la proporción exacta, sino que ningún Pod sano quede en cero — eso es lo que confirma que el Service los está considerando a los tres, no que el reparto sea matemáticamente perfecto en una muestra tan pequeña.
Olvidar borrar curl-client al terminar, y confundirlo en una lección o módulo futuro con un objeto real de Andes Cargo (de disciplina). Qué pasa: alguien termina este proyecto sin correr el kubectl delete pod curl-client del Paso 5, y en un módulo posterior (por ejemplo, el Módulo 6, cuando Gatekeeper empiece a exigir límites de recursos en todo el namespace andes-cargo) se encuentra con un Pod sin ninguna relación con andes-cargo-status-api violando una política, sin recordar de dónde salió. Por qué pasa: es fácil olvidar un Pod de depuración temporal una vez que cumplió su propósito inmediato. Cómo detectarlo: kubectl get pods -n andes-cargo muestra un Pod llamado curl-client, sin ninguna etiqueta app=andes-cargo-status-api, mucho después de que este proyecto terminó. Cómo corregirlo: el Paso 5 de este proyecto incluye la limpieza explícita por esta razón — cualquier Pod de depuración temporal que crees en el resto de esta guía (vas a repetir este patrón) debería borrarse en cuanto termine de cumplir su propósito, no quedar corriendo indefinidamente dentro de andes-cargo.
Ejercicios
Ejercicio 1 — Reconstruye el proyecto completo de memoria. Sin volver a la lección, enumera los cinco pasos de este proyecto, en orden, y qué confirmó cada uno.
Ver solución
- Escalar
deployment.yamldereplicas: 2areplicas: 3, y aplicarlo — confirma que un solo número cambia todo lo necesario. - Verificar con
kubectl get deployments/kubectl get pods -o wideque las tres réplicas están sanas, repartidas entre los dos nodosworker. - Lanzar un Pod temporal (
curl-client) dentro del clúster, y hacer varias solicitudes contra el nombre DNS delService— confirma, con los logs de cada Pod, que las tres réplicas reciben tráfico, no solo una o dos. - Borrar un Pod a propósito, y confirmar que el
Deploymentnunca cae de3/3— elReplicaSetrepone la réplica perdida de inmediato. - Reverificar el balanceo después del reemplazo, confirmando que el Pod nuevo se integra automáticamente a la lista de
EndpointsdelService, sin ninguna configuración adicional.
Ejercicio 2 — Explica por qué port-forward no sirve para verificar balanceo. Sin volver a "Errores comunes", explica en dos o tres frases por qué kubectl port-forward no es la herramienta correcta para confirmar que un Service reparte tráfico entre varias réplicas, y qué herramienta sí lo es.
Ver solución
kubectl port-forward abre un túnel fijo hacia un solo backend (un Pod específico, elegido una vez al iniciar el túnel) y mantiene esa misma conexión para todas las solicitudes que pasen por él durante su vida — no vuelve a consultar la lista de Endpoints del Service en cada solicitud individual. Para ver balanceo real, la solicitud tiene que originarse dentro del clúster y llegar directo a la IP (o el nombre DNS) del Service, dejando que kube-proxy decida, solicitud por solicitud, a cuál Pod enrutarla — el patrón que este proyecto usó con el Pod temporal curl-client.
Ejercicio 3 — Diseña una verificación para cinco réplicas. Si este mismo Deployment tuviera replicas: 5 en vez de 3, y quisieras confirmar que las cinco reciben tráfico, ¿cuántas solicitudes mínimas correrías en el bucle del Paso 3 para tener una probabilidad razonable de que ninguna réplica sana quede en cero, y por qué más que cinco?
Ver solución
Correr exactamente cinco solicitudes no sería suficiente para tener confianza razonable — como el reparto de kube-proxy no garantiza una distribución perfectamente uniforme (confirmado en "Errores comunes" de esta lección), con una muestra del mismo tamaño que el número de réplicas existe una probabilidad real de que, por simple azar de la secuencia de reparto, alguna réplica quede en cero solo por mala suerte estadística, no por ningún problema real. Una regla práctica razonable es correr un múltiplo claro del número de réplicas —por ejemplo, quince o veinte solicitudes para cinco réplicas (tres o cuatro veces el número de backends)— para que la probabilidad de que una réplica sana quede en cero por simple variación estadística sea baja, sin necesitar un número exageradamente grande.
Resumen y siguiente paso
Este proyecto cerró el Módulo 2 con el sistema completo funcionando junto: escalaste andes-cargo-status-api a tres réplicas con un solo cambio de número, confirmaste con evidencia real —conteos de logs por Pod, no solo la palabra "balanceo"— que las tres reciben tráfico a través de status-api-service, y repetiste, una última vez, el experimento central de este módulo: borrar un Pod a propósito, viendo al ReplicaSet reponerlo de inmediato mientras el Service seguía respondiendo sin interrupción visible. El checklist final (kubectl get all -n andes-cargo) te deja con las cuatro piezas de este módulo —Deployment, ReplicaSet, tres Pod, un Service— en una sola pantalla.
Antes de avanzar deberías poder: escalar un Deployment existente sin recrear ningún otro objeto; verificar balanceo real entre réplicas usando un Pod temporal dentro del clúster, distinguiendo esa técnica de port-forward; y reconstruir, de memoria, por qué borrar un Pod administrado nunca reduce el número de réplicas disponibles por más de un instante.
Siguiente módulo: configuración, secretos, salud y autoscaling. El Módulo 3 toma exactamente este mismo Deployment —sano, con tres réplicas, balanceado— y resuelve lo que la lección 7 de este módulo dejó honestamente pendiente: ConfigMap y Secret para que andes-cargo-status-api tenga la configuración que le falta, liveness/readiness/startup probes para que Kubernetes sepa distinguir un Pod sano de uno que solo parece estarlo, y un HorizontalPodAutoscaler que escale las réplicas por métricas reales, no por un número fijo que tú decides a mano cada vez.
Recursos
- Kubernetes — Scaling a Deployment — la guía oficial de escalar un
Deployment, la operación central del Paso 1 de este proyecto. - Kubernetes — Debug Services — referencia oficial de diagnóstico de
Service/Endpoints, la base técnica de la verificación de balanceo de este proyecto. - Kubernetes — kube-proxy — referencia oficial del componente responsable del reparto de tráfico entre réplicas, mencionado en esta lección y desarrollado a fondo en el Módulo 4.
aws-serverless-and-containers-guide(NIEVA), Módulo 7, lección 8 — elstatus-api-serviceECS-solo-documentado que este módulo terminó de reemplazar con unServicede Kubernetes real, corriendo con réplicas balanceadas.