
Mejores Prácticas de Kubernetes para Producción: Guía Definitiva de un SRE
"Guía técnica avanzada y exhaustiva para operar Kubernetes a gran escala: gestión de recursos y QoS, eliminación de errores 502, PDB, Topology Spread, hardening y observabilidad."
Cualquier clúster de Kubernetes puede ejecutar cargas de trabajo en un entorno de desarrollo o pruebas. Sin embargo, cuando se traslada ese mismo manifiesto a producción bajo tráfico masivo, despliegues continuos y mantenimientos automáticos de nodos, la abstracción se desmorona rápidamente si no se han tomado decisiones arquitectónicas deliberadas.
Como Principal SRE tras una década gestionando flotas de clústeres multirregión con decenas de miles de núcleos, he presenciado cómo los fallos más destructivos no provienen de caídas en el plano de control (etcd o kube-apiserver), sino de errores de configuración fundamentales en el ciclo de vida de los pods, presunciones erróneas sobre el kernel Linux y políticas de red inexistentes.
En esta guía técnica profundizaremos en las cinco áreas críticas para construir y operar cargas de trabajo de misión crítica en Kubernetes con resiliencia, determinismo y seguridad de nivel empresarial.
1. Gestión de Recursos y Clases de QoS: Evitando Throttling y OOMKills
Uno de los errores más generalizados en la adopción de Kubernetes es asumir que el planificador (kube-scheduler) y el kernel Linux manejan los recursos de la misma forma.
El Funcionamiento Interno: Requests vs Limits
Kubernetes utiliza dos conceptos que operan en capas totalmente distintas:
requests: Utilizado exclusivamente por elkube-scheduleren el momento de decidir en qué nodo colocar el Pod. Si un nodo no tiene suficiente capacidad disponible (basada en la suma de requests de los pods existentes), el Pod permanece en estadoPending. Los requests garantizan la reserva a nivel de cgroups (cpu.sharesymemory.min/memory.low).limits: Configurado por el runtime del contenedor (comocontainerdoCRI-O) a través de los controladores del kernel Linux (cgroups v1/v2). Define el techo absoluto que el contenedor jamás puede sobrepasar.
# resources-example.yaml
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1024Mi"
La Trampa del CFS Quota Throttling en CPU
A diferencia de la memoria (que es un recurso no comprimible), la CPU es un recurso comprimible. Cuando un contenedor intenta exceder su límite de CPU, el kernel no mata el proceso, sino que lo estrangula (throttling).
En Linux, los límites de CPU se implementan mediante el Completely Fair Scheduler (CFS) Quota:
- La cuota se mide en ventanas temporales fijas (por defecto
cpu.cfs_period_us = 100000µs, es decir, 100 milisegundos). - Si asignas
limit: 500m(0.5 núcleos), tu contenedor recibe 50 milisegundos de tiempo de ejecución por cada periodo de 100ms. - Si una aplicación multihilo procesa una ráfaga entrante y consume sus 50ms en los primeros 10ms del periodo, el kernel congelará los hilos durante los 90ms restantes.
[!WARNING] El Pitfall de Producción: Una aplicación puede mostrar un uso promedio de CPU de apenas 20% en Prometheus, pero sus tiempos de respuesta P99 pueden dispararse a cientos de milisegundos debido al CFS throttling continuo.
Recomendación de Producción SRE:
- Para cargas de trabajo con latencia crítica (APIs HTTP, microservicios gRPC), considera no configurar límites de CPU (
limits.cpu: null) siempre que utilicesrequests.cpuprecisos y cuentes con cuotas de Namespace (ResourceQuota). - Si tu organización exige límites de CPU por cumplimiento, calcula los límites con un margen holgado (al menos 2x a 3x del request) y monitoriza la métrica
container_cpu_cfs_throttled_periods_total.
Memoria y el OOM Killer (Exit Code 137)
La memoria es un recurso no recuperable. Cuando un contenedor supera su limits.memory:
- El subsistema de memoria de cgroups activa el Out-Of-Memory (OOM) Killer del kernel.
- El proceso principal recibe una señal
SIGKILLforzada (código de salida137). - Kubernetes marca el contenedor con la razón
OOMKilled.
[!NOTE] Al calcular alertas y dimensionamiento de memoria, nunca utilices
container_memory_usage_bytes, ya que incluye la caché de páginas del sistema de archivos (page cache), la cual el kernel puede liberar bajo presión. Utiliza siemprecontainer_memory_working_set_bytes.
Selección Estratégica de Clases de Calidad de Servicio (QoS)
Kubernetes clasifica cada pod en una de tres clases de QoS basándose en la relación entre sus requests y limits:
| Clase QoS | Definición | Prioridad ante desalojo (Eviction) | Caso de Uso en Producción |
|---|---|---|---|
Guaranteed |
requests == limits tanto para CPU como para Memoria en todos los contenedores del Pod. |
Últimos en ser expulsados. Puntuación oom_score_adj = -997. |
Cargas de misión crítica, bases de datos (PostgreSQL, Redis), routers de tráfico e ingress. |
Burstable |
requests < limits en al menos un recurso o contenedor. |
Expulsados cuando el nodo sufre presión de memoria si no hay pods BestEffort. | La gran mayoría de microservicios web, workers de colas y APIs transaccionales. |
BestEffort |
No define ningún request ni limit. | Primeros en ser liquidados inmediatamente ante la menor presión de recursos (oom_score_adj = 1000). |
Trabajos batch no críticos, pruebas efímeras, tareas prescindibles. Nunca para producción. |
2. Ciclo de Vida del Pod y Healthchecks: Cero Errores 502 en Rolling Updates
Un clúster de producción experimenta cientos de cambios de estado diarios: despliegues continuos, reescalado de réplicas con HPA, consolidación de nodos de Karpenter y actualizaciones de seguridad de los nodos subyacentes. Si el ciclo de vida del Pod no está correctamente sincronizado con el plano de datos de la red, los usuarios finales recibirán errores HTTP 502 Bad Gateway y conexiones reseteadas.
La Condición de Carrera en la Terminación del Pod
Cuando Kubernetes termina un Pod (por ejemplo, durante un kubectl rollout restart), ocurren dos flujos paralelos y asíncronos:
Flujo 1 (Plano de Datos de Red):
Kube-apiserver -> EndpointSlice Controller -> kube-proxy / eBPF -> iptables/IPVS actualizados en cada nodo
Flujo 2 (Ciclo de Vida del Contenedor):
Kubelet -> Container Runtime -> Envía SIGTERM al proceso del contenedor
Debido a la latencia de propagación en clústeres medianos y grandes, el Flujo 2 casi siempre es más rápido que el Flujo 1. El contenedor recibe SIGTERM, cierra sus sockets HTTP y deja de aceptar tráfico antes de que kube-proxy o Cilium hayan eliminado su IP del balanceador de carga o ingress. Resultado: peticiones enviadas a un puerto cerrado -> 502 Bad Gateway.
La Solución: El Hook preStop y terminationGracePeriodSeconds
Para sincronizar ambos flujos, debemos forzar al contenedor a retrasar su apagado hasta que los endpoints de red hayan sido purgados:
# zero-downtime-lifecycle.yaml
spec:
terminationGracePeriodSeconds: 60
containers:
- name: api-service
image: registry.example.com/api:v2.4.1
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
¿Cómo funciona esta protección?
- Cuando se marca el Pod para eliminación,
kubeletejecuta el comandosleep 15del hookpreStop. - Mientras el contenedor duerme 15 segundos, sigue aceptando tráfico en sus sockets existentes.
- Paralelamente, el controlador de EndpointSlices retira la IP del Pod de todos los nodos e Ingress controllers.
- Tras los 15 segundos,
kubeletenvíaSIGTERM. - La aplicación inicia su apagado grácil (graceful shutdown): completa las conexiones HTTP pendientes y rechaza nuevas peticiones (con
Connection: close). - Si la aplicación no ha finalizado dentro de los 45 segundos restantes (
60s - 15s),kubeletenviaráSIGKILL.
Healthchecks Reales: Startup, Liveness y Readiness Probes
Configurar sondas incorrectamente es una de las principales fuentes de incidentes en cascada.
# probes-best-practices.yaml
startupProbe:
httpGet:
path: /healthz/startup
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 30 # Otorga hasta 150s (30x5s) de tiempo de arranque
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
periodSeconds: 5
timeoutSeconds: 2
successThreshold: 1
failureThreshold: 2
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
Reglas de Oro para Probes:
startupProbe: Esencial para aplicaciones con tiempos de inicialización lentos (Java/Spring Boot, aplicaciones Node.js cargando grandes modelos en memoria). Evita que ellivenessProbemate el contenedor prematuramente durante el arranque.readinessProbe(Anti-patrón crítico): NUNCA verifiques dependencias externas (bases de datos, Redis, APIs de terceros) en elreadinessProbede un microservicio. Si tu base de datos sufre una ralentización momentánea, todos los pods de tu servicio fallarán el probe a la vez, retirando el 100% de los endpoints de la red y provocando un apagón total en lugar de una degradación parcial con reintentos.livenessProbe: Debe ser ultraligero y verificar únicamente que el proceso no esté en un deadlock irrecuperable. No utilices liveness probes si la aplicación falla de forma rápida ante excepciones no controladas.
3. Alta Disponibilidad y Resiliencia en el Scheduler
Un despliegue con replicas: 3 no garantiza alta disponibilidad por sí solo si el kube-scheduler coloca los tres pods en el mismo nodo físico o en la misma zona de disponibilidad (AZ) que sufre una interrupción en el proveedor de nube.
Topology Spread Constraints: El Estándar Moderno
Históricamente, los ingenieros utilizaban podAntiAffinity para separar réplicas. Sin embargo, en clústeres de gran escala (>100 nodos), el cálculo de afinidades tiene una complejidad computacional prohibitiva ($O(N \times M)$), causando latencias severas en el kube-scheduler.
topologySpreadConstraints es la solución nativa y de alto rendimiento:
# topology-spread.yaml
spec:
topologySpreadConstraints:
# 1. Distribuir uniformemente entre Zonas de Disponibilidad (Multi-AZ)
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: payment-api
# 2. Distribuir entre nodos individuales dentro de cada zona
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: payment-api
maxSkew: 1: La diferencia en el número de pods entre cualquier par de zonas no puede ser mayor a 1.whenUnsatisfiable: DoNotSchedule: Política estricta (hard). Si no hay nodos en una zona vacía, el pod no se programa en zonas sobrecargadas, garantizando tolerancia real a fallos de zona.
PodDisruptionBudget (PDB): Blindaje ante Mantenimiento
Un PodDisruptionBudget define cuántos pods pueden estar no disponibles simultáneamente durante interrupciones voluntarias (ejecución de kubectl drain, actualización de versión de Kubernetes de los nodos, reemplazo de instancias spot).
# pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: payment-api-pdb
spec:
minAvailable: "50%"
selector:
matchLabels:
app: payment-api
[!CAUTION] Peligro en Upgrades: Nunca configures
minAvailable: 100%nimaxUnavailable: 0. Esto impedirá que cualquier proceso de desalojo de nodos drene el nodo, bloqueando de forma permanente los pipelines de actualización del clúster. Para servicios conreplicas: 1, utilizamaxUnavailable: 1si permites interrupciones efímeras, o escala a mínimo 2 réplicas antes de aplicar un PDB.
4. Hardening y Seguridad en Runtime: Paradigma Zero Trust
La seguridad en Kubernetes debe implementarse bajo la asunción de que el código de la aplicación puede verse comprometido. El objetivo del hardening en runtime es contener el impacto y evitar la escalada de privilegios hacia el nodo subyacente o hacia otros servicios del clúster.
Manifiesto de Hardening Estricto con securityContext
A continuación se muestra un pod configurado de acuerdo con el perfil Restricted de los estándares de seguridad de pods (Pod Security Standards / PSS):
# secure-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-service
spec:
replicas: 3
selector:
matchLabels:
app: secure-service
template:
metadata:
labels:
app: secure-service
spec:
# Hardening a nivel de Pod
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/app:1.2.0
# Hardening específico a nivel de Contenedor
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
# Directorio efímero en RAM para archivos temporales necesarios
- name: tmp-volume
mountPath: /tmp
volumes:
- name: tmp-volume
emptyDir:
medium: Memory
sizeLimit: 64Mi
Anatomía de la Seguridad:
runAsNonRoot: true&runAsUser: 10001: Evita que el contenedor ejecute procesos como UID 0 (root). Si un atacante logra escapar del contenedor, heredará privilegios sin privilegios en el host.allowPrivilegeEscalation: false: Bloquea la obtención de privilegios adicionales mediante binarios con bitssetuidosetgid.readOnlyRootFilesystem: true: El sistema de archivos del contenedor es de solo lectura. Un atacante no puede descargar herramientas de explotación, modificar librerías del sistema ni sobrescribir binarios. Cualquier necesidad de escritura temporal debe resolverse con volúmenesemptyDiraislados.capabilities: drop: ["ALL"]: Elimina todas las capacidades POSIX del kernel (comoCAP_NET_RAW,CAP_SYS_ADMIN), neutralizando la gran mayoría de técnicas de escape de contenedores.seccompProfile: type: RuntimeDefault: Restringe las llamadas al sistema (syscalls) que el proceso puede emitir al kernel Linux según el perfil seguro por defecto del runtime.
Segmentación de Red con NetworkPolicies: Estrategia Default-Deny
Por defecto, la red de Kubernetes es totalmente plana: cualquier pod puede comunicarse con cualquier otro pod en cualquier namespace. En producción, esto representa una vulnerabilidad inaceptable.
El primer paso en cualquier namespace de producción debe ser aislarlo por completo:
# 1-default-deny-all.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # Aplica a todos los pods del namespace
policyTypes:
- Ingress
- Egress
Una vez bloqueado todo el tráfico entrante y saliente, se definen políticas explícitas de lista blanca (allowlist):
# 2-allow-app-traffic.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-secure-service
namespace: production
spec:
podSelector:
matchLabels:
app: secure-service
policyTypes:
- Ingress
- Egress
ingress:
# Permitir tráfico únicamente desde el Ingress Controller
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: 8080
egress:
# 1. Permitir resolución de nombres DNS hacia CoreDNS
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# 2. Permitir llamadas hacia la base de datos PostgreSQL
- to:
- podSelector:
matchLabels:
app: postgres-primary
ports:
- protocol: TCP
port: 5432
5. Observabilidad SRE y Operaciones Day-2
Tener dashboards con cientos de gráficos no equivale a tener observabilidad. En entornos de gran escala, la telemetría debe estructurarse en torno a las Cuatro Señales Doradas de Google SRE: Latencia, Tráfico, Errores y Saturación.
Consultas PromQL Críticas para Detección Proactiva
En lugar de alertar sobre el uso superficial de CPU, implementa alertas basadas en las métricas de cAdvisor y kube-state-metrics que realmente anticipan o diagnostican degradación:
1. Detección de CFS CPU Throttling Excesivo
Calcula el porcentaje de periodos en los que los contenedores están sufriendo estrangulamiento por cuota de CPU:
sum(rate(container_cpu_cfs_throttled_periods_total{container!=""}[5m])) by (namespace, pod, container)
/
sum(rate(container_cpu_cfs_periods_total{container!=""}[5m])) by (namespace, pod, container) * 100 > 25
Umbral SRE: Si supera el 25% durante más de 5 minutos, la latencia de tu servicio está comprometida por un límite de CPU artificialmente bajo.
2. Saturación Real de Memoria (Proximidad de OOMKill)
Monitorea qué porcentaje del límite de memoria está utilizando el working_set:
(
container_memory_working_set_bytes{container!=""}
/
kube_pod_container_resource_limits{resource="memory"}
) * 100 > 85
Umbral SRE: Alertas por encima del 85% permiten a los ingenieros actuar antes de que el kernel invoque el OOM Killer.
3. Pods Bloqueados en Estado No-Ready o Pending
Identifica si el planificador no encuentra capacidad o si fallan las sondas de preparación:
sum by (namespace, pod) (kube_pod_status_phase{phase="Pending"}) > 0
Estrategia de Despliegue Progresivo: Más Allá del RollingUpdate Estándar
El controlador RollingUpdate nativo de Kubernetes tiene una limitación fundamental: solo verifica que los pods pasen sus readinessProbe. Si una nueva versión del software pasa la sonda de salud pero lanza errores HTTP 500 al 5% de los usuarios reales, el rollout continuará reemplazando todas las réplicas sanas hasta provocar un incidente total.
En arquitecturas maduras, se adoptan operadores de despliegue continuo progresivo como Argo Rollouts o Flagger:
[Versión Actual (Stable)] ---> 90% Tráfico
|---> Análisis PromQL (Error rate < 0.1% & P99 < 200ms)
[Versión Canary (v2.4.1)] ---> 10% Tráfico
|
¿Métricas degradadas?
/ \
(SÍ) (NO)
/ \
Auto-Rollback Instantáneo Promoción al 100%
6. Lista de Verificación (Checklist) de Producción para Equipos
Antes de autorizar el despliegue de cualquier carga de trabajo en el clúster de producción, verifica el cumplimiento de los siguientes controles:
| Área de Control | Requisito Técnico Obligatorio | Estado |
|---|---|---|
| Recursos | requests de CPU y Memoria calibrados con telemetría real de producción. |
[ ] |
| Recursos | limits de Memoria configurados; límites de CPU eliminados o probados contra CFS throttling. |
[ ] |
| Ciclo de Vida | Hook preStop con sleep 10-20 implementado en servicios HTTP para drenado de endpoints. |
[ ] |
| Ciclo de Vida | terminationGracePeriodSeconds dimensionado con holgura (ej. 45-60s). |
[ ] |
| Sondas | startupProbe presente si la aplicación tarda más de 10s en inicializar. |
[ ] |
| Sondas | readinessProbe sin acoplamiento a dependencias externas (bases de datos/APIs). |
[ ] |
| Resiliencia | topologySpreadConstraints configurado por Zona (topology.kubernetes.io/zone) y Nodo. |
[ ] |
| Resiliencia | PodDisruptionBudget aplicado (maxUnavailable: 1 o minAvailable: 50%). |
[ ] |
| Seguridad | runAsNonRoot: true, readOnlyRootFilesystem: true, y capabilities.drop: [ALL]. |
[ ] |
| Seguridad | NetworkPolicy de aislamiento aplicada (Default-Deny + lista blanca explícita). |
[ ] |
| Observabilidad | Alertas configuradas para CFS Throttling, OOMKills y tasa de error HTTP. | [ ] |
Conclusión
Operar Kubernetes en producción no consiste en memorizar comandos de kubectl, sino en comprender los contratos implícitos entre el plano de control, el planificador, el kernel Linux y la pila de red.
Implementar Requests/Limits deterministas, hooks de pre-parada que erradiquen los errores 502, distribución de topología multi-zona y perfiles de seguridad restrictivos transforma un clúster frágil en una plataforma resiliente, autónoma y predecible capaz de soportar cargas de misión crítica sin sobresaltos a las 3:00 AM.