Be Software Architecture Wise
Be Wise Apps

Read in English · Lire en français · Auf Deutsch lesen

Sección 10.2 de 10.8 · Avanzado · 25 min de lectura

Contenedores y orquestación con Kubernetes

Videolección

5 min · narrado en inglés, con subtítulos
Solo escuchar (audio)
Leer la transcripción

Ejecutar un contenedor es un solo comando. Ejecutar cuarenta servicios, con varias copias cada uno, en decenas de máquinas, no lo es. Así es como lo hace Kubernetes, y cuándo es más de lo que necesitas.

Piensa en un gran puerto. Las mercancías llegan en contenedores de transporte estándar, así que cualquier grúa, barco o camión puede manejarlos sin saber qué hay dentro. El capitán del puerto no mueve las cajas él mismo. Tiene un tablero que indica qué debe estar dónde. Su personal compara sin cesar el muelle con el tablero, y corrige cualquier diferencia. Los contenedores son las cajas estándar, y Kubernetes es la oficina del capitán del puerto.

Una imagen de contenedor es una pila de capas de solo lectura, más una pequeña configuración y un manifiesto que las enumera. Cada capa se identifica por un digest, un hash de su contenido, así que las capas idénticas se almacenan y se descargan una sola vez. Una etiqueta, como la versión 2.4.1, es un rótulo movible: cualquiera con permisos de push puede apuntarla mañana a otra imagen. Un digest nunca cambia de significado, así que en producción deben ejecutarse imágenes fijadas por digest. Y los estándares abiertos hacen que una imagen creada con cualquier herramienta pueda almacenarse en cualquier registro compatible y ejecutarse en cualquier runtime compatible.

Un clúster de Kubernetes tiene dos mitades: un plano de control, que toma las decisiones, y nodos de trabajo, que ejecutan las cargas de trabajo. Tú, un pipeline o un agente de GitOps declaran el estado deseado al API server, la puerta de entrada. Comprueba quién eres y valida cada solicitud. Solo el API server se comunica con el almacén que guarda el estado del clúster. El scheduler vigila los pods nuevos y elige un nodo para cada uno. El controller manager ejecuta decenas de controladores, cada uno responsable de un tipo de objeto, como mantener en ejecución el número correcto de pods. En cada nodo de trabajo, un agente vigila el API server en busca de los pods que se le asignan, pide al runtime de contenedores que los inicie, comprueba su estado de salud e informa. Fíjate en la dirección. El plano de control nunca inicia sesión en un nodo. Los nodos vigilan el estado en el que deberían estar y convergen hacia él, así que un nodo que quedó aislado simplemente se pone al día.

La idea central es declarativa. No le dices al clúster que inicie tres contenedores. Guardas el estado deseado: deben existir tres copias. Después, los controladores ejecutan un bucle para siempre: observan el estado real, lo comparan con el estado deseado y actúan para eliminar la diferencia. Si un nodo falla en la hora punta de la cena, el controlador ve dos pods donde se desean tres y crea uno; el scheduler lo coloca en un nodo sano. Como los controladores actúan sobre la diferencia actual, un evento perdido o una caída no causan daños duraderos.

Hay dos ajustes que importan más que ningún otro. Un request es lo que reserva el scheduler: un pod que solicita media CPU solo va a un nodo con esa capacidad sin reservar. Un limit es un techo. Un contenedor que supera su límite de CPU se ralentiza; uno que supera su límite de memoria se mata y se reinicia. Las sondas informan a Kubernetes del estado de salud. Mientras falla una sonda de preparación, el pod no recibe tráfico, pero sigue en ejecución. Cuando la sonda de actividad falla repetidamente, el contenedor se reinicia. Así que nunca dejes que la sonda de actividad compruebe la base de datos: un breve failover de la base de datos reiniciaría todos los pods a la vez y convertiría un tropiezo en una caída.

Dimensionemos el servicio de pedidos para la hora punta. Cada pod solicita media CPU, y el autoescalador apunta al sesenta por ciento de eso, con entre tres y doce réplicas. Cada nodo tiene unas tres punto ocho CPU disponibles, así que caben siete de estos pods. A las seis de la tarde, cuatro réplicas están al noventa por ciento de su request. Cuatro por noventa, dividido entre sesenta, da seis réplicas. Veinte minutos después, seis réplicas están al ochenta y cinco por ciento. Seis por ochenta y cinco, dividido entre sesenta, da ocho punto cinco, que se redondea hacia arriba a nueve. Si los nodos ya están llenos, tres pods nuevos esperan, y un nodo nuevo tarda quizá de dos a cuatro minutos en unirse. El autoescalado reacciona a una carga que ya ha llegado. Así que sube el mínimo antes de una hora punta predecible, mantén capacidad de nodos libre y mantén las imágenes pequeñas para que los pods nuevos arranquen rápido.

Kubernetes es una plataforma para construir plataformas, y cobra por ello. Alguien tiene que diseñar la red, la identidad, los secretos, las políticas y la observabilidad. Tiene tres versiones menores al año, cada una con soporte durante unos catorce meses, y los planos de control gestionados solo eliminan una parte del trabajo. Para un equipo pequeño con unos pocos servicios web sin estado, un PaaS o una plataforma de contenedores serverless ofrece la mayoría de los beneficios con una fracción del trabajo. Compensa con muchos servicios, varios equipos y un equipo de plataforma que lo opere. De lo contrario, las organizaciones suelen acabar construyendo, en la práctica, un PaaS peor encima de él.

La sección completa trata los objetos principales, manifiestos de ejemplo y el apagado ordenado. A continuación, en la sección diez punto tres, veremos la aplicación de doce factores. Nos vemos allí.

Lo que necesitas saber

Imágenes, capas y registros

El Capítulo 0 presentó los contenedores como procesos aislados que comparten el kernel del host, empaquetados como imágenes creadas a partir de un Dockerfile (servidores y la nube). Un arquitecto necesita algo más de detalle, porque el diseño de las imágenes afecta a la velocidad de compilación, a la seguridad y a la fiabilidad con la que el mismo software llega a todos los entornos.

Una imagen de contenedor es una pila de capas de solo lectura, cada una un conjunto comprimido de cambios en archivos, más un pequeño documento de configuración (el comando que hay que ejecutar, las variables de entorno, el usuario) y un manifiesto que las enumera. Cada capa y cada manifiesto se identifican por un digest, un hash SHA-256 de su contenido. Las capas idénticas se almacenan y se descargan una sola vez, así que cien imágenes construidas sobre la misma base comparten sus capas. Como cada instrucción de un Dockerfile produce una capa y las herramientas de construcción reutilizan las capas en caché hasta que algo cambia, las instrucciones se ordenan de la que cambia con menos frecuencia a la que cambia con más: la imagen base, después las dependencias y después el código de la aplicación. Las compilaciones multietapa compilan en una etapa y copian solo el resultado en una imagen final pequeña, dejando atrás los compiladores y las herramientas de compilación.

Las imágenes viven en un registro: Docker Hub, GitHub Container Registry, Amazon ECR, Azure Container Registry, Google Artifact Registry o uno autoalojado como Harbor. Una etiqueta (tag) como orders:2.4.1 es un rótulo movible; cualquiera con permisos de push puede apuntarla mañana a otra imagen. Un digest como orders@sha256:3f1c... nunca puede cambiar de significado. Por eso, los despliegues en producción deben resolver las etiquetas a digests, de modo que lo que se probó sea exactamente lo que se ejecuta, y nunca deben usar latest.

Las imágenes pequeñas se descargan más rápido y tienen menos componentes a los que aplicar parches. La práctica habitual es una base mínima (una distribución reducida o una imagen distroless que contiene solo el runtime), ejecutarse como usuario no root, reconstruir con regularidad para incorporar las correcciones de seguridad y analizar las imágenes en el pipeline en busca de vulnerabilidades conocidas. Firmar las imágenes y adjuntarles una lista de materiales de software forma parte de la seguridad de la cadena de suministro, que trata el Capítulo 8 (seguridad de la cadena de suministro de software).

Los estándares OCI

Docker popularizó los contenedores a partir de 2013, y en 2015 Docker y otros fundaron la Open Container Initiative (OCI) bajo la Linux Foundation para evitar que el formato se fragmentara. La OCI mantiene tres especificaciones. La especificación de runtime dice cómo ejecutar un contenedor desempaquetado (namespaces, grupos de control, puntos de montaje, el proceso que hay que iniciar); runc es su implementación de referencia. La especificación de imagen define las capas, los manifiestos y la configuración. La especificación de distribución define la API HTTP que exponen los registros para subir y descargar imágenes. La versión 1.1 de las especificaciones de imagen y de distribución (2024) añadió formas estándar de almacenar otros artefactos en los registros y de vincularlos a una imagen, que es como hoy viajan las firmas y las listas de materiales de software junto a las imágenes que describen.

El resultado práctico es la intercambiabilidad. Una imagen creada con Docker, Podman, Buildah o un servicio de compilación en la nube puede almacenarse en cualquier registro compatible y ejecutarse en cualquier runtime compatible: containerd y CRI-O en un nodo de Kubernetes, una plataforma de contenedores serverless o la computadora portátil de un desarrollador. Por eso los contenedores, a diferencia de la mayoría de los formatos de empaquetado anteriores, se convirtieron en una lengua franca entre el desarrollo, las operaciones y todas las grandes nubes.

Por qué hace falta la orquestación

Ejecutar un contenedor es un solo comando. Ejecutar la plataforma de reparto de comida no lo es: cuarenta servicios, de tres a treinta copias cada uno, repartidos en decenas de máquinas en tres zonas de disponibilidad, con una nueva versión de algo saliendo cada hora. Alguien tiene que decidir en qué máquina se ejecuta cada copia, reiniciar las copias que se caen, sustituir las máquinas que mueren, añadir copias para la hora punta de la cena y retirarlas después, desplegar nuevas versiones sin perder solicitudes, permitir que los servicios se encuentren entre sí mientras las copias aparecen y desaparecen, entregarles la configuración y los secretos, y conectar almacenamiento a los pocos que lo necesitan.

La orquestación de contenedores es software que hace todo esto a partir de una declaración de lo que debería estar en ejecución. Google usó para ello durante más de una década un sistema interno llamado Borg; en 2014 anunció Kubernetes, un sistema de código abierto basado en esa experiencia, lanzó la versión 1.0 en julio de 2015 y lo donó a la recién fundada Cloud Native Computing Foundation, donde en 2018 se convirtió en el primer proyecto en graduarse. Existen alternativas (HashiCorp Nomad, Amazon ECS, plataformas de contenedores serverless), pero Kubernetes es hoy el sustrato común que ofrecen todos los grandes proveedores y la mayoría de las plataformas para instalaciones propias.

Arquitectura de Kubernetes

Arquitectura de Kubernetes: los clientes se comunican con el API server; solo el API server guarda el estado en etcd; el scheduler y el controller manager vigilan el API server; en cada nodo de trabajo, el kubelet vigila sus pods y los inicia mediante el runtime de contenedores, mientras kube-proxy enruta el tráfico de los Serviceskubectl, CI o agente GitOpsdeclara estado deseadoPlano de controlAPI serverautent., admisión, validaciónetcdestado clúster (Raft)Schedulerelige un nodoController managerbucles de reconciliaciónNodo de trab. 1kubeletkube-proxyRuntime contenedorPodPodNodo de trab. 2kubeletkube-proxyRuntime contenedorPodPodkubelets vigilan el API server e informan del estado
Todos los componentes se comunican con el API server; solo el API server se comunica con etcd, y los nodos obtienen su trabajo por extracción en lugar de recibir órdenes.

Cómo funciona Kubernetes: plano de control y nodos

Un clúster de Kubernetes tiene un plano de control, que toma las decisiones, y nodos de trabajo, que ejecutan las cargas de trabajo. El API server es la puerta de entrada: todos los clientes, desde el kubectl de un desarrollador hasta los propios componentes del clúster, leen y escriben objetos a través de su API REST, y él autentica a quien llama, comprueba la autorización y ejecuta controles de admisión que pueden validar o modificar cada solicitud (por ejemplo, rechazar contenedores que se ejecutan como root). Es el único componente que se comunica con etcd, un almacén clave-valor con consistencia fuerte que replica el estado del clúster con el protocolo de consenso Raft entre, normalmente, tres o cinco miembros, de modo que una mayoría sobrevive a la pérdida de uno o dos (consistencia, CAP y PACELC).

El scheduler vigila los pods nuevos sin nodo asignado, descarta los nodos que no pueden ejecutarlos (CPU o memoria no reservadas insuficientes, zona equivocada, hardware que falta, taints que el pod no tolera), puntúa los demás (repartiendo las copias entre zonas, empaquetando de forma eficiente) y registra su elección a través del API server. El controller manager ejecuta decenas de controladores, cada uno responsable de un tipo de objeto: el controlador de ReplicaSet mantiene el número correcto de pods, el controlador de Node detecta los nodos que dejan de informar, el controlador de Job ejecuta tareas hasta completarlas. En una nube, un cloud controller manager también crea balanceadores de carga y conecta discos a través de la API del proveedor.

Cada nodo de trabajo ejecuta un kubelet, un agente que vigila el API server en busca de pods asignados a su nodo, pide al runtime de contenedores (normalmente containerd o CRI-O, a través de la Container Runtime Interface; el shim integrado para Docker Engine se eliminó en la versión 1.24, en 2022) que los inicie, ejecuta sus sondas de estado e informa de su estado. kube-proxy programa las reglas de reenvío de paquetes del nodo para que el tráfico enviado a la dirección virtual de un Service llegue a uno de sus pods; algunos plugins de red, como Cilium, lo sustituyen por un plano de datos eBPF. Un plugin de red da a cada pod su propia dirección IP, y un servicio DNS del clúster (normalmente CoreDNS) permite a los pods encontrar los Services por su nombre.

Fíjate en la dirección de las flechas. El plano de control nunca inicia sesión en un nodo para ejecutar un comando; los nodos vigilan el estado en el que deberían estar y convergen hacia él. Este diseño basado en extracción, de vigilar y converger, es lo que hace robusto a Kubernetes: un nodo que estuvo desconectado diez minutos simplemente se pone al día cuando vuelve. Con Kubernetes gestionado (Amazon EKS, Azure Kubernetes Service, Google Kubernetes Engine y otros), el proveedor opera y actualiza el plano de control; el cliente sigue eligiendo los tipos de nodo, actualizando los grupos de nodos y siendo responsable de todo lo que se despliega en el clúster.

Estado deseado declarativo y bucles de reconciliación

La idea central de Kubernetes es la configuración declarativa. No le dices al clúster que inicie tres contenedores; guardas un objeto que dice que deben existir tres copias de una imagen dada: el estado deseado. Después, los controladores ejecutan bucles de reconciliación: observan el estado real, lo comparan con el estado deseado y actúan para eliminar la diferencia, para siempre.

El bucle de reconciliación

Bucle de reconciliación de un controlador: observa el estado real del clúster, lo compara con el estado deseado y actúa para eliminar la diferencia; después se repiteEstado deseado3 réplicas de orders:2.41 Observa2 pods en ejecución2 Comparafalta 1 pod3 Actúapide 1 pod nuevoEstado real del clúster: nodos, pods, endpointsrepite con cada evento de cambio y periódicamenteleeescribe
Los controladores se activan por nivel: actúan sobre la diferencia actual, no sobre una secuencia de eventos recordada, así que los eventos perdidos y los reinicios no causan daños duraderos.

Sigue lo que ocurre cuando falla un nodo de la zona B durante la hora punta de la cena. El controlador de Node deja de recibir heartbeats y marca el nodo como no preparado; tras un timeout, sus pods quedan marcados para desalojo. El controlador de ReplicaSet del servicio de pedidos observa dos pods en ejecución donde se desean tres y crea un nuevo objeto pod. El scheduler ve un pod sin planificar y lo asigna a un nodo sano, prefiriendo los supervivientes de la zona B u otra zona según las reglas de reparto. El kubelet de ese nodo ve la asignación, descarga la imagen e inicia el contenedor. Cuando su sonda de preparación (readiness) se supera, se actualizan los endpoints que hay detrás del Service de pedidos y el tráfico empieza a llegarle. Ningún componente le dijo a otro qué hacer; cada uno reaccionó al estado que observaba.

Como los controladores se activan por nivel (reaccionan a la diferencia actual) y no se activan por flanco (reaccionando a eventos individuales), un controlador que se cae, se pierde una notificación o se ejecuta dos veces sigue convergiendo al resultado correcto. El mismo patrón se puede ampliar: las definiciones de recursos personalizados añaden nuevos tipos de objeto, y el patrón operador, presentado por CoreOS en 2016, empaqueta un controlador que codifica conocimiento operativo, por ejemplo crear un clúster de PostgreSQL, hacer sus copias de seguridad y gestionar su failover a partir de un único objeto declarado. La sección 10.4 muestra el mismo bucle aplicado a entornos completos mediante GitOps.

El control declarativo tiene un costo: tienes que pensar en términos de estados finales y renunciar a la secuencia exacta. Cuando dos controladores quieren estados deseados distintos para el mismo campo, se pelean. Un ejemplo clásico es un manifiesto de Deployment en Git que dice replicas: 3 mientras un autoescalador lo fija en nueve; cada sincronización de GitOps vuelve a reducir el servicio en plena hora punta. La solución es que cada campo tenga exactamente un dueño, aquí eliminando replicas del manifiesto.

Los objetos principales de Kubernetes

ObjetoQué esEjemplo de reparto de comida
PodUno o más contenedores planificados juntos en un nodo, que comparten una dirección IP y volúmenes. Los pods son desechables y se sustituyen, nunca se reparan.Una copia del servicio de pedidos, quizá con un pequeño sidecar que envía los logs.
ReplicaSetMantiene en ejecución un número indicado de pods idénticos. Rara vez se escribe a mano.Lo crea y lo gestiona el Deployment de pedidos.
DeploymentDeclara la plantilla de pod y el número de réplicas de una aplicación sin estado, y despliega los cambios moviendo pods de un ReplicaSet antiguo a uno nuevo, dentro de unos límites de exceso y de no disponibilidad.Los servicios de pedidos, de carta y de precios.
ServiceUn nombre estable y una IP virtual para un conjunto cambiante de pods seleccionados por etiqueta, con la carga repartida entre ellos.orders.default.svc, usado por el servicio de pago.
Ingress y Gateway APIEnrutan el tráfico HTTP(S) externo hacia los Services por host y ruta. Gateway API (disponible de forma general desde la versión 1.0, en octubre de 2023) es el sucesor más expresivo y orientado a roles: el equipo de plataforma es dueño del Gateway, y los equipos de aplicación, de sus HTTPRoutes.api.example.com/orders enrutado al Service de pedidos, con TLS.
ConfigMapConfiguración no secreta que se entrega a los pods como variables de entorno o archivos.Ajustes de funcionalidades, timeouts, la URL del servicio de carta.
SecretValores sensibles que se entregan como los ConfigMaps. En etcd se guardan solo codificados en base64, salvo que se configure el cifrado en reposo; muchos equipos los sincronizan desde un gestor de secretos externo.La clave de API del proveedor de pagos, la contraseña de la base de datos.
StatefulSetPods con nombres estables, identidades de red estables y sus propios volúmenes persistentes, que se inician y se detienen en orden.Un clúster de Kafka o de Redis autogestionado, si no se usa un servicio gestionado.
DaemonSetExactamente un pod en cada nodo (o en cada nodo seleccionado).Recolector de logs o agente de monitorización del nodo.
Job y CronJobPods que se ejecutan hasta completarse, una vez o según un calendario, con reintentos.Migración de la base de datos antes de un lanzamiento; informe nocturno de pagos a los restaurantes.
HorizontalPodAutoscalerAjusta el número de réplicas de un Deployment para mantener una métrica cerca de un objetivo.Escalar pedidos de 3 a 12 réplicas a medida que sube el uso de CPU por la noche.

Un Deployment y un Service en YAML

Los manifiestos siguientes declaran el servicio de pedidos: la configuración a partir de un Secret, los requests de recursos y las sondas, un Service estable delante y un autoescalador que mantiene entre tres y doce réplicas. El Deployment no tiene a propósito el campo replicas, para que el autoescalador sea el único dueño del número. Se aplican con kubectl apply o, mejor, mediante un pipeline o un agente de GitOps a partir de un repositorio.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders
spec:
  # no replicas field: the autoscaler below owns the count
  selector:
    matchLabels: {app: orders}
  template:
    metadata:
      labels: {app: orders}
    spec:
      containers:
      - name: orders
        image: registry.example.com/orders:2.4.1
        ports: [{containerPort: 8080}]
        env:
        - name: DATABASE_URL
          valueFrom: {secretKeyRef: {name: orders-db, key: url}}
        resources:
          requests: {cpu: 500m, memory: 512Mi}
          limits: {memory: 512Mi}  # no CPU limit: may burst
        readinessProbe:
          httpGet: {path: /ready, port: 8080}
          periodSeconds: 5
        livenessProbe:
          httpGet: {path: /live, port: 8080}
          periodSeconds: 10
          failureThreshold: 3
Un Deployment para el servicio de pedidos. En producción, la imagen se fijaría por digest en lugar de por etiqueta.
apiVersion: v1
kind: Service
metadata:
  name: orders
spec:
  selector: {app: orders}
  ports: [{port: 80, targetPort: 8080}]
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: orders
spec:
  scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: orders}
  minReplicas: 3
  maxReplicas: 12
  metrics:
  - type: Resource
    resource:
      name: cpu
      target: {type: Utilization, averageUtilization: 60}
Un Service que da a los pods una única dirección estable, y un autoescalador que apunta a un uso medio de CPU del 60% de la CPU solicitada.

Requests, limits y calidad de servicio de los recursos

Los requests y limits son las líneas con más consecuencias de un manifiesto. Un request es lo que reserva el scheduler: un pod que solicita 500 milicores (media CPU) y 512 MiB de memoria solo se coloca en un nodo con esa capacidad sin reservar, y tiene garantizada esa parte cuando hay contención. Un limit es un techo que impone el kernel: un contenedor que supera su límite de CPU se estrangula (se ralentiza), y uno que supera su límite de memoria se mata y se reinicia, el conocido estado OOMKilled. Kubernetes deriva de estos ajustes una clase de calidad de servicio: Guaranteed (los requests son iguales a los limits para todos los recursos), Burstable (requests definidos, limits más altos o ausentes) y BestEffort (nada definido; son los primeros en ser desalojados cuando un nodo se queda corto).

Los profesionales discrepan sobre los límites de CPU. Un grupo los define en todas partes por previsibilidad y por equidad entre equipos; otro define requests de CPU precisos y ningún límite de CPU, porque el estrangulamiento provoca picos de latencia incluso cuando el nodo tiene CPU libre, y los requests ya garantizan a cada pod su parte. Con la memoria hay poco debate: fija el límite igual al request, porque la memoria no se puede recuperar de un proceso sin matarlo, y un pod que usa más de lo que solicitó es el primer candidato al desalojo. En cualquier caso, los requests deben basarse en mediciones; los requests inflados desperdician dinero (el nodo está reservado pero inactivo), y los subestimados causan problemas de vecinos ruidosos y un autoescalado engañoso.

El HorizontalPodAutoscaler calcula réplicas deseadas = techo(réplicas actuales × métrica actual ÷ objetivo), midiendo el uso de CPU como porcentaje del request. Escala hacia arriba con rapidez pero, por defecto, espera una ventana de estabilización de cinco minutos antes de escalar hacia abajo, para evitar el flapping. Cuando los pods nuevos no caben en los nodos existentes, quedan en estado Pending, y un autoescalador de nodos (el Cluster Autoscaler, o Karpenter en algunos proveedores) añade máquinas, lo que lleva minutos en lugar de segundos. El autoescalado de pods y el de nodos son bucles separados con retrasos separados, y hay que planificar ambos para la hora punta.

Ejemplo práctico

Ejemplo práctico: dimensionar el servicio de pedidos para la hora punta de la cena

Cada pod de pedidos solicita 500m de CPU y 512 MiB, con un límite de memoria de 512 MiB y sin límite de CPU. Los nodos tienen 4 vCPU y 16 GiB, de los que unos 3.8 vCPU y 14.5 GiB son asignables tras las reservas del sistema (cifras ilustrativas). El HPA apunta a un 60% de CPU, con entre 3 y 12 réplicas.

Pods por nodoCPU: 3.8 ÷ 0.5 = 7 pods. Memoria: 14.5 GiB ÷ 0.5 GiB = 29 pods. La CPU es la restricción limitante: 7 pods de pedidos por nodo si no se ejecuta nada más en él.
18:00, empieza la hora punta4 réplicas con una media del 90% de su request de CPU. Deseadas = techo(4 × 90 ÷ 60) = 6 réplicas.
18:20, sigue subiendo6 réplicas al 85%. Deseadas = techo(6 × 85 ÷ 60) = techo(8.5) = 9 réplicas, que solicitan 4.5 vCPU en total.
Capacidad de los nodosSi otros servicios ya llenan los nodos, tres de los pods nuevos quedan en Pending; el autoescalador de nodos añade un nodo, que tarda quizá de dos a cuatro minutos en unirse y descargar la imagen.
Respuesta de diseñoSube minReplicas antes de la hora punta predecible (escalado programado), mantén algo de capacidad de nodos libre y mantén la imagen pequeña para que los pods nuevos arranquen rápido.
Efecto de un request equivocadoSi cada pod en realidad solo necesitara 250m pero el request se quedara en 500m, la misma carga de la hora punta (225m por pod) aparecería como un 45% en lugar de un 90%: el HPA no escalaría hasta que el uso superara los 300m por pod, y el clúster reservaría el doble de CPU de la que necesita.

El autoescalado reacciona a una carga que ya ha llegado; para un pico diario predecible, el escalado previo programado es más barato que los pedidos perdidos.

Los requests guían tanto al scheduler como al autoescalador, así que medirlos es una tarea arquitectónica de primer orden, no un detalle menor de ajuste.

Sondas de actividad, de preparación y de arranque

Las sondas son la forma en que Kubernetes se entera del estado de salud. Una sonda de preparación (readiness) responde a si este pod debe recibir tráfico ahora; mientras falla, el pod se retira de los endpoints de su Service, pero sigue en ejecución. Una sonda de actividad (liveness) responde a si el proceso está atascado sin posibilidad de recuperarse; cuando falla repetidamente, el kubelet reinicia el contenedor. Una sonda de arranque (startup) retiene las otras dos hasta que una aplicación de arranque lento ha terminado de inicializarse.

El error más dañino es una sonda de actividad que comprueba las dependencias. Si el endpoint de actividad del servicio de pedidos consulta la base de datos, un failover de la base de datos de treinta segundos hace que todos los pods de pedidos fallen la sonda de actividad a la vez, Kubernetes los reinicia todos, todos se vuelven a conectar en el mismo momento, y un breve tropiezo se convierte en una caída. La sonda de actividad solo debe comprobar que el propio proceso puede responder; la de preparación puede tener en cuenta si el pod puede hacer trabajo útil, aunque incluso ahí muchos equipos evitan dejar todos los pods como no preparados por una dependencia compartida, y prefieren hacer fallar rápido las solicitudes con un circuit breaker (consulta patrones de resiliencia).

Las sondas funcionan junto con el apagado ordenado. Cuando se elimina un pod durante un despliegue progresivo o una reducción de escala, Kubernetes lo retira de los endpoints y envía al proceso SIGTERM; el proceso debe dejar de aceptar trabajo nuevo, terminar las solicitudes en curso y salir dentro del periodo de gracia (30 segundos por defecto) antes de que lo maten. Como la retirada de los endpoints se propaga de forma asíncrona, un breve retardo preStop evita enviar solicitudes a un pod que ya ha empezado a detenerse. La sección 10.3 vuelve sobre esto como el principio de desechabilidad de los doce factores.

Cuándo Kubernetes es excesivo

Kubernetes es una plataforma de propósito general para construir plataformas, y cobra por esa generalidad. Alguien tiene que diseñar la red, la entrada de tráfico, la identidad, los secretos, las políticas, la observabilidad y la multitenencia; seguir el ritmo de tres versiones menores al año, cada una con soporte durante unos catorce meses, además de los complementos que dependen de ellas; y estar de guardia tanto para el clúster como para las aplicaciones. Los planos de control gestionados eliminan parte de este trabajo, no la mayor parte.

Para un equipo pequeño que ejecuta un puñado de servicios web sin estado, un PaaS o una plataforma de contenedores serverless (Google Cloud Run, Azure Container Apps, Amazon ECS con Fargate y similares) ofrece la mayoría de los beneficios (contenedores, autoescalado, despliegues graduales, TLS gestionado) con una fracción de la carga operativa. Kubernetes empieza a compensar cuando hay muchos servicios y varios equipos, un equipo de plataforma que lo opere, cargas de trabajo con necesidades poco habituales (sistemas con estado, GPU, trabajos por lotes, controladores personalizados), el requisito de ejecutar la misma plataforma en instalaciones propias y en varias nubes, o un ecosistema de herramientas que la organización quiere usar. Muchas organizaciones lo han adoptado porque estaba de moda y después han pasado años construyendo, en la práctica, un PaaS peor encima de él. La sección 10.7 explica cómo evitan ese destino los equipos de plataforma (ingeniería de plataformas).

Comprueba lo que has entendido

¿Por qué un despliegue en producción debe referirse a una imagen por digest y no por etiqueta?

Una etiqueta es un rótulo movible que se puede volver a apuntar a otro contenido; un digest es un hash del contenido y siempre identifica exactamente la imagen que se probó.

Un nodo muere. ¿Qué componentes actúan, y en qué orden, para restaurar la tercera réplica?

El controlador de Node marca el nodo como no preparado y sus pods se desalojan; el controlador de ReplicaSet ve que hay muy pocos pods y crea uno; el scheduler lo asigna a un nodo sano; el kubelet de ese nodo lo inicia a través del runtime; cuando supera la sonda de preparación, se une a los endpoints del Service.

¿Qué le ocurre a un contenedor que supera su límite de memoria, y a uno que supera su límite de CPU?

Superar el límite de memoria hace que el contenedor se mate (OOMKilled) y se reinicie; superar el límite de CPU solo lo estrangula.

¿Por qué es peligroso comprobar la base de datos en una sonda de actividad?

Un breve problema de la base de datos hace que todos los pods fallen la sonda de actividad a la vez, así que Kubernetes los reinicia todos de golpe, y un corto tropiezo de una dependencia se convierte en una caída total.

Siguiente · 10.3Doce factoresLos doce factores al completo, con lo que significa cada uno, por qué importa y dónde ha envejecido; los principios que van más allá (desechabilidad, inmutabilidad, observabilidad, seguridad); y una auditoría práctica de un servicio existente con correcciones priorizadas.Continuar →

Fuentes y lecturas recomendadas

¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.

Pregunta: “¿Debería la plataforma de reparto de comida fijar límites de CPU en sus pods de Kubernetes, o solo requests?”