Cloud

Kubernetes Producción: Primer Deploy, Errores y Soluciones

Kubernetes Producción: Primer Deploy, Errores y Soluciones

Viernes por la tarde. Kubernetes en producción desde hace 6 horas. 40 pods en CrashLoopBackOff. El CEO esperando la demo. El equipo sin comprender qué estaba pasando. Habíamos olvidado configurar las liveness probe.

El primer despliegue de un clúster Kubernetes en producción es un rito de paso para cualquier equipo DevOps o SysAdmin. Es un momento de gran expectativa, pero también de potencial desastre. En mi trayectoria profesional, he liderado muchas migraciones complejas, pero pocos eventos han quedado tan grabados como nuestro primer acercamiento real a Kubernetes en un entorno empresarial. Aquel día nos enseñó lecciones valiosas, forjando un enfoque más robusto y consciente de la orquestación de contenedores. Si estás a punto de afrontar tu primer despliegue, o si ya has vivido momentos similares, esta experiencia te ofrecerá pautas prácticas para evitar los errores más comunes y estabilizar rápidamente tu entorno.

Requisitos Previos / Entorno de Prueba

Nuestro escenario contemplaba la migración de un conjunto de aplicaciones monolíticas, previamente gestionadas con Docker Compose, a un nuevo clúster Kubernetes. El objetivo era mejorar la escalabilidad, la resiliencia y acelerar el proceso de despliegue continuo. El entorno estaba compuesto por 5 nodos worker y 3 nodos master, con almacenamiento Ceph y un balanceador de carga externo. Las aplicaciones eran principalmente Java Spring Boot, Node.js y algunos microservicios Python. El plan de migración era ambicioso: tres semanas para poner todo en línea, incluyendo pruebas de integración y rendimiento.

El contexto: migración de Docker Compose a Kubernetes en 3 semanas

La decisión de pasar a Kubernetes fue impulsada por la necesidad de gestionar una carga de trabajo creciente, con picos impredecibles, y de estandarizar la infraestructura para futuros desarrollos. Docker Compose había alcanzado sus límites en términos de orquestación y auto-recuperación. La promesa de Kubernetes era la solución a muchos de nuestros problemas operativos. Dedicamos semanas a la formación del equipo, a la contenerización de las aplicaciones y a la definición de los manifiestos YAML. El proyecto iba por buen camino, con un entorno de staging que parecía estable y de alto rendimiento. La presión era alta, dada la agresiva línea de tiempo y la importancia estratégica de la migración para la empresa. Una encuesta de la Cloud Native Computing Foundation (CNCF) de 2023 reveló que el 96% de las organizaciones utiliza o planea utilizar Kubernetes, lo que subraya su centralidad, pero también la complejidad del primer acercamiento.

Horas 0-2: despliegue exitoso, todo parece funcionar

El día del despliegue en producción comenzó con euforia. Después de aplicar los manifiestos, kubectl get pods mostraba todos los pods en estado Running. Los servicios respondían correctamente, el tráfico se enrutaba sin problemas y las primeras verificaciones funcionales daban un resultado positivo. Todo el equipo respiró aliviado. Se había informado al CEO del éxito inicial y la demo se había programado para la tarde, un momento crucial para mostrar el valor de la nueva infraestructura. Las métricas básicas del clúster, como el uso de CPU y memoria, parecían normales. En ese momento, pensamos que habíamos superado la fase más crítica.

Horas 2-6: CrashLoopBackOff en la mitad de los pods, causa desconocida

La euforia duró poco. Aproximadamente dos horas después del despliegue, el monitoreo comenzó a señalar anomalías. Un número creciente de pods entraba en el estado CrashLoopBackOff. Inicialmente uno o dos, luego diez, veinte, hasta casi la mitad de los 80 pods totales. Los logs de las aplicaciones eran escasos o inexistentes. kubectl logs no ayudaba mucho, mostrando solo el reinicio continuo de los contenedores. La situación era crítica: la demo del CEO estaba a punto de comenzar y el servicio estaba parcialmente degradado, con un impacto directo en los usuarios. La presión se hizo insostenible.

Para investigar, el primer paso fue utilizar kubectl describe para obtener una visión detallada del estado de los pods problemáticos:

kubectl describe pod <nombre-del-pod>
grep -A20 Events

Esta salida nos mostró una serie de eventos Liveness probe failed y Restarting container. No era un error de aplicación directo, sino un problema de gestión del ciclo de vida del contenedor por parte de Kubernetes.

El error: liveness probe con timeout demasiado bajo en aplicación Java

La causa raíz principal se identificó en las livenessProbe configuradas de forma demasiado agresiva para nuestras aplicaciones Java Spring Boot. Las aplicaciones Java, especialmente al inicio, requieren un cierto tiempo para inicializar el contexto Spring, cargar las dependencias y abrir las conexiones a la base de datos. Nuestra livenessProbe estaba configurada con un initialDelaySeconds demasiado bajo (ej. 5 segundos) y un timeoutSeconds igualmente restrictivo (ej. 1 segundo). Esto hacía que Kubernetes considerara el contenedor no receptivo y lo reiniciara, una y otra vez, antes de que la aplicación tuviera la oportunidad de iniciarse completamente. Se creaba un círculo vicioso de reinicios continuos que impedía a los pods alcanzar un estado Running estable.

La solución inmediata fue aumentar significativamente initialDelaySeconds y timeoutSeconds para permitir que las aplicaciones Java completaran su arranque. Una configuración más realista para una aplicación Java Spring Boot podría ser:

livenessProbe:
  httpGet:
    path: /actuator/health
    port: 8080
  initialDelaySeconds: 60
  periodSeconds: 10
  failureThreshold: 3

Esto concede a la aplicación 60 segundos para iniciarse antes de la primera verificación y permite hasta 3 fallos consecutivos antes de declarar el pod no saludable, con un intervalo de 10 segundos entre una verificación y otra. Este es un error clásico, pero fácil de cometer bajo presión. El 30% de los incidentes en producción en Kubernetes, según un informe de 2024 de Datadog, está relacionado con configuraciones incorrectas de las probes.

El segundo problema: ningún resource limit configurado

Mientras resolvíamos el problema de las liveness probe, surgió otra criticidad. Algunos nodos worker mostraban un uso excesivo de memoria y CPU, con caídas repentinas de rendimiento. Al inspeccionar los pods en esos nodos, notamos que no habíamos configurado resources.limits en los manifiestos. Nuestras aplicaciones, aunque habían sido optimizadas, no tenían un límite máximo de consumo de recursos. En un entorno de producción, esto es un error grave que puede llevar a la inestabilidad y a efectos en cascada.

Sin resource limits, una aplicación con un comportamiento anómalo (por ejemplo, con una fuga de memoria) puede consumir todos los recursos disponibles en el nodo, dejando sin recursos a los otros pods y llevando al propio nodo a un estado inestable. Kubernetes, en estos casos, interviene terminando los procesos que superan los recursos disponibles, a menudo con un OOMKilled (Out Of Memory Killed).

Horas 6-24: OOMKilled en cascada en todo el nodo

La falta de resource limits y el problema inicial de las liveness probe crearon una tormenta perfecta. Después de corregir las probes, las aplicaciones comenzaron a iniciarse, pero sin límites, algunas comenzaron a consumir más memoria de lo previsto, desencadenando OOMKilled en cascada. Un nodo worker con 32 GB de RAM se encontró con varios pods Java que, sin límites, intentaron asignar más memoria de la debida. El kernel de Linux intervino, terminando los procesos para proteger el sistema operativo. Esto llevó a reinicios adicionales de los pods y, en algunos casos, al bloqueo de todo el nodo. Para verificar el estado de OOMKilled de un pod, se puede usar:

kubectl get pod <nombre-del-pod> -o json | jq '.status.containerStatuses[].lastState'

Este comando muestra el último estado del contenedor, y si se encuentra la entrada reason: OOMKilled, se tiene la confirmación del problema. Este escenario es particularmente insidioso porque puede ser difícil de diagnosticar sin un monitoreo adecuado y sin una comprensión profunda de cómo Kubernetes y el kernel de Linux gestionan los recursos.

Cómo recuperamos y estabilizamos el clúster

La recuperación requirió un enfoque multifase y coordinado:

  1. Corrección de las Liveness/Readiness Probe: Aplicamos un parche a todos los despliegues para actualizar las configuraciones de las probes, aumentando initialDelaySeconds y timeoutSeconds, especialmente para las aplicaciones Java. También introdujimos readinessProbe separadas para indicar cuándo la aplicación estaba lista para recibir tráfico, distinguiéndola de la simple ‘vida’ del pod. Esto redujo drásticamente los CrashLoopBackOff.
  1. Configuración de los Resource Limits: Definimos resources.requests y resources.limits para cada contenedor. Inicialmente, optamos por límites conservadores basados en las observaciones en staging, con la intención de refinarlos más tarde con pruebas de carga más precisas. Esto evitó que un solo pod pudiera agotar los recursos de todo el nodo.
  1. Escalabilidad Horizontal y Vertical: Escalamos temporalmente el número de nodos worker para distribuir mejor la carga y proporcionar un búfer de recursos. Al mismo tiempo, aumentamos los recursos (RAM/CPU) en los nodos existentes para gestionar mejor los picos. Esto nos dio tiempo para analizar y optimizar.
  1. Monitoreo Avanzado: Configuramos paneles de monitoreo más granulares con Prometheus y Grafana, centrándonos en métricas clave como el uso de CPU, memoria, I/O de disco para cada pod y nodo, y los estados de los pods. Esto permitió identificar rápidamente las nuevas anomalías.
  1. Comunicación y Plan de Rollback: Nos comunicamos constantemente con el CEO y las partes interesadas, explicando la situación y los pasos para la resolución. Afortunadamente, teníamos un plan de rollback (aunque no probado a fondo) que nos habría permitido volver a Docker Compose, pero logramos estabilizar el clúster antes de tener que invocarlo.

Errores Comunes y Resolución de Problemas

Este incidente destacó varios errores comunes en el despliegue de Kubernetes:

  • Liveness/Readiness Probe mal configuradas: Demasiado agresivas, no tienen en cuenta los tiempos de arranque de las aplicaciones (especialmente Java). La documentación oficial de Kubernetes sobre las probes es un excelente punto de partida para configurarlas correctamente. Link externo: Kubernetes Documentation on Probes
  • Falta de Resource Limits: Dejar los contenedores sin límites de CPU y memoria es una receta para el desastre en producción, lo que lleva a OOMKilled e inestabilidad del nodo.
  • Pruebas de carga insuficientes: El entorno de staging no siempre replica fielmente la carga y los patrones de tráfico de producción. Es esencial realizar pruebas de carga realistas.
  • Monitoreo deficiente: Sin un monitoreo robusto y alertas oportunas, es difícil diagnosticar y reaccionar rápidamente a los incidentes.
  • Planes de rollback no probados: Tener un plan es bueno, probarlo es fundamental. No se puede dar por sentado que funcionará bajo estrés.

FAQ — Preguntas Frecuentes

¿Cuál es la diferencia entre livenessProbe y readinessProbe?

La livenessProbe indica a Kubernetes si el contenedor está «vivo» y funcionando. Si falla, Kubernetes reinicia el contenedor. La readinessProbe, en cambio, indica si el contenedor está listo para servir tráfico. Si falla, Kubernetes deja de enviar tráfico a ese pod, pero no lo reinicia. Son complementarias para garantizar tanto la estabilidad como la disponibilidad del servicio.

¿Cómo puedo configurar correctamente los resource limits para mis aplicaciones?

La mejor manera es monitorear el consumo de recursos de tus aplicaciones en un entorno de staging bajo carga típica. Comienza con requests ligeramente inferiores al consumo promedio y limits ligeramente superiores al consumo de pico detectado. Afina los valores con el tiempo, basándote en métricas y pruebas de estrés. Un buen punto de partida es establecer requests.cpu y requests.memory para garantizar una programación equitativa, y limits.memory para prevenir OOMKilled.

¿Qué hacer cuando un pod está en CrashLoopBackOff?

Lo primero es usar kubectl describe pod para ver los eventos recientes y kubectl logs para los logs del contenedor. A menudo, la causa es una aplicación que no se inicia correctamente, un crash interno o una livenessProbe mal configurada. Revisa también los logs del controller manager o de kubelet para errores más a nivel de sistema.

¿Cómo prevenir OOMKilled en los contenedores?

Principalmente, definiendo resources.limits.memory para cada contenedor. Asegúrate de que el límite sea suficiente para la aplicación, pero no tan alto como para permitir un consumo excesivo. Monitorea atentamente el uso de la memoria y optimiza tus aplicaciones para reducir su huella. Considera también el uso de requests.memory para garantizar que el pod se programe solo en nodos con suficiente memoria.

Conclusiones con Puntos Clave Operativos

Nuestro primer despliegue de Kubernetes en producción fue una experiencia rica en aprendizajes, salpicada de errores comunes pero resolubles. La lección más importante es que la teoría y la práctica pueden divergir significativamente, especialmente en entornos complejos. La definición precisa de livenessProbe y readinessProbe, junto con la configuración meticulosa de los resource limits, son pilares fundamentales para la estabilidad de cualquier clúster Kubernetes. Nunca subestimes la importancia de pruebas de carga realistas y de un monitoreo proactivo. Este enfoque reactivo nos permitió estabilizar un servicio crítico en pocas horas, aprendiendo a gestionar la presión y a identificar rápidamente los culpables. Con una preparación adecuada y una metodología de resolución de problemas robusta, es posible transformar un desastre potencial en una oportunidad de crecimiento y mejora continua.

5 cosas que hacer ANTES del primer despliegue en producción

  1. Probar las Probes en Staging: Simula el inicio de tus aplicaciones con las configuraciones de livenessProbe y readinessProbe que usarás en producción. Asegúrate de que los tiempos de arranque se gestionen correctamente.
  2. Definir Resource Limits Realistas: No vayas a producción sin requests y limits para CPU y memoria. Monitorea tus aplicaciones y establece valores basados en datos reales.
  3. Ejecutar Pruebas de Carga y Estrés: No te fíes solo de las pruebas funcionales. Simula la carga de producción (y más allá) para ver cómo reacciona el clúster y dónde están los cuellos de botella.
  4. Configurar Monitoreo y Alertas: Implementa un sistema de monitoreo completo (ej. Prometheus/Grafana) con alertas configuradas para estados críticos de los pods, uso elevado de recursos y errores de sistema. Un buen monitoreo reduce el MTTR (Mean Time To Resolution).
  5. Preparar un Plan de Rollback: Aunque esperes no usarlo, tener un plan detallado para volver a la configuración anterior (ej. Docker Compose, o una versión anterior de los manifiestos) es crucial. Y, sobre todo, ¡pruébalo!

Actualizado: Julio de 2026

Lee también: Kubernetes Networking: DNS, Services, Ingress — Guía Práctica Sysadmin

Comparte este artículo:

Escrito por

Rosario Giordano

Rosario Giordano es administrador de sistemas y consultor de TI especializado en c iberseguridad y cloud, con más de 20 años de experiencia en la gestión de infraestructuras Linux empresariales. Sus áreas de especialización incluyen el hardening de SSH, plataformas Kubernetes , bases de datos PostgreSQL, virtualización con VMware/Proxmox y el cumplimiento de los marcos de seguridad NIS2 e ISO 27001.