Cerramos el módulo 6 con un diagnóstico incómodo: la plataforma Rutas Norte ya es potente —tiene estado, es extensible y está automatizada— pero es completamente opaca. No sabemos si api-reservas está realmente sana, ni cuánta memoria consume de verdad postgres-reservas, ni qué ocurrió anoche a las tres de la madrugada. El módulo 7 se dedica a abrir esa caja negra, y el primer paso no es medir: es enseñarle a Kubernetes a distinguir un contenedor vivo de una aplicación que funciona.

Esta lección trata de las sondas (probes): los tres mecanismos con los que Kubernetes pregunta periódicamente a cada contenedor si está vivo, si está preparado para recibir tráfico y si ha terminado de arrancar. Son la pieza que faltaba desde el módulo 2: allí dejamos dicho explícitamente que un despliegue sin corte de servicio no es posible sin sondas, y aquí saldamos esa deuda. También son, con diferencia, la funcionalidad de Kubernetes que más incidentes de producción causa cuando se configura mal: una livenessProbe demasiado agresiva puede tumbar una plataforma entera en pocos minutos. Vamos a entenderlas a fondo.

Contenido

  1. El problema: "el proceso está vivo" no significa "la aplicación funciona"
  2. Las tres sondas y qué hace Kubernetes con cada resultado
  3. Los cuatro manejadores: httpGet, tcpSocket, exec y grpc
  4. Los parámetros temporales y el cálculo exacto del tiempo de detección
  5. El error más caro del módulo: la liveness en cascada
  6. startupProbe: la solución a los arranques lentos de postgres-reservas
  7. Sondas y despliegues sin corte: cerrando la deuda de 02-04
  8. El cierre limpio: terminationGracePeriodSeconds, preStop y los 502
  9. Diseño de las sondas de cada componente de Rutas Norte
  10. Errores comunes y consejos
  11. Ejercicios

  1. El problema: "el proceso está vivo" no significa "la aplicación funciona"

Hasta ahora, la única señal de salud que Kubernetes tenía de nuestros contenedores era brutalmente simple: ¿sigue ejecutándose el proceso principal (PID 1)? Si el proceso termina, el kubelet aplica la restartPolicy que estudiamos en 02-01 y reinicia el contenedor. Si el proceso no termina, Kubernetes asume que todo va bien.

Esa suposición es falsa la mayor parte del tiempo. Veamos el caso real de api-reservas.

api-reservas es una API Node.js que mantiene un pool de conexiones contra postgres-reservas, con un máximo de 20 conexiones. Un martes por la tarde, una consulta mal indexada de la pantalla de "histórico de viajes" empieza a tardar 40 segundos. Los usuarios recargan. En dos minutos, las 20 conexiones del pool están ocupadas por consultas lentas. A partir de ahí:

  • El proceso Node.js sigue vivo: PID 1 existe, no ha lanzado ninguna excepción no capturada.
  • El servidor HTTP sigue aceptando conexiones: el socket está abierto.
  • Pero toda petición que necesite la base de datos se queda esperando un hueco en el pool y acaba dando timeout.

Desde el punto de vista de Kubernetes, el pod está perfecto. Desde el punto de vista del cliente que quiere comprar un billete Bilbao–Santander, la plataforma está caída. Y lo peor: el Service api-reservas sigue enviando tráfico a ese pod, y a los otros cuatro que están exactamente igual.

flowchart LR
    C[Cliente] --> S[Service api-reservas]
    S --> P1[Pod 1<br/>proceso vivo<br/>pool agotado]
    S --> P2[Pod 2<br/>proceso vivo<br/>pool agotado]
    S --> P3[Pod 3<br/>proceso vivo<br/>pool agotado]
    P1 -.timeout.-> DB[(postgres-reservas)]
    P2 -.timeout.-> DB
    P3 -.timeout.-> DB

Las sondas existen precisamente para cerrar esta brecha. Le dan a la aplicación la oportunidad de responder por sí misma a dos preguntas distintas:

  • ¿Estoy tan rota que lo mejor es que me reinicies?livenessProbe.
  • ¿Puedo atender peticiones ahora mismo?readinessProbe.

Son preguntas diferentes, con consecuencias diferentes, y confundirlas es el origen de casi todos los desastres de esta lección.

  1. Las tres sondas y qué hace Kubernetes con cada resultado

Kubernetes define tres sondas por contenedor. Todas son opcionales y todas se declaran dentro de la especificación del contenedor, no del pod.

livenessProbe — ¿sigues vivo?

Comprueba si el contenedor está en un estado del que ya no puede recuperarse solo. Si falla el número de veces configurado, el kubelet mata el contenedor y aplica la restartPolicy (normalmente Always, así que lo reinicia). El pod no se recrea: es el mismo pod, con el mismo nombre y la misma IP, con su contador RESTARTS incrementado.

Casos legítimos: un interbloqueo (deadlock) del que el proceso no sale, un bucle infinito que no responde, una fuga de memoria conocida sin arreglo a corto plazo. Si tu aplicación no tiene ningún estado del que solo se salga reiniciando, es perfectamente válido no poner livenessProbe.

readinessProbe — ¿puedes atender tráfico?

Comprueba si el contenedor puede servir peticiones ahora mismo. Si falla, Kubernetes no reinicia nada: simplemente marca el pod como no listo (Ready=False) y el controlador de endpoints retira su dirección IP del EndpointSlice del Service. Deja de llegarle tráfico, pero sigue vivo. Cuando la sonda vuelve a pasar, la IP se reincorpora automáticamente.

Este es el mecanismo correcto para el caso del pool agotado: el pod se aparta un momento, deja de recibir peticiones que no puede atender, drena sus consultas lentas y vuelve.

startupProbe — ¿has terminado de arrancar?

Es una sonda de arranque. Mientras se está ejecutando y no ha pasado, las otras dos sondas quedan deshabilitadas. Cuando pasa por primera vez, deja de ejecutarse para siempre y liveness y readiness entran en juego. Si nunca pasa dentro de su presupuesto de tiempo, el contenedor se mata y se reinicia.

Sirve para aplicaciones con arranques lentos o de duración muy variable, donde poner un initialDelaySeconds enorme en la liveness haría que la detección de fallos reales fuese lentísima el resto de la vida del contenedor.

Tabla comparativa

Aspecto livenessProbe readinessProbe startupProbe
Pregunta que responde ¿Debo reiniciarte? ¿Te mando tráfico? ¿Has acabado de arrancar?
Acción al fallar Mata y reinicia el contenedor Quita la IP de los Endpoints Mata y reinicia el contenedor
Acción al pasar Nada (sigue vigilando) Devuelve la IP a los Endpoints Se desactiva para siempre
¿Cuándo se ejecuta? Toda la vida del contenedor Toda la vida del contenedor Solo hasta el primer éxito
¿Puede comprobar dependencias externas? Nunca Sí, y debe Solo lo mínimo para arrancar
¿Afecta al Service? No directamente Sí, directamente Indirectamente (bloquea readiness)
¿Es obligatoria? No Prácticamente sí, si hay Service Solo si el arranque es lento
Riesgo de mal uso Muy alto (reinicios en cascada) Bajo Bajo

Una regla que conviene memorizar: la liveness protege al contenedor de sí mismo; la readiness protege a los clientes del contenedor.

  1. Los cuatro manejadores: httpGet, tcpSocket, exec y grpc

Cada sonda usa exactamente uno de estos cuatro mecanismos para hacer la comprobación.

httpGet

El más habitual y el más recomendable para servicios HTTP. El kubelet hace una petición GET a la IP del pod, en el puerto y ruta indicados.

livenessProbe:
  httpGet:
    path: /salud                 # ruta que atenderá api-reservas
    port: 8080                   # número o nombre de puerto declarado en ports
    scheme: HTTP                 # HTTP (por defecto) o HTTPS
    httpHeaders:
      - name: X-Origen-Sonda
        value: kubelet

Puntos importantes para principiantes:

  • Se considera éxito cualquier código de respuesta entre 200 y 399, ambos incluidos. Un 204 es éxito. Un 301 también, y esto sorprende: si tu aplicación redirige /salud a /login, la sonda pasará aunque la aplicación esté rota. Un 400, 404, 500 o 503 es fallo.
  • La petición la hace el kubelet del nodo, no otro pod. Por eso las NetworkPolicies de rutas-norte-pro no la bloquean: no pasa por la red de pods normal.
  • El campo port admite el nombre de un puerto declarado en ports, lo cual es mucho más legible y resistente a cambios.
  • Las cabeceras httpHeaders sirven para dos cosas muy prácticas: identificar en los logs de la aplicación qué peticiones vienen del kubelet (y así poder excluirlas de las métricas de tráfico) y pasar cabeceras que la aplicación exija, como un Host concreto.
  • El kubelet no envía cookies ni sigue autenticación: el endpoint de salud debe ser accesible sin credenciales desde dentro del pod.

tcpSocket

El kubelet intenta abrir una conexión TCP al puerto indicado. Si el handshake se completa, es éxito; si la conexión se rechaza o expira, es fallo.

readinessProbe:
  tcpSocket:
    port: 5432

Es la opción para servicios que no hablan HTTP, como redis-cache o el propio PostgreSQL a bajo nivel. Su gran limitación: solo comprueba que hay algo escuchando. PostgreSQL puede tener el puerto abierto y estar rechazando conexiones por haber alcanzado max_connections, o estar en modo recuperación. La sonda TCP pasaría igualmente.

exec

El kubelet ejecuta un comando dentro del contenedor. Éxito si el código de salida es 0, fallo en cualquier otro caso.

readinessProbe:
  exec:
    command:
      - /bin/sh
      - -c
      - pg_isready -U rutasnorte -d reservas -h 127.0.0.1

Es el más flexible y el más caro. Cada ejecución implica crear un proceso nuevo dentro del contenedor: consume CPU y memoria del pod (contabilizados contra sus limits, lo cual puede provocar un OOMKilled si vas justo) y añade carga al runtime del nodo. Con periodSeconds: 5 en 60 pods estás lanzando 12 procesos por segundo en el clúster solo para preguntar cómo están.

Consejos:

  • Usa exec solo cuando no haya alternativa HTTP o TCP.
  • Sube el periodSeconds (10-30 s) respecto a lo que usarías con httpGet.
  • El binario debe existir en la imagen. Es un error habitual escribir una sonda con curl en una imagen distroless que no lo tiene: la sonda falla siempre y el contenedor entra en CrashLoopBackOff.

grpc

Desde Kubernetes 1.24 es una funcionalidad estable. El kubelet actúa como cliente del protocolo estándar de health checking de gRPC.

livenessProbe:
  grpc:
    port: 9000
    service: reservas.Disponibilidad   # opcional: nombre del servicio a consultar

La aplicación debe implementar el servicio grpc.health.v1.Health. Es éxito si responde SERVING. Evita tener que instalar grpc_health_probe como binario en la imagen, que era la solución anterior con exec.

Comparativa de manejadores

Manejador Cuándo usarlo Coste Riesgo principal
httpGet Servicios HTTP/REST (api-reservas, tienda-web) Muy bajo Los 3xx cuentan como éxito
tcpSocket Servicios TCP sin HTTP (redis-cache) Muy bajo Superficial: solo mira el socket
exec Comprobaciones que exigen lógica local (pg_isready) Alto Consumo de recursos y binarios ausentes
grpc Servicios gRPC Bajo Requiere implementar el servicio de salud

  1. Los parámetros temporales y el cálculo exacto del tiempo de detección

Las cinco variables temporales son idénticas para las tres sondas. Entenderlas de verdad significa poder responder a la pregunta "¿cuánto tarda Kubernetes en darse cuenta de que este contenedor está muerto?" con un número, no con una intuición.

Parámetro Por defecto Mínimo Significado
initialDelaySeconds 0 0 Segundos de espera desde que arranca el contenedor hasta la primera comprobación
periodSeconds 10 1 Cada cuántos segundos se repite la comprobación
timeoutSeconds 1 1 Segundos que se espera la respuesta antes de contarla como fallo
successThreshold 1 1 Éxitos consecutivos para pasar de fallo a éxito
failureThreshold 3 1 Fallos consecutivos para dar la sonda por fallida

Dos matices que se olvidan siempre:

  • successThreshold debe valer 1 obligatoriamente en livenessProbe y startupProbe. Solo la readinessProbe admite valores mayores. Tiene sentido: no puedes "reiniciar a medias".
  • timeoutSeconds: 1 (el valor por defecto) es peligrosamente bajo para una aplicación real bajo carga. Un endpoint de salud que normalmente tarda 20 ms puede tardar 1,5 s cuando el nodo está saturado, y entonces la sonda falla por timeout aunque la aplicación esté bien.

El cálculo

El tiempo máximo desde que un contenedor deja de responder hasta que el kubelet lo mata es:

T_deteccion = initialDelaySeconds (solo la primera vez)
            + (failureThreshold - 1) * periodSeconds
            + timeoutSeconds

Más un margen de hasta periodSeconds porque el fallo puede ocurrir justo después de una comprobación exitosa. En la práctica se usa la fórmula pesimista:

T_peor = failureThreshold * periodSeconds + timeoutSeconds

Ejemplo con la configuración que usaremos en api-reservas:

livenessProbe:
  httpGet:
    path: /salud
    port: http
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3
T_peor = 3 * 10 + 3 = 33 segundos

Es decir: si api-reservas se cuelga, pasarán hasta 33 segundos hasta que el kubelet lo reinicie, y después habrá que sumar el tiempo de arranque del contenedor. Si eso es inaceptable para el negocio, bajas periodSeconds a 5 y failureThreshold a 3 → 18 segundos. Pero cuanto más agresiva la sonda, mayor el riesgo de falsos positivos, y ese riesgo es el tema del apartado siguiente.

Y para la readiness, el mismo cálculo determina cuánto tiempo seguirá llegando tráfico a un pod que ya no puede atenderlo:

readinessProbe:
  httpGet:
    path: /preparado
    port: http
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 2
T_peor = 2 * 5 + 2 = 12 segundos de tráfico enviado a un pod roto

Regla práctica: la readiness debe ser más rápida y más sensible que la liveness. Apartar un pod es una acción barata y reversible; reiniciarlo, no.

  1. El error más caro del módulo: la liveness en cascada

Este es el apartado que hay que leer dos veces. Es el fallo que más veces ha tumbado plataformas enteras de producción, y siempre por el mismo motivo.

El escenario

Un ingeniero de Rutas Norte, con la mejor intención, configura así la liveness de api-reservas:

# INCORRECTO — no copiéis esto
livenessProbe:
  httpGet:
    path: /salud/completo    # comprueba API + PostgreSQL + Redis + pasarela de pagos
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 1
  failureThreshold: 2

El endpoint /salud/completo hace un SELECT 1 contra postgres-reservas, un PING a redis-cache y una llamada a pagos.proveedorexterno.example. Parece completísimo. Es una bomba.

Llega un puente de mayo. El tráfico se multiplica por seis. postgres-reservas empieza a responder en 1,2 s en lugar de en 5 ms. Entonces:

  1. La sonda de salud hace timeout (1 s) en los diez pods de api-reservas a la vez, porque todos dependen de la misma base de datos.
  2. A los 10 segundos (2 * 5), el kubelet mata los diez contenedores simultáneamente.
  3. Los diez arrancan de nuevo. Al arrancar, cada uno abre su pool de 20 conexiones contra PostgreSQL: 200 conexiones nuevas de golpe contra una base de datos que ya iba ahogada.
  4. PostgreSQL se satura aún más. Las sondas vuelven a fallar. Vuelta al paso 2.
  5. Kubernetes aplica backoff exponencial a los reinicios, así que los pods entran en CrashLoopBackOff. La plataforma pasa de lenta a totalmente caída.
flowchart TD
    A[Pico de tráfico] --> B[postgres-reservas responde lento]
    B --> C[La liveness comprueba PostgreSQL y hace timeout]
    C --> D[kubelet mata los 10 pods de api-reservas]
    D --> E[10 pods arrancan a la vez y abren 200 conexiones]
    E --> B
    D --> F[CrashLoopBackOff: caída total]

Fíjate en lo esencial: la base de datos estaba lenta, no caída. Sin sondas, la plataforma habría ido lenta durante veinte minutos y se habría recuperado sola. Con esa liveness, se cayó del todo. La sonda amplificó el fallo en lugar de mitigarlo.

Las dos reglas de oro

Regla 1: la livenessProbe NUNCA debe comprobar dependencias externas. Ni bases de datos, ni cachés, ni APIs de terceros, ni otros microservicios. Solo debe responder a "¿este proceso, aislado, sigue siendo capaz de procesar una petición?".

Regla 2: la readinessProbe SÍ debe comprobar las dependencias que necesita para servir. Si api-reservas no puede hablar con PostgreSQL, no puede servir reservas, y lo correcto es que salga de los Endpoints hasta que pueda.

¿Y qué pasa si la readiness falla en los diez pods a la vez? Que el Service se queda sin Endpoints y el Ingress devuelve 503. Es malo, pero es honesto y reversible: en cuanto PostgreSQL se recupera, los diez pods vuelven a estar listos en cinco segundos, sin arranques en frío, sin avalancha de conexiones, sin CrashLoopBackOff. La diferencia entre una degradación de servicio y un desastre.

Por eso api-reservas tendrá dos endpoints distintos:

Endpoint Lo usa Qué comprueba Qué NO comprueba
/salud livenessProbe El bucle de eventos de Node responde; no hay deadlock; hay memoria para responder PostgreSQL, Redis, pasarela de pagos
/preparado readinessProbe Todo lo anterior más una conexión libre en el pool de PostgreSQL y PING a redis-cache La pasarela de pagos externa (ver nota)

Nota sobre la pasarela de pagos: pagos.proveedorexterno.example es un tercero fuera de nuestro control. Si lo metemos en la readiness, una caída del proveedor deja toda la plataforma sin Endpoints, incluida la consulta de horarios, que no necesita pagos. La decisión correcta es no incluirlo en ninguna sonda y gestionarlo con un circuit breaker dentro de la aplicación que devuelva un error concreto solo en el flujo de pago.

  1. startupProbe: la solución a los arranques lentos de postgres-reservas

postgres-reservas, tras el crecimiento del último año, tarda entre 20 y 180 segundos en aceptar conexiones cuando arranca: si el cierre anterior fue sucio, debe reproducir el WAL, y ese tiempo depende del volumen pendiente.

Sin startupProbe, tenemos dos malas opciones:

  • Opción A: initialDelaySeconds: 200 en la liveness. Cubre el peor caso, pero durante los 200 primeros segundos de cada reinicio nadie vigila el contenedor. Y peor: no puedes bajar el periodo de detección después.
  • Opción B: failureThreshold: 40 con periodSeconds: 5. Cubre el arranque, pero también significa que, ya en régimen, un cuelgue tardará 40 * 5 = 200 segundos en detectarse.

La startupProbe separa los dos presupuestos de tiempo: uno generoso para arrancar, otro estricto para la vida en régimen.

# Fragmento del contenedor postgres del StatefulSet postgres-reservas
startupProbe:
  exec:
    command: ["/bin/sh", "-c", "pg_isready -U rutasnorte -h 127.0.0.1"]
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 30        # 30 * 10 = 300 s de presupuesto de arranque
livenessProbe:
  exec:
    command: ["/bin/sh", "-c", "pg_isready -U rutasnorte -h 127.0.0.1"]
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3         # ya en régimen: 35 s para detectar un cuelgue
readinessProbe:
  exec:
    command: ["/bin/sh", "-c", "pg_isready -U rutasnorte -h 127.0.0.1 && psql -U rutasnorte -d reservas -c 'SELECT 1' -tA"]
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 2

Cómo se comporta esto en el tiempo:

sequenceDiagram
    participant K as kubelet
    participant C as contenedor postgres
    K->>C: t=10s startupProbe → fallo (recuperando WAL)
    K->>C: t=20s startupProbe → fallo
    Note over K: liveness y readiness NO se ejecutan
    K->>C: t=90s startupProbe → ÉXITO
    Note over K: startupProbe se desactiva para siempre
    K->>C: t=100s livenessProbe → éxito
    K->>C: t=100s readinessProbe → éxito
    Note over K: el pod entra en los Endpoints del Service headless

Si a los 300 segundos la startupProbe sigue fallando, el contenedor se mata y se reinicia. Es el comportamiento correcto: algo va realmente mal.

Detalle importante para el exec: pg_isready y psql existen en la imagen postgres:16.4, y nos conectamos a 127.0.0.1 (dentro del propio pod), no al Service. Comprobar el Service desde la sonda de un pod sería comprobar una dependencia externa, con todos los problemas del apartado 5.

  1. Sondas y despliegues sin corte: cerrando la deuda de 02-04

En 02-04 vimos RollingUpdate con maxSurge y maxUnavailable, y dejamos escrito que aquello no era todavía un despliegue sin corte. Ahora se entiende por qué.

El controlador de Deployment considera que un pod nuevo "está bien" cuando el pod está Ready. Sin readinessProbe, un pod se considera Ready en cuanto sus contenedores arrancan. Para api-reservas, eso ocurre unos 400 ms después de lanzar node, mucho antes de que Express escuche en el 8080 y de que el pool de conexiones esté abierto.

Secuencia del desastre silencioso:

  1. kubectl apply con la nueva imagen.
  2. Se crea el pod nuevo. A los 400 ms está Running y, sin readiness, Ready.
  3. El controlador de endpoints añade su IP al EndpointSlice: empieza a recibir tráfico real.
  4. El Deployment, al ver un pod nuevo listo, termina un pod antiguo.
  5. Durante los siguientes 3-8 segundos, el pod nuevo devuelve ECONNREFUSED a todo el tráfico que le llega.
  6. Se repite pod a pod. El resultado: unos segundos de errores por cada réplica sustituida, invisibles en kubectl get pods, muy visibles en la tasa de errores.

Con readinessProbe, el paso 2 cambia: el pod está Running pero Ready=False, no entra en los Endpoints y el Deployment no avanza hasta que pase la sonda. Ese es, literalmente, el mecanismo del despliegue sin corte.

minReadySeconds

Hay un caso residual: aplicaciones que pasan la readiness y se caen tres segundos después (por ejemplo, al recibir la primera petición real que toca un código nuevo defectuoso). minReadySeconds obliga al Deployment a esperar N segundos con el pod continuamente listo antes de darlo por bueno y seguir sustituyendo pods.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reservas
  namespace: rutas-norte-pro
  labels:
    app: api-reservas
    app.kubernetes.io/part-of: rutas-norte
    entorno: pro
spec:
  replicas: 6
  minReadySeconds: 15            # 15 s listo de forma continuada antes de avanzar
  progressDeadlineSeconds: 300   # si en 5 min no progresa, se marca como fallido
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 0          # nunca menos de 6 pods sirviendo
  selector:
    matchLabels:
      app: api-reservas
      entorno: pro
  template:
    metadata:
      labels:
        app: api-reservas
        app.kubernetes.io/part-of: rutas-norte
        entorno: pro
    spec:
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reservas:2.8.1
          ports:
            - name: http
              containerPort: 8080

Con maxUnavailable: 0 + readinessProbe + minReadySeconds: 15, un despliegue de api-reservas es genuinamente sin corte. El precio: tarda más. Un despliegue de 6 réplicas pasa de 40 segundos a unos 3 minutos. Es un precio razonable.

Un aviso sobre progressDeadlineSeconds: si la readiness nunca pasa (por ejemplo, porque la nueva versión tiene un error de configuración), el Deployment se queda atascado pero los pods viejos siguen sirviendo. A los 300 segundos el Deployment se marca como ProgressDeadlineExceeded, lo que permite detectarlo automáticamente. No hace rollback solo: eso lo decides tú con kubectl rollout undo.

  1. El cierre limpio: terminationGracePeriodSeconds, preStop y los 502

Hemos resuelto la entrada de pods nuevos. Queda la salida de los viejos, que es donde nacen los 502 que Rutas Norte ve en cada despliegue.

La carrera

Cuando Kubernetes decide terminar un pod, ocurren dos cosas en paralelo, sin coordinación entre ellas:

sequenceDiagram
    participant API as API Server
    participant EPC as Controlador de Endpoints
    participant KP as kube-proxy / Ingress
    participant KL as kubelet
    participant P as Pod api-reservas
    API->>EPC: pod marcado para borrado
    API->>KL: pod marcado para borrado
    par Camino A (red)
        EPC->>EPC: quita la IP del EndpointSlice
        EPC->>KP: propaga la regla actualizada
        KP->>KP: reprograma iptables/IPVS (100-2000 ms)
    and Camino B (proceso)
        KL->>P: SIGTERM inmediato
    end
    Note over P,KP: el proceso muere ANTES de que dejen de mandarle tráfico → 502

El camino B (matar el proceso) es casi instantáneo. El camino A (propagar la retirada del endpoint a todos los nodos y a los controladores de Ingress) tarda desde unos cientos de milisegundos hasta un par de segundos en un clúster con carga. En esa ventana, el balanceador sigue enviando peticiones a un proceso que ya está cerrando. Cada una de esas peticiones es un 502 para un cliente de Rutas Norte.

La solución: preStop

El hook preStop se ejecuta antes del SIGTERM y el kubelet espera a que termine antes de enviarlo. Insertar ahí una pausa da tiempo al camino A.

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 10"]

Parece un truco sucio, y en cierto modo lo es, pero es la solución recomendada por la propia documentación de Kubernetes y la que usa prácticamente todo el mundo en producción. Durante esos 10 segundos:

  • El contenedor sigue atendiendo peticiones con normalidad (nadie le ha dicho que pare).
  • El controlador de endpoints ya lo ha retirado, así que deja de llegarle tráfico nuevo.
  • Las peticiones en vuelo terminan tranquilamente.

Si tu aplicación tiene un endpoint que le hace fallar la readiness voluntariamente, una variante más elegante es llamarlo y luego esperar. Pero el sleep funciona y no requiere tocar la aplicación.

El presupuesto completo de terminación

spec:
  terminationGracePeriodSeconds: 45
  containers:
    - name: api
      lifecycle:
        preStop:
          exec:
            command: ["/bin/sh", "-c", "sleep 10"]

Cronología real:

Momento Qué ocurre
t=0 s El pod pasa a Terminating. Arranca el preStop y el reloj de terminationGracePeriodSeconds
t=0 a 2 s El endpoint se retira de todos los nodos y del Ingress
t=0 a 10 s preStop durmiendo. El contenedor sirve las peticiones en vuelo
t=10 s Termina preStop. El kubelet envía SIGTERM
t=10 a 45 s La aplicación cierra el servidor HTTP, drena el pool de PostgreSQL, hace commit de lo pendiente y sale
t=45 s Si el proceso sigue vivo, el kubelet envía SIGKILL. Sin piedad

Punto crítico que se olvida siempre: el tiempo del preStop se descuenta del periodo de gracia, no se suma. Con terminationGracePeriodSeconds: 45 y un preStop de 10 s, la aplicación solo tiene 35 segundos para cerrar tras el SIGTERM. Ajusta el número teniendo esto en cuenta.

Y como vimos en 02-01, si tu proceso arranca vía shell (CMD npm start), el SIGTERM puede llegarle al shell y no a Node. Usa la forma exec (CMD ["node", "server.js"]) o shareProcessNamespace/tini para asegurarte de que la señal llega a quien debe.

Para worker-notificaciones, que procesa correos de confirmación, el periodo de gracia debe cubrir el envío en curso más largo: fijamos terminationGracePeriodSeconds: 90 y la aplicación deja de tomar mensajes nuevos de la cola al recibir el SIGTERM.

  1. Diseño de las sondas de cada componente de Rutas Norte

Aplicamos todo lo anterior componente a componente. La tabla de diseño primero, los manifiestos después.

Componente Liveness Readiness Startup Notas
tienda-web (nginx) httpGet / puerto 80 httpGet / puerto 80 No Estático y rápido; ambas iguales es aceptable
api-reservas httpGet /salud httpGet /preparado httpGet /salud, 60 s El caso de referencia de la lección
postgres-reservas exec pg_isready local exec pg_isready + SELECT 1 exec pg_isready, 300 s Arranque variable por el WAL
redis-cache exec redis-cli PING exec redis-cli PING No Arranque rapidísimo
worker-notificaciones httpGet /salud puerto 8081 Ninguna No No tiene Service: la readiness no aporta nada
informes-ocupacion (CronJob) Ninguna Ninguna No Job de vida corta: el éxito es el código de salida

Dos decisiones que merecen explicación:

  • worker-notificaciones sin readiness: la readiness solo tiene sentido si algo consulta los Endpoints. Este worker no está detrás de ningún Service; consume de una cola. Poner readiness no haría nada. Sí tiene liveness, con un pequeño servidor HTTP interno que responde 200 mientras el bucle de consumo esté activo.
  • CronJob sin sondas: un pod que vive 4 minutos y cuyo éxito se mide por su código de salida no gana nada con sondas. En su lugar, en 07-04 alertaremos sobre kube_job_status_failed.

Manifiesto completo de api-reservas

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reservas
  namespace: rutas-norte-pro
  labels:
    app: api-reservas
    app.kubernetes.io/part-of: rutas-norte
    entorno: pro
spec:
  replicas: 6
  minReadySeconds: 15
  progressDeadlineSeconds: 300
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 0
  selector:
    matchLabels:
      app: api-reservas
      entorno: pro
  template:
    metadata:
      labels:
        app: api-reservas
        app.kubernetes.io/part-of: rutas-norte
        entorno: pro
    spec:
      terminationGracePeriodSeconds: 45
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reservas:2.8.1
          ports:
            - name: http
              containerPort: 8080
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "1"
              memory: "512Mi"

          # ---- ARRANQUE: presupuesto generoso, sin vigilancia todavía ----
          startupProbe:
            httpGet:
              path: /salud
              port: http
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12      # 12 * 5 = 60 s para arrancar

          # ---- VIVACIDAD: solo el proceso, NUNCA dependencias externas ----
          livenessProbe:
            httpGet:
              path: /salud
              port: http
              httpHeaders:
                - name: X-Origen-Sonda
                  value: kubelet-liveness
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3       # peor caso: 33 s hasta el reinicio

          # ---- PREPARACIÓN: sí comprueba PostgreSQL y Redis ----
          readinessProbe:
            httpGet:
              path: /preparado
              port: http
              httpHeaders:
                - name: X-Origen-Sonda
                  value: kubelet-readiness
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2       # peor caso: 12 s fuera de Endpoints
            successThreshold: 1

          # ---- CIERRE LIMPIO: evitar los 502 del despliegue ----
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]

Qué debe hacer cada endpoint por dentro

Pseudocódigo del /salud (liveness). Barato, local, sin red saliente:

// GET /salud  -> usado por livenessProbe
// Responde 200 si el proceso puede procesar peticiones. NADA de dependencias.
app.get('/salud', (req, res) => {
  const retrasoBucleMs = medirRetrasoDelBucleDeEventos();  // p. ej. con perf_hooks
  if (retrasoBucleMs > 5000) {
    // El bucle de eventos lleva 5 s bloqueado: el proceso no se recupera solo
    return res.status(503).json({ estado: 'bloqueado', retrasoBucleMs });
  }
  return res.status(200).json({ estado: 'vivo', version: process.env.APP_VERSION });
});

Pseudocódigo del /preparado (readiness). Comprueba dependencias, con timeouts cortos:

// GET /preparado -> usado por readinessProbe
// Responde 200 solo si podemos servir una reserva de verdad AHORA.
app.get('/preparado', async (req, res) => {
  const comprobaciones = {};
  try {
    // 1. ¿Queda alguna conexión libre en el pool? (el caso del apartado 1)
    comprobaciones.poolLibre = poolPg.idleCount > 0 || poolPg.totalCount < poolPg.options.max;
    if (!comprobaciones.poolLibre) throw new Error('pool de conexiones agotado');

    // 2. ¿PostgreSQL responde en menos de 1 s?
    await poolPg.query({ text: 'SELECT 1', timeout: 1000 });
    comprobaciones.postgres = 'ok';

    // 3. ¿Redis responde? Degradación aceptable: sin caché servimos igual, más lento
    try {
      await redis.ping();
      comprobaciones.redis = 'ok';
    } catch (e) {
      comprobaciones.redis = 'degradado';   // NO impide estar preparado
    }

    // OJO: no comprobamos pagos.proveedorexterno.example a propósito
    return res.status(200).json({ estado: 'preparado', comprobaciones });
  } catch (err) {
    return res.status(503).json({ estado: 'no-preparado', motivo: err.message, comprobaciones });
  }
});

postgres-reservas y redis-cache

# Fragmento del StatefulSet postgres-reservas (contenedor principal)
containers:
  - name: postgres
    image: postgres:16.4
    ports:
      - name: pg
        containerPort: 5432
    startupProbe:
      exec:
        command: ["/bin/sh", "-c", "pg_isready -U rutasnorte -h 127.0.0.1"]
      periodSeconds: 10
      timeoutSeconds: 5
      failureThreshold: 30       # 300 s: cubre la recuperación del WAL
    livenessProbe:
      exec:
        command: ["/bin/sh", "-c", "pg_isready -U rutasnorte -h 127.0.0.1"]
      periodSeconds: 15
      timeoutSeconds: 5
      failureThreshold: 3
    readinessProbe:
      exec:
        command:
          - /bin/sh
          - -c
          - pg_isready -U rutasnorte -h 127.0.0.1 && psql -U rutasnorte -d reservas -tAc 'SELECT 1'
      periodSeconds: 10
      timeoutSeconds: 5
      failureThreshold: 2
# Fragmento del StatefulSet redis-cache
containers:
  - name: redis
    image: redis:7.4-alpine
    ports:
      - name: redis
        containerPort: 6379
    livenessProbe:
      exec:
        command: ["redis-cli", "ping"]
      periodSeconds: 15
      timeoutSeconds: 3
      failureThreshold: 3
    readinessProbe:
      exec:
        command: ["redis-cli", "ping"]
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 2

Verificación en el clúster

# Estado de preparación de todos los pods del namespace
kubectl -n rutas-norte-pro get pods -l app.kubernetes.io/part-of=rutas-norte

# Ver los eventos de sondas fallidas de un pod concreto
kubectl -n rutas-norte-pro describe pod api-reservas-7d9f8c4b5-x2klm | grep -A 20 Events

# Ver la condición Ready y su motivo exacto
kubectl -n rutas-norte-pro get pod api-reservas-7d9f8c4b5-x2klm \
  -o jsonpath='{.status.conditions[?(@.type=="Ready")]}' | jq

# Comprobar qué IPs están realmente en los Endpoints (la verdad del Service)
kubectl -n rutas-norte-pro get endpointslices -l kubernetes.io/service-name=api-reservas -o yaml

Salida típica de un pod cuya readiness falla:

NAME                            READY   STATUS    RESTARTS   AGE
api-reservas-7d9f8c4b5-x2klm    0/1     Running   0          4m12s

Presta atención: STATUS es Running y RESTARTS es 0. Todo "parece" bien. La única señal es el 0/1 de la columna READY. Y en los eventos:

Events:
  Type     Reason     Age                  From     Message
  ----     ------     ----                 ----     -------
  Warning  Unhealthy  2m (x24 over 4m)     kubelet  Readiness probe failed: HTTP probe failed with statuscode: 503

Esa es la traza que hay que buscar. En 07-06 sistematizaremos esta forma de leer eventos.

Errores Comunes y Consejos

1. Poner la misma sonda en liveness y readiness comprobando dependencias. Es el error del apartado 5 disfrazado. Si copias el mismo httpGet /salud/completo en las dos, has creado el bucle de reinicios en cascada. Endpoints distintos, siempre.

2. timeoutSeconds: 1 (el valor por defecto). Bajo carga, un endpoint sano tarda más de un segundo con facilidad. Sube a 2-3 s en readiness y 3-5 s en liveness. Este parámetro por defecto ha causado más falsos positivos que ningún otro.

3. Olvidar la startupProbe en aplicaciones lentas. El síntoma clásico: CrashLoopBackOff en un contenedor que "en local arranca bien". Lo que ocurre es que la liveness lo mata a los 30 segundos y nunca llega a terminar de arrancar. Si ves reinicios cíclicos sin logs de error de la aplicación, sospecha de esto lo primero.

4. Sonda exec con un binario que no existe. curl, wget o nc no están en imágenes distroless ni en muchas alpine minimalistas. Comprueba con kubectl exec -it <pod> -- which curl antes de escribir la sonda. El evento delator es Liveness probe errored: exec: "curl": executable file not found in $PATH.

5. Endpoint de salud que exige autenticación. El kubelet no envía credenciales. Un /salud protegido por JWT devuelve 401 y la sonda falla siempre. Deja el endpoint de salud abierto, sin datos sensibles en la respuesta, y protégelo por red si hace falta.

6. Confiar en los códigos 3xx. Recuerda: 200-399 es éxito. Un /salud que redirige a / seguirá "pasando" aunque la aplicación esté completamente rota. Devuelve 200 explícito.

7. readinessProbe sin preStop. Tienes despliegues sin corte a la entrada pero sigues generando 502 a la salida. Los dos mecanismos son complementarios y hacen falta los dos.

8. Sondas demasiado caras. Un /preparado que hace SELECT count(*) FROM reservas está lanzando una consulta pesada cada 5 segundos por cada réplica. Con 6 réplicas son 72 consultas por minuto solo para preguntar cómo estamos. Las comprobaciones deben ser triviales: SELECT 1 y poco más.

9. Usar sondas para lo que sirve un PodDisruptionBudget. Las sondas no protegen de un drenaje de nodo ni de una actualización del clúster. Eso es 09-05.

10. Un preStop más largo que el periodo de gracia. Si pones preStop: sleep 60 con terminationGracePeriodSeconds: 30, a los 30 segundos llega el SIGKILL y la aplicación nunca recibe el SIGTERM: no cierra nada limpiamente. El preStop siempre debe ser bastante menor que el periodo de gracia.

Ejercicios

Ejercicio 1 — Calcular y ajustar el presupuesto de detección

Un compañero ha configurado así la liveness de worker-notificaciones:

livenessProbe:
  httpGet:
    path: /salud
    port: 8081
  initialDelaySeconds: 20
  periodSeconds: 30
  timeoutSeconds: 1
  failureThreshold: 5

Responde:

  1. ¿Cuánto tarda, en el peor caso, en detectarse que el worker se ha colgado?
  2. El negocio exige que un worker colgado se reinicie en menos de 60 segundos. Propón una configuración que lo cumpla sin volverla propensa a falsos positivos.
  3. El worker tarda 8 segundos en arrancar y conectarse a la cola. ¿Es correcto el initialDelaySeconds: 20? ¿Qué alternativa mejor existe?

Ejercicio 2 — Detectar y arreglar una liveness peligrosa

Este es el manifiesto real de tienda-web en rutas-norte-pre. Contiene tres problemas relacionados con las sondas y el cierre. Identifícalos y escribe el manifiesto corregido.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tienda-web
  namespace: rutas-norte-pre
  labels:
    app: tienda-web
    entorno: pre
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  selector:
    matchLabels:
      app: tienda-web
      entorno: pre
  template:
    metadata:
      labels:
        app: tienda-web
        entorno: pre
    spec:
      containers:
        - name: nginx
          image: registry.rutasnorte.example/tienda-web:5.2.0
          ports:
            - containerPort: 80
          livenessProbe:
            httpGet:
              path: /api/estado-completo   # proxy a api-reservas y a PostgreSQL
              port: 80
            periodSeconds: 5
            timeoutSeconds: 1
            failureThreshold: 2

Ejercicio 3 — Diseñar las sondas de un componente nuevo

Rutas Norte añade un componente: buscador-rutas, un servicio Java (Spring Boot) que mantiene en memoria un índice de todas las rutas y horarios. Datos:

  • Al arrancar carga el índice completo desde postgres-reservas: entre 45 y 150 segundos según el volumen.
  • Una vez cargado el índice, no vuelve a tocar PostgreSQL: sirve todo desde memoria.
  • Expone /actuator/health/liveness y /actuator/health/readiness (Spring Boot Actuator).
  • Está detrás de un Service buscador-rutas que consulta tienda-web.
  • Ocasionalmente sufre pausas largas del recolector de basura (hasta 4 segundos).

Escribe el bloque de sondas completo y justifica cada valor.

Soluciones

Solución 1

1. Tiempo de detección.

T_peor = failureThreshold * periodSeconds + timeoutSeconds
       = 5 * 30 + 1
       = 151 segundos

Más de dos minutos y medio con el worker colgado y sin enviar ni un correo de confirmación. Muy por encima del requisito de 60 segundos.

2. Configuración propuesta.

livenessProbe:
  httpGet:
    path: /salud
    port: 8081
  periodSeconds: 10
  timeoutSeconds: 3      # 1 s era demasiado ajustado bajo carga
  failureThreshold: 4
T_peor = 4 * 10 + 3 = 43 segundos  ✅ cumple el requisito de < 60 s

Se ha bajado el periodo de 30 a 10 s (detección más rápida) y subido el timeoutSeconds de 1 a 3 s (menos falsos positivos). El failureThreshold: 4 da margen para que un pico puntual no dispare un reinicio: hacen falta cuatro fallos consecutivos, es decir 40 segundos de problema sostenido.

3. El initialDelaySeconds.

Es una solución tosca. Con 20 s fijos: si un día el arranque tarda 25 s por lentitud del nodo, la liveness empieza a fallar durante el arranque y el pod puede entrar en CrashLoopBackOff. Y si arranca en 8 s, hemos perdido 12 s de vigilancia.

La alternativa correcta es una startupProbe que separe los presupuestos:

startupProbe:
  httpGet:
    path: /salud
    port: 8081
  periodSeconds: 3
  timeoutSeconds: 2
  failureThreshold: 15      # 45 s de margen de arranque, de sobra para 8 s
livenessProbe:
  httpGet:
    path: /salud
    port: 8081
  periodSeconds: 10         # sin initialDelaySeconds: ya no hace falta
  timeoutSeconds: 3
  failureThreshold: 4

Ventaja añadida: el startupProbe con periodSeconds: 3 detecta el arranque terminado en cuanto ocurre, así que un pod que arranca en 8 s está listo en ~9 s, no en 20.

Solución 2

Los tres problemas:

  1. La liveness comprueba dependencias externas. /api/estado-completo es un proxy hacia api-reservas y de ahí a PostgreSQL. Si la API o la base de datos van lentas, se reinician los tres pods de tienda-web a la vez, dejando la web pública completamente caída aunque nginx estuviera perfectamente. Es exactamente el escenario del apartado 5.
  2. No hay readinessProbe. Combinado con maxUnavailable: 1, cada despliegue manda tráfico a pods de nginx que aún no han terminado de arrancar → errores durante cada actualización.
  3. No hay preStop ni periodo de gracia ajustado. Los pods que se retiran mueren antes de que se propague la retirada del endpoint → 502 en cada despliegue. Añadido: timeoutSeconds: 1 es demasiado ajustado.

Manifiesto corregido:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tienda-web
  namespace: rutas-norte-pre
  labels:
    app: tienda-web
    app.kubernetes.io/part-of: rutas-norte
    entorno: pre
spec:
  replicas: 3
  minReadySeconds: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0        # nunca bajar de 3 pods sirviendo
  selector:
    matchLabels:
      app: tienda-web
      entorno: pre
  template:
    metadata:
      labels:
        app: tienda-web
        app.kubernetes.io/part-of: rutas-norte
        entorno: pre
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: nginx
          image: registry.rutasnorte.example/tienda-web:5.2.0
          ports:
            - name: http
              containerPort: 80
          # Liveness: SOLO nginx, sin salir del pod
          livenessProbe:
            httpGet:
              path: /nginx-salud     # ubicación estática que devuelve 200 desde nginx
              port: http
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          # Readiness: la web estática se sirve sin depender de la API
          readinessProbe:
            httpGet:
              path: /nginx-salud
              port: http
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 5"]

Con la configuración de nginx correspondiente:

location = /nginx-salud {
    access_log off;
    return 200 "ok\n";
    add_header Content-Type text/plain;
}

El access_log off; evita que las sondas (una cada 5 s por pod, 3 pods → más de 50 000 líneas al día) inunden los logs que centralizaremos en 07-05.

Solución 3

# ---- ARRANQUE: presupuesto muy generoso, cubre el peor caso de 150 s ----
startupProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 24        # 24 * 10 = 240 s > 150 s del peor caso

# ---- VIVACIDAD: la JVM responde; NADA de PostgreSQL ----
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  periodSeconds: 15
  timeoutSeconds: 6           # > 4 s de la pausa máxima del GC
  failureThreshold: 3         # peor caso: 51 s

# ---- PREPARACIÓN: ¿está el índice cargado y sirve consultas? ----
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 6
  failureThreshold: 3
  successThreshold: 1

Justificación de cada decisión:

  • startupProbe con 240 s de presupuesto. El arranque va de 45 a 150 s. Dar un 60 % de margen sobre el peor caso conocido evita reinicios espurios el día que PostgreSQL vaya lento durante la carga. Sin esta sonda, la liveness mataría el pod a los ~50 s y entraría en CrashLoopBackOff eterno: nunca llegaría a cargar el índice.
  • timeoutSeconds: 6 en liveness y readiness. Las pausas del recolector de basura llegan a 4 segundos. Si el timeout fuese 3 s, cada pausa larga contaría como fallo. Con 6 s, una pausa de GC no dispara nada. Este es un ajuste específico de la JVM que hay que hacer conscientemente.
  • periodSeconds: 15 en liveness. El componente es estable; no necesitamos detección subsegundo. Un periodo alto reduce la presión de sondas sobre la aplicación.
  • Liveness contra /actuator/health/liveness y no contra el endpoint general. Spring Boot separa por defecto los grupos liveness (estado de la JVM) y readiness (dependencias listas). Usar /actuator/health a secas incluiría los indicadores de PostgreSQL en la liveness: el error del apartado 5.
  • Readiness que sí comprueba el índice. Aunque buscador-rutas no toque PostgreSQL en régimen, sí necesita el índice cargado. Que la readiness falle mientras el índice no esté listo es correcto: el pod no debe recibir búsquedas que no puede resolver.
  • successThreshold: 1. Volver rápido al servicio en cuanto se recupera.

Detalle práctico: en Spring Boot hay que activar los grupos de sondas con management.endpoint.health.probes.enabled=true (se activa solo si detecta que corre en Kubernetes).

Conclusión

Las sondas son el primer paso hacia una plataforma observable, y también el primero hacia una plataforma fiable. En esta lección hemos visto que:

  • Un proceso vivo no es una aplicación sana, y el caso del pool de conexiones agotado de api-reservas lo demuestra sin ambigüedad.
  • livenessProbe reinicia, readinessProbe retira del Service, startupProbe retrasa a las otras dos. Tres preguntas distintas con tres consecuencias distintas.
  • Los cuatro manejadores (httpGet, tcpSocket, exec, grpc) tienen costes y precisiones muy diferentes; exec es el más flexible y el más caro.
  • El tiempo de detección se calcula, no se intuye: failureThreshold * periodSeconds + timeoutSeconds.
  • La regla que evita la caída más cara del módulo: la liveness nunca comprueba dependencias externas; la readiness sí debe hacerlo.
  • Las sondas cierran la deuda de 02-04: junto a maxUnavailable: 0, minReadySeconds y un preStop que gana la carrera contra el SIGTERM, tenemos por fin despliegues genuinamente sin corte y sin los 502 que veíamos en cada actualización.

Rutas Norte ya sabe si cada componente está sano. Lo que todavía no sabe es cuánto consume. En 03-04 fijamos las requests y los limits de cada componente prácticamente a ojo, y dejamos escrito que los calibraríamos observando el consumo real. Ese momento ha llegado: en la próxima lección instalaremos metrics-server, la primera fuente de datos de consumo del clúster, y con kubectl top compararemos lo que pedimos con lo que de verdad gastamos.

Curso de Kubernetes

Módulo 1: Introducción a Kubernetes

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados