Probes, el estado del Pod
Kubernetes no sabe nada de tu aplicación. Sabe que un proceso está vivo porque el kernel se lo dice, y ahí se acaba su intuición: un proceso vivo puede estar bloqueado, arrancando, sin conexión a la base de datos o completamente inútil. Las probes son el único canal por el que la aplicación le cuenta al clúster cómo se encuentra de verdad.
Son tres, y confundirlas es una de las mejores formas de tirar un servicio en producción:
- readiness: ¿puedo recibir tráfico? Si falla, el Pod sale de los endpoints del Service. No se le mata.
- liveness: ¿sigo funcionando? Si falla, el kubelet mata el contenedor y lo reinicia.
- startup: ¿he terminado de arrancar? Mientras no tenga éxito, las otras dos ni se evalúan.
Trabaja desde la pestaña dev-machine.
Paso 1: Un Deployment con probes de verdad
Crea el archivo probes.yaml. Trae Deployment y Service juntos, porque la gracia de la readiness solo se ve con un Service delante:
cat << 'EOF' > probes.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: nginx
image: ghcr.io/iximiuz/labs/nginx:alpine
command: ["sh", "-c"]
args:
- |
touch /tmp/listo
exec nginx -g 'daemon off;'
ports:
- containerPort: 80
readinessProbe:
exec:
command: ["cat", "/tmp/listo"]
periodSeconds: 3
failureThreshold: 2
livenessProbe:
httpGet:
path: /
port: 80
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- port: 80
targetPort: 80
EOF
El YAML, explicado con preguntas y respuestas
¿Por qué la readiness es exec y la liveness httpGet?
Porque queremos poder romper una sin romper la otra. La readiness comprueba un fichero que tú vas a borrar a mano; la liveness comprueba que nginx sigue sirviendo HTTP, cosa que seguirá haciendo. Así aislamos el efecto de cada una. En una aplicación real ambas suelen ser HTTP, pero apuntando a endpoints distintos: /readyz mira las dependencias (base de datos, cachés) y /healthz solo mira que el proceso no esté bloqueado.
¿Qué significan periodSeconds y failureThreshold?
Cada cuánto se ejecuta la comprobación y cuántos fallos seguidos hacen falta para darla por fallida. Con periodSeconds: 3 y failureThreshold: 2, un Pod tarda unos 6 segundos en salir de los endpoints. Los otros temporizadores de la familia son initialDelaySeconds (espera antes del primer intento), timeoutSeconds (cuánto se aguanta una respuesta lenta, 1 segundo por defecto, que se queda corto más veces de las que parece) y successThreshold (cuántos aciertos seguidos hacen falta para volver a darla por buena).
¿Qué pasa si no declaro ninguna probe?
Que Kubernetes usa el único criterio que tiene: el contenedor está vivo si su proceso principal no ha terminado. Ese es el comportamiento por defecto de todo lo que has desplegado hasta ahora, y explica por qué un Pod puede estar Running y READY 1/1 mientras la aplicación devuelve errores 500 a todo el mundo.
¿Los tres tipos de probe admiten los mismos mecanismos?
Sí: httpGet (200 a 399 es éxito), tcpSocket (basta con que el puerto acepte la conexión) y exec (código de salida 0). Existe además grpc para servicios que implementan el protocolo de health checking de gRPC.
Aplícalo y comprueba la foto de partida:
kubectl apply -f probes.yaml
kubectl get pods -l app=api
kubectl get endpointslice -l kubernetes.io/service-name=api
Dos Pods ready, dos endpoints en el Service.
Paso 2: Rompe la readiness de un solo Pod
Aquí está el experimento que da sentido a la lección. Elige uno de los dos Pods y bórrale el fichero que su readiness comprueba:
kubectl get pods -l app=api
Para evitar tener que escribir el Pod en los siguientes comandos, elige un Pod y guarda el nombre en la variable POD: export POD=<uno-de-los-dos-pods>
kubectl exec $POD -- rm /tmp/listo
Y ahora observa durante unos segundos:
kubectl get pods -l app=api --watch
El Pod pasa a READY 0/1, pero fíjate en la columna RESTARTS: sigue a cero. El contenedor está vivo, nginx responde perfectamente, simplemente ha dicho "ahora mismo no me mandes tráfico". Confírmalo donde importa:
kubectl get pod $POD -o jsonpath='{.status.conditions}' | jq .
Comprueba el estado de los EndpointSlice:
kubectl get endpointslice -l kubernetes.io/service-name=api -o go-template='
{{- printf "%-35s | %-15s | %-12s | %-6s | %-7s | %-11s\n" "targetRef.name" "ip address" "port info" "ready" "serving" "terminating" -}}
{{- range .items -}}
{{- range .endpoints -}}
{{- printf "%-35s | %-15s | %-12s | %-6v | %-7v | %-11v\n" .targetRef.name (index .addresses 0) (printf "%d/%s" (index (index $.items 0).ports 0).port (index (index $.items 0).ports 0).protocol) .conditions.ready .conditions.serving .conditions.terminating -}}
{{- end -}}
{{- end -}}
'
kubectlsoporta varios formatos de salida. Utiliza el que más te convenga en cada situación: https://kubernetes.io/docs/reference/kubectl/#output-options
Un solo endpoint. El Service ha dejado de enviarle peticiones al Pod que ha fallado la readiness sin matarlo, y el otro Pod absorbe todo el tráfico. Esa es la promesa de la readiness: retirarse del servicio es reversible y no cuesta un reinicio.
Devuélvelo al servicio para ver el camino de vuelta (la recuperación también es automática):
kubectl exec $POD -- touch /tmp/listo
kubectl get endpointslice -l kubernetes.io/service-name=api
💡 El error clásico: usar la misma URL para liveness y readiness cuando esa URL comprueba la base de datos. Si la base de datos se cae, la readiness saca a los Pods del Service (correcto) y la liveness los mata a todos en bucle (catastrófico). Una liveness nunca debe depender de una dependencia externa: solo del propio proceso.
Paso 3: La startup probe y los procesos lentos
Ahora el otro fallo clásico. Imagina una aplicación con un arranque largo (una JVM, una migración, una caché que se construye). Simúlalo con este Pod, que tarda 40 segundos en estar listo.
Crea lento.yaml sin startup probe, tal cual:
cat << 'EOF' > lento.yaml
apiVersion: v1
kind: Pod
metadata:
name: lento
spec:
containers:
- name: app
image: ghcr.io/iximiuz/labs/nginx:alpine
command: ["sh", "-c"]
args:
- |
sleep 40
touch /tmp/listo
exec nginx -g 'daemon off;'
livenessProbe:
exec:
command: ["cat", "/tmp/listo"]
periodSeconds: 5
failureThreshold: 1
EOF
Aplícalo y mira el desastre (no seas impaciente y espera hasta que veas un cambio de estado del Pod):
kubectl apply -f lento.yaml
kubectl get pod lento --watch
CrashLoopBackOff. La liveness empieza a comprobar a los pocos segundos, el fichero todavía no existe, y el kubelet mata el contenedor antes de que la aplicación llegue a arrancar. El Pod nunca podrá salir de ese bucle: cada intento vuelve a empezar de cero.
La tentación es subir initialDelaySeconds a 60. Es una mala solución: si aciertas por lo bajo sigues matando el arranque, y si aciertas por lo alto tardas un minuto en detectar un cuelgue real. La respuesta correcta es la startup probe.
¿Qué hace exactamente una startup probe?
Desactiva a las otras dos hasta que ella tenga éxito por primera vez. Le das un presupuesto generoso de intentos (failureThreshold por periodSeconds) y, en cuanto el proceso arranca, se apaga para siempre y la liveness toma el relevo con temporizadores agresivos. Arranque lento y detección rápida de cuelgues, sin tener que elegir.
Corrige el manifiesto añadiendo la startup probe, borra el Pod y aplícalo de nuevo (las probes, como casi todo el spec, son inmutables):
startupProbe:
exec:
command: ["cat", "/tmp/listo"]
periodSeconds: 5
failureThreshold: 20
Veinte intentos cada 5 segundos son 100 segundos de margen para un arranque de 40. La liveness se queda como está, con su failureThreshold: 1, porque una vez arrancado el proceso sí queremos matarlo rápido si se cuelga.
kubectl delete pod lento
kubectl apply -f lento.yaml
kubectl get pod lento --watch
Cuarenta segundos de paciencia y el Pod llega a Running y READY 1/1, con cero reinicios.
Resumen
- readiness: te saca del Service. No mata. Es la que protege a tus usuarios.
- liveness: te reinicia. Solo debe mirar hacia dentro, nunca hacia una dependencia externa.
- startup: silencia a las otras dos durante el arranque. Es la respuesta correcta al
CrashLoopBackOffde los procesos lentos. - Un Pod
Runningsin probes solo garantiza que un proceso existe, no que sirva para algo.
En la siguiente lección aparece la otra mitad del contrato del Pod con el clúster: los recursos, los limits y las clases de QoS. Y justo después, un challenge donde una de estas probes estará mal apuntada.
- Previous lesson
- Labels, selectors y annotations
- Next lesson
- Requests, limits y QoS Classes