Saltar al contenido principal

Troubleshooting de aplicaciones en Kubernetes

Si el troubleshooting del cluster era el pan de cada día del administrador, este capítulo es el del desarrollador: nuestra aplicación no arranca, se reinicia en bucle o no responde, y el cluster está perfectamente sano. Vamos a construir un método sistemático para no depurar a ciegas.

El método: estado → describe → logs​

Ante cualquier problema, siempre la misma secuencia:

kubectl get pods # 1. ¿En qué estado está?
kubectl describe pod <pod> # 2. ¿Qué dicen los eventos?
kubectl logs <pod> [--previous] # 3. ¿Qué dice la aplicación?

El campo STATUS de kubectl get pods ya nos dice dónde mirar. Vamos estado por estado.

Pending: el pod no encuentra sitio​

El pod está aceptado pero el scheduler no consigue asignarle nodo. kubectl describe pod mostrará un evento FailedScheduling con el motivo exacto:

  • Insufficient cpu/memory: ningún nodo tiene los recursos que pide nuestro requests. Solución: bajar requests, escalar el cluster o liberar carga.
  • Taints sin tolerar: node(s) had untolerated taint... (capítulo 123).
  • Selectores imposibles: un nodeSelector o affinity que ningún nodo cumple (capítulo 122).
  • PVC sin resolver: el pod monta un PVC en Pending (revisemos la StorageClass y los PV disponibles).

ImagePullBackOff / ErrImagePull: la imagen no llega​

Kubernetes no consigue descargar la imagen. Los eventos del describe nos dan la causa:

  • Nombre o tag mal escritos: nginx:lastest en lugar de latest. Más común de lo que parece.
  • Imagen privada sin credenciales: falta el imagePullSecrets con un secret de tipo docker-registry.
  • Registry inaccesible desde los nodos (red, proxy, TLS).
kubectl describe pod <pod> | grep -A5 Events
# Failed to pull image "myregistry.io/app:v2": ... unauthorized

Arreglo rápido habitual en examen: kubectl set image deployment/web app=nginx:1.27 o editar el deployment.

CrashLoopBackOff: arranca, muere, repite​

El contenedor arranca y termina (con error) una y otra vez; Kubernetes espera cada vez más entre reintentos (el backoff). Las causas viven casi siempre dentro de la app, así que:

# Los logs del intento anterior (el contenedor actual quizá ni ha arrancado)
kubectl logs <pod> --previous

Causas típicas:

  • Error de la aplicación al arrancar: configuración que falta (revisa env, ConfigMaps y Secrets referenciados), conexión a base de datos imposible...
  • El comando termina: si el contenedor ejecuta algo que acaba (un script, un comando mal puesto en command), Kubernetes lo considera muerto y lo reinicia. Un contenedor "de servicio" debe quedarse en primer plano.
  • Liveness probe demasiado agresiva: la app tarda en arrancar más que el initialDelaySeconds y la probe la mata en bucle. El describe lo delata (Liveness probe failed). Solución: ajustar tiempos o añadir una startup probe.

OOMKilled: el caso especial​

Si en el describe vemos Last State: Terminated, Reason: OOMKilled, el contenedor superó su límite de memoria y el kernel lo mató. No habrá nada en los logs: la app no "falló", la fulminaron. Solución: subir el limits.memory (o arreglar la fuga de memoria, pero eso ya es nuestro código).

kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState}'

Running pero no funciona​

El pod está Running y 1/1 Ready... y aun así la aplicación no responde. Ahora toca entrar:

# Shell dentro del contenedor
kubectl exec -it <pod> -- sh

# ¿Responde la app en local?
kubectl exec <pod> -- wget -qO- localhost:8080/health

# Probar desde fuera sin exponer nada
kubectl port-forward pod/<pod> 8080:8080
curl localhost:8080

kubectl debug: cuando no hay ni shell​

Las imágenes modernas (distroless, scratch) no traen shell ni utilidades. Para eso existen los ephemeral containers: kubectl debug añade un contenedor temporal con herramientas al pod en ejecución, compartiendo su red (y su filesystem si se lo pides):

# Contenedor efímero con utilidades, unido al pod problemático
kubectl debug -it <pod> --image=busybox:1.36 --target=<contenedor>

# O una copia del pod para experimentar sin tocar el original
kubectl debug <pod> -it --copy-to=debug-copy --container=app -- sh

Es la herramienta moderna de depuración y ya aparece en los exámenes.

El service no llega a los pods​

La app funciona con port-forward pero no a través del service. El 90% de las veces es una de estas tres cosas, en este orden:

  1. El selector no casa con los labels de los pods:
kubectl get endpointslices -l kubernetes.io/service-name=<servicio> # ¿Sin endpoints? Selector mal
kubectl get pods --show-labels
  1. El targetPort no es el puerto de la app: el service apunta al 80 pero la app escucha en el 8080.
  2. Los pods no están Ready: una readiness probe fallando saca al pod de los endpoints silenciosamente.

La API Endpoints (y por tanto kubectl get endpoints) está deprecada desde Kubernetes 1.33 en favor de EndpointSlices. Sigue funcionando, pero muestra un aviso; mejor acostumbrarse ya al comando nuevo.

Y si el service tiene endpoints pero el nombre no resuelve, vuelve al capítulo de DNS.

Chuleta de diagnóstico​

Estado/SíntomaDónde mirarCausa típica
Pendingdescribe → eventosRecursos, taints, selectores, PVC
ImagePullBackOffdescribe → eventosTag erróneo, registry privado
CrashLoopBackOfflogs --previousApp falla al arrancar, liveness agresiva
OOMKilleddescribe → Last StateLímite de memoria insuficiente
Init:X/Y colgadologs -c <init>Init container esperando dependencia
Running pero KOexec, port-forward, debugBug de la app, config errónea
Service no respondeget endpointslicesSelector, targetPort, readiness

Resumen​

  • Método fijo: estado → describe (eventos) → logs (con --previous si se reinicia).
  • Cada STATUS apunta a una familia de causas: Pending = scheduling, ImagePull = imagen, CrashLoop = la app o sus probes, OOMKilled = memoria.
  • kubectl debug con ephemeral containers es la vía para imágenes sin shell.
  • En problemas de service, los endpoints son la primera comprobación: selector, targetPort y readiness.

Volver al índice