Kubernetes en producción: lecciones de 3 años operando clusters
La pregunta que deberías hacerte primero
Antes de hablar de cómo operar Kubernetes, hay una pregunta que demasiados equipos se saltan: debería usar Kubernetes?
La respuesta no es automáticamente sí. K8s resuelve un problema específico: orquestar cargas de trabajo containerizadas a escala, con alta disponibilidad, despliegues declarativos y autoescalado. Si tienes 3 servicios con tráfico predecible, Kubernetes es probablemente overengineering. Un par de VMs con Docker Compose y un load balancer te dan el 80% del beneficio con una fracción de la complejidad.
Nuestra regla práctica: Kubernetes empieza a justificarse a partir de 8-10 servicios, o cuando necesitas autoescalado real, o cuando tus requisitos de disponibilidad exigen zero-downtime deployments sistemáticos. Por debajo de eso, las alternativas más simples (ECS, Cloud Run, incluso Railway o Render) cubren la mayoría de necesidades con menos overhead operativo.
Dicho esto, cuando K8s es la respuesta correcta, es una herramienta extraordinaria. Estas son las lecciones de operarlo durante tres años.
Dimensionamiento de cluster: el error del día uno
El primer cluster que ayudamos a poner en producción estaba sobredimensionado un 300%. Tres nodos m5.2xlarge (8 vCPU, 32GB RAM cada uno) para una carga que cabía holgadamente en un único m5.xlarge. El motivo era el clásico: “por si acaso necesitamos escalar.”
El problema del sobredimensionamiento en cloud no es solo el coste (que lo es: esos tres nodos costaban 850 EUR/mes y la carga real necesitaba 280 EUR/mes). El problema es que enmascara errores de configuración. Cuando tienes 96GB de RAM, no notas que un pod tiene un memory leak de 500MB. Cuando tienes 24 vCPUs, no notas que un servicio consume 4 CPUs por un bucle ineficiente. Los recursos sobrados esconden problemas que explotan cuando realmente necesitas escalar.
Lo que funciona: empezar con nodos right-sized, activar el Cluster Autoscaler desde el día uno, y usar nodos de diferentes tamaños. Una combinación de nodos small para cargas base y nodos medium/large que el autoscaler añade y elimina según demanda. En EKS, Karpenter ha reemplazado al Cluster Autoscaler clásico y hace esto mucho mejor: selecciona automáticamente el tipo de instancia óptimo para los pods pendientes.
Requests y limits: la configuración más importante y la más ignorada. Los requests determinan cuánta CPU y memoria le garantiza el scheduler a un pod. Los limits determinan cuánto puede consumir como máximo. Sin requests, el scheduler no puede tomar buenas decisiones de placement. Sin limits, un pod con un leak puede tumbar un nodo entero.
La regla que seguimos: requests al 70-80% del consumo medio observado, limits al 150-200% del pico observado. Y revisamos estas cifras trimestralmente, porque el consumo cambia con cada release.
Networking: donde viven los dragones
El networking de Kubernetes es la fuente de más incidentes en nuestra experiencia. No porque sea mal diseñado, sino porque es complejo y la mayoría de los equipos no lo entiende a fondo.
CNI (Container Network Interface). La elección de CNI importa más de lo que parece. Calico es el más común y el más versatile. Cilium está ganando terreno rápido gracias a eBPF, que le da visibilidad y rendimiento superiores. AWS VPC CNI asigna IPs reales de la VPC a los pods, lo que simplifica la integración con servicios AWS pero limita el número de pods por nodo (depende del tipo de instancia y del número de ENIs soportadas).
Hemos tenido un incidente en producción causado por alcanzar el límite de IPs de la subnet en un cluster con VPC CNI. Los pods quedaban en Pending indefinidamente y el error era críptico. Desde entonces, siempre dimensionamos las subnets con un margen de 3x sobre la capacidad esperada.
Ingress. Nginx Ingress Controller sigue siendo el estándar para la mayoría de cargas. Pero atención al dimensionamiento: un único ingress controller con los defaults es un single point of failure. Corremos dos o tres réplicas con anti-affinity para distribuirlas en nodos diferentes, y monitorizamos la latencia del ingress como métrica primaria.
Service mesh. Istio sigue siendo potente pero pesado. Para equipos que necesitan observabilidad de red y mTLS entre servicios, Linkerd es más ligero y más fácil de operar. Para la mayoría de los casos que vemos en mid-market, no se necesita un service mesh completo. Las Network Policies nativas de Kubernetes cubren la microsegmentación básica.
DNS. CoreDNS es el componente invisible que causa problemas visibles. En clusters con cientos de pods, la resolución DNS puede convertirse en cuello de botella. Dos ajustes que siempre aplicamos: ndots:2 en la configuración de pods (para reducir queries innecesarias) y NodeLocal DNSCache (un cache DNS local en cada nodo).
Seguridad: lo mínimo que no es negociable
La seguridad en Kubernetes tiene capas. Estas son las que consideramos obligatorias:
RBAC. Ningún usuario ni servicio debería tener permisos cluster-admin excepto el equipo de plataforma. Cada equipo tiene un namespace con roles limitados: pueden desplegar en su namespace, leer logs, pero no pueden modificar recursos de otros equipos ni recursos globales del cluster. Esto parece obvio, pero hemos encontrado clusters en producción donde todos los service accounts tenían permisos de admin.
Pod Security Standards. Desde Kubernetes 1.25, los Pod Security Standards (PSS) reemplazan a los PodSecurityPolicies. El nivel “restricted” debería ser el default: no root, no host network, no privileged containers, no escalation de privilegios. Las excepciones se documentan y se auditan.
Escaneo de imágenes. Trivy o Grype en el pipeline de CI/CD. Cada imagen que entra al cluster pasa un escaneo de vulnerabilidades. Las críticas bloquean el despliegue. Las altas generan un issue que hay que resolver en 7 días. Esto suena restrictivo hasta que descubres que el 40% de las imágenes base en Docker Hub tienen vulnerabilidades críticas conocidas.
Secrets. Los Secrets de Kubernetes nativos están codificados en base64, no cifrados. Para producción usamos External Secrets Operator que sincroniza secretos desde AWS Secrets Manager o HashiCorp Vault. Así los secretos reales nunca viven en etcd.
Network Policies. Por defecto, todos los pods de un cluster Kubernetes pueden hablar con todos los demás. Esto es terrible para seguridad. Implementamos network policies de deny-all por defecto en cada namespace, y luego abrimos explícitamente solo las comunicaciones necesarias. Sí, es trabajo adicional. Pero un pod comprometido en un namespace no debería poder alcanzar la base de datos de otro namespace.
Despliegues que no despiertan a nadie a las 3am
El patrón de despliegue por defecto de Kubernetes (RollingUpdate) funciona bien para la mayoría de los casos. Pero hay detalles que marcan la diferencia entre un despliegue tranquilo y un incidente:
Readiness probes bien configuradas. La readiness probe debe verificar que la aplicación está realmente lista para recibir tráfico, no solo que el proceso está corriendo. Si tu aplicación necesita 30 segundos para calentar la cache, la readiness probe debe reflejar eso. De lo contrario, Kubernetes envía tráfico a pods que todavía no están listos y los usuarios ven errores 5xx.
PodDisruptionBudgets. Un PDB garantiza que siempre haya un número mínimo de pods disponibles durante actualizaciones de nodos o despliegues. Sin PDB, una actualización de nodo puede tumbar todas las réplicas de un servicio simultáneamente. Siempre configuramos PDB con al menos maxUnavailable: 1 para servicios críticos.
Canary deployments. Para servicios de alto tráfico o cambios arriesgados, usamos Argo Rollouts para canary deployments: el 5% del tráfico va a la nueva versión, monitorizamos métricas durante 10 minutos, y si todo está bien, incrementamos progresivamente. Si las métricas se degradan, rollback automático. Esto ha evitado incidentes en producción que habríamos detectado demasiado tarde con un rolling update tradicional.
Optimización de costes: el ciclo continuo
Kubernetes en cloud no es barato. Un cluster EKS con 10 nodos m5.xlarge cuesta aproximadamente 1.400 EUR/mes solo en compute (sin contar el fee de EKS, networking, storage). Optimizar costes es un proceso continuo, no un proyecto puntual.
Spot instances para cargas tolerantes. Workloads batch, jobs de CI/CD, y entornos de desarrollo corren en nodos spot, que cuestan un 60-70% menos que on-demand. Karpenter gestiona automáticamente la mezcla de spot y on-demand. Lo crítico es que las cargas en spot deben tolerar interrupciones: graceful shutdown, checkpointing, y la lógica de retry adecuada.
Vertical Pod Autoscaler (VPA). VPA analiza el consumo real de los pods y recomienda (u opcionalmente aplica) ajustes de requests y limits. Es la herramienta más infrautilizada del ecosistema. En un cluster de 80 pods, VPA identificó que el 60% tenía requests de CPU sobredimensionados entre 2x y 5x. Ajustarlos nos permitió reducir el número de nodos de 12 a 8.
Namespaces de desarrollo con TTL. Los entornos de desarrollo en Kubernetes tienden a acumularse. Un namespace creado para probar una feature que se mergeó hace tres meses sigue consumiendo recursos. Implementamos un sistema de TTL que marca namespaces de desarrollo para borrado automático después de 7 días de inactividad, con una notificación previa al equipo.
Revisión mensual de costes. Kubecost o OpenCost proporcionan visibilidad de coste por namespace, por deployment, por label. Revisamos estos datos mensualmente y los compartimos con los equipos de desarrollo. Cuando un equipo ve que su servicio cuesta 200 EUR/mes en compute, toma decisiones de optimización que antes ni se planteaba.
Kubernetes bien operado es una plataforma que multiplica la productividad de los equipos de desarrollo. Mal operado, es una fuente inagotable de incidentes y costes. La diferencia está en invertir el tiempo necesario en los fundamentos: dimensionamiento, networking, seguridad y observabilidad. No hay atajos.