Hay un momento en la vida de todo equipo de desarrollo donde alguien dice: “ya tenemos el cluster de Kubernetes andando, ahora hay que meter la app”. Y ahí empieza un descubrimiento que nadie anticipa: desarrollar una aplicación que corre en Kubernetes no es lo mismo que desarrollar una que corre en un servidor.
No se trata de Docker — dockerizar tu app es el paso más fácil. Se trata de que Kubernetes introduce un conjunto de reglas y comportamientos que tu aplicación necesita respetar. Cosas que aparecen a las 3 AM cuando algo se rompe en producción y nadie entiende por qué.
Este post es la guía que nos hubiera gustado tener antes de poner nuestra primera aplicación en un cluster.
⚡Tu aplicación va a tener múltiples réplicas. Preparate.
En un servidor tradicional, tu app corre como un único proceso. Un solo estado en memoria. Un solo sistema de archivos. En Kubernetes, tu aplicación va a tener 2, 3, 10 o más réplicas corriendo al mismo tiempo — cada pod independiente, sin compartir memoria ni disco con los demás.
Estado en memoria: si un usuario hace login en el pod A y el siguiente request cae en el pod B, el pod B no tiene idea de quién es. Las sesiones en memoria, los caches locales y los contadores en variables globales no funcionan con múltiples réplicas.
Archivos locales: si tu app guarda algo en /tmp/reports/, ese archivo solo existe en ese pod. Las otras réplicas no lo ven. Y cuando el pod muere, desaparece.
Tareas de inicio: si tu app corre migraciones al arrancar y tenés 5 réplicas levantando al mismo tiempo, tenés 5 migraciones en paralelo — con resultados que van de inofensivos a catastróficos.
Todo estado compartido vive fuera de tu app: sesiones en Redis, cache en Memcached, archivos en S3 o un PersistentVolume compartido. Tu app debe poder responder cualquier request en cualquier réplica sin asumir que el anterior cayó en el mismo pod.
Las migraciones deben ser un Job separado que corre antes del deploy, no parte del startup de tu aplicación.
🔄Durante un deploy, dos versiones de tu app conviven.
Con Rolling Update, los pods se reemplazan de a uno. Esto significa que durante segundos — o minutos — van a estar corriendo pods con la versión vieja y la nueva al mismo tiempo. Este es probablemente el punto más importante y el menos intuitivo para equipos que vienen del mundo de “un servidor, un deploy”.
Si la versión nueva renombra una columna de base de datos, los pods viejos (que todavía están sirviendo tráfico) van a explotar porque esperan el esquema anterior. Lo mismo aplica para cambios en contratos de API entre servicios.
Los cambios destructivos de esquema necesitan hacerse en múltiples deploys:
APIs y mensajes: los cambios en contratos deberían ser siempre aditivos (agregar campos, agregar endpoints) y nunca destructivos — al menos no sin un período de coexistencia.
Feature flags: permiten activar funcionalidad nueva solo cuando el 100% de los pods ya están en la versión nueva. Esto desacopla el deploy del release.
💀Tu aplicación va a morir. Muchas veces. Aceptalo.
En un servidor tradicional, tu proceso corre durante meses. En Kubernetes, tus pods pueden morir y renacer constantemente — y esto es comportamiento normal, no un error.
Rolling Update, autoscaler bajando réplicas, Karpenter consolidando nodos, una Spot Instance interrumpiéndose, el liveness probe fallando, OOMKilled por exceder el memory limit, o un problema de hardware en el nodo.
- Implementá graceful shutdown: cuando Kubernetes envía
SIGTERM, tu app debería dejar de aceptar conexiones nuevas, terminar los requests en vuelo, cerrar conexiones a bases de datos y colas, y recién entonces terminar. Sin esto, los requests en vuelo se pierden. - Startup rápido y repetible: levantá el proceso, conectate a las dependencias críticas, reportá que estás listo, empezá a servir. Todo lo que no sea necesario para el primer request puede hacerse de forma asincrónica.
- No schedules tareas internas con
setIntervalpara dentro de 6 horas — ese pod probablemente no va a existir. Usá CronJobs de Kubernetes o una cola externa para tareas diferidas.
⚙️La configuración no va en el código. Va en el entorno.
Tu aplicación no debería saber si está corriendo en desarrollo, staging o producción mirando un if en el código. Debería comportarse diferente porque recibe configuración diferente del entorno.
URL de la base de datos, credenciales de servicios externos, feature flags, nivel de logging, URLs de APIs internas, configuración de cache. Kubernetes tiene ConfigMaps para configuración no sensible y Secrets para credenciales.
El mismo build de Docker debería funcionar en cualquier entorno. Si tenés que buildear una imagen diferente para staging y producción, algo está mal. El artefacto es el mismo; lo que cambia es la configuración que recibe. Esto hace que tus tests en staging sean representativos de producción — porque es literalmente el mismo código.
❤️Los health checks no son opcionales. Son tu contrato con Kubernetes.
Kubernetes necesita saber tres cosas sobre tu aplicación. Cada una con una probe específica:
Usar el mismo endpoint para readiness y liveness. Si tu readiness check valida la conexión a la BD y la BD tiene un problema momentáneo, querés que Kubernetes saque el pod de la rotación (readiness), no que lo mate y reinicie (liveness) — reiniciarlo no va a arreglar un problema en la base de datos.
Liveness: endpoint básico que valida que el proceso está respondiendo (devuelve 200). Readiness: valida que la app puede servir requests correctamente — conexión a dependencias críticas, estado interno sano.
📋Los logs van a stdout. Siempre.
En un servidor tradicional escribís logs en archivos. En Kubernetes, no hagas esto. Los archivos de log dentro de un pod desaparecen cuando el pod muere. Y el pod va a morir.
Las aplicaciones escriben sus logs a stdout y stderr. Kubernetes los captura automáticamente y los hace disponibles via kubectl logs. Si tenés Loki, recolecta los logs de stdout de cada pod automáticamente — sin que tu app tenga que saber que Loki existe.
- Formato estructurado (JSON): cuando tenés 50 pods generando logs simultáneamente, poder filtrar por campos (
level,service,request_id) es la diferencia entre encontrar el problema en segundos o en horas. - Correlation ID en cada log: cuando un request atraviesa múltiples servicios, el correlation ID te permite seguir el rastro completo a través de todos los pods involucrados.
- No loguees información sensible — los logs centralizados son accesibles por más personas que el servidor original. El blast radius de un log con credenciales es mucho mayor.
📊Definí tus recursos. No es opcional.
Cada pod debería tener requests y limits de CPU y memoria configurados. Sin ellos, Kubernetes no puede tomar buenas decisiones sobre dónde colocar tus pods, el autoscaler no puede dimensionar los nodos, y un pod con memory leak puede tumbar a todos los pods que comparten su nodo.
No poner recursos: Kubernetes trata tu pod como “best effort” — el primero en morir cuando hay presión en el nodo. El autoscaler no tiene información para dimensionar correctamente.
Adivinar los valores: poner 1Gi de memoria porque “suena bien” es tan malo como no poner nada. Los valores deberían basarse en datos reales de uso — de un dashboard de observabilidad que muestre consumo bajo carga normal y picos.
Requests es lo mínimo que tu pod necesita. Kubernetes lo usa para decidir en qué nodo colocar el pod. Limits es el máximo que puede consumir — si excede la memoria, el pod es OOMKilled; si excede la CPU, es throttled pero no muere.
🔗Pensá en la comunicación entre servicios
En un monolito, llamar a otra parte de la app es una función. En microservicios dentro de Kubernetes, llamar a otro servicio es un request de red. Y la red falla.
- Retries con backoff exponencial: si un request a otro servicio falla, no lo reintentes inmediatamente en loop. Usá backoff con jitter para no generar una avalancha de reintentos que empeore el problema.
- Timeouts en todas las llamadas externas: cada request HTTP, query a BD, conexión a Redis. Sin timeouts, un servicio colgado puede bloquear a todos los que dependen de él — efecto dominó.
- Circuit breakers: si un servicio downstream falla consistentemente, tu app debería dejar de llamarlo temporalmente. Esto protege a ambos servicios y le da tiempo para recuperarse.
- DNS no siempre resuelve instantáneamente: bajo carga o durante cambios de topología puede haber latencia. Cachear resoluciones DNS y manejar errores de resolución es importante para servicios de alta frecuencia.
| Área | Qué validar | Estado |
|---|---|---|
| Estado | Sin estado en memoria ni en filesystem local entre requests | Crítico |
| Migraciones | Corren como Job separado, no al startup de cada réplica | Crítico |
| Retrocompat. | Cambios de esquema y API en múltiples deploys aditivos | Crítico |
| Shutdown | Escucha SIGTERM, termina requests en vuelo, cierra conexiones | Crítico |
| Config | ConfigMaps y Secrets, mismo build para todos los entornos | Crítico |
| Health checks | Startup, readiness y liveness probes distintos y correctos | Crítico |
| Logging | stdout, JSON estructurado, correlation ID, sin datos sensibles | Importante |
| Recursos | requests y limits basados en datos reales de observabilidad | Importante |
| Comunicación | Retries con backoff, timeouts explícitos, circuit breakers | Importante |
La infraestructura que acompaña a tu equipo
Desde SleakOps, la infraestructura de Kubernetes viene configurada con las mejores prácticas para que tu equipo pueda enfocarse en estos patrones de aplicación — sin preocuparse además por la complejidad del cluster, el networking, el autoescalado y la observabilidad.
Conocé SleakOps →