Mejores Prácticas de Kubernetes para Producción: Guía Definitiva de un SRE
Destacado

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."

14 min de lectura
KubernetesDevOpsSRECloud NativeProduction

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 el kube-scheduler en 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 estado Pending. Los requests garantizan la reserva a nivel de cgroups (cpu.shares y memory.min / memory.low).
  • limits: Configurado por el runtime del contenedor (como containerd o CRI-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:

  1. Para cargas de trabajo con latencia crítica (APIs HTTP, microservicios gRPC), considera no configurar límites de CPU (limits.cpu: null) siempre que utilices requests.cpu precisos y cuentes con cuotas de Namespace (ResourceQuota).
  2. 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:

  1. El subsistema de memoria de cgroups activa el Out-Of-Memory (OOM) Killer del kernel.
  2. El proceso principal recibe una señal SIGKILL forzada (código de salida 137).
  3. 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 siempre container_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?

  1. Cuando se marca el Pod para eliminación, kubelet ejecuta el comando sleep 15 del hook preStop.
  2. Mientras el contenedor duerme 15 segundos, sigue aceptando tráfico en sus sockets existentes.
  3. Paralelamente, el controlador de EndpointSlices retira la IP del Pod de todos los nodos e Ingress controllers.
  4. Tras los 15 segundos, kubelet envía SIGTERM.
  5. La aplicación inicia su apagado grácil (graceful shutdown): completa las conexiones HTTP pendientes y rechaza nuevas peticiones (con Connection: close).
  6. Si la aplicación no ha finalizado dentro de los 45 segundos restantes (60s - 15s), kubelet enviará 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 el livenessProbe mate el contenedor prematuramente durante el arranque.
  • readinessProbe (Anti-patrón crítico): NUNCA verifiques dependencias externas (bases de datos, Redis, APIs de terceros) en el readinessProbe de 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% ni maxUnavailable: 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 con replicas: 1, utiliza maxUnavailable: 1 si 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:

  1. 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.
  2. allowPrivilegeEscalation: false: Bloquea la obtención de privilegios adicionales mediante binarios con bits setuid o setgid.
  3. 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úmenes emptyDir aislados.
  4. capabilities: drop: ["ALL"]: Elimina todas las capacidades POSIX del kernel (como CAP_NET_RAW, CAP_SYS_ADMIN), neutralizando la gran mayoría de técnicas de escape de contenedores.
  5. 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.