En la lección anterior enseñamos a Kubernetes a distinguir un contenedor vivo de una aplicación sana. Rutas Norte ya sabe si cada componente funciona. Lo que todavía no sabe es cuánto consume. Y ese dato no es un lujo: en 03-04 fijamos las requests y los limits de cada componente prácticamente a ojo, prometiendo que los calibraríamos observando el consumo real. Sin ese dato, la mitad de nuestro clúster está reservando memoria que nadie usa mientras la otra mitad sufre throttling de CPU en silencio.

Esta lección presenta la primera fuente de datos de consumo real del clúster: metrics-server, un componente ligero que recoge el uso de CPU y memoria de nodos y pods y lo publica dentro de la propia API de Kubernetes. Es la pieza que hace funcionar kubectl top, y también la que hará funcionar el autoescalado del módulo 9. Vamos a entender exactamente qué mide, de dónde saca los datos, y —tan importante como lo anterior— qué no puede hacer, porque esas limitaciones son justo la razón por la que en la lección siguiente instalaremos Prometheus.

Contenido

  1. Qué lugar ocupa metrics-server en el ecosistema de observabilidad
  2. De dónde salen los datos: kubelet, cAdvisor y la Summary API
  3. Cómo se publica en la API: la capa de agregación y el APIService
  4. Instalación: el addon de minikube y el manifiesto oficial
  5. kubectl top nodes: leer correctamente cada columna
  6. kubectl top pods: --containers, -A, --sort-by y sus matices
  7. Las limitaciones esenciales y por qué hace falta Prometheus
  8. El uso central: recalibrar las requests y limits de Rutas Norte
  9. Por qué el HPA y el VPA dependen de este componente
  10. Diagnóstico de los fallos típicos
  11. Errores comunes y consejos
  12. Ejercicios

  1. Qué lugar ocupa metrics-server en el ecosistema de observabilidad

Antes de instalar nada conviene situar la pieza, porque es el componente que más confusión genera del módulo. Mucha gente lo instala esperando un sistema de monitorización y se lleva una decepción.

metrics-server no es un sistema de monitorización. Es un componente de infraestructura interna del clúster con un propósito muy concreto y muy acotado:

Proporcionar a la API de Kubernetes el consumo actual de CPU y memoria de nodos y pods, para que los mecanismos de autoescalado del propio Kubernetes puedan tomar decisiones.

Todo lo demás —que puedas verlo con kubectl top— es un efecto secundario útil.

Aspecto metrics-server Prometheus (07-03)
Propósito de diseño Alimentar al HPA y al VPA Observar, consultar y alertar
Métricas que recoge Solo CPU y memoria Cualquier métrica expuesta
Histórico Ninguno (~1-2 min en memoria) Semanas o meses en disco
Consultas No hay lenguaje de consulta PromQL completo
Alertas No Sí, con Alertmanager
Almacenamiento Memoria volátil Base de datos de series temporales
Consumo de recursos Muy bajo (~50 Mi) Alto (GB de RAM y disco)
¿Es obligatorio? Prácticamente sí No, pero imprescindible en producción
Granularidad 15 s por defecto Configurable, típico 15-30 s

Los dos coexisten en cualquier clúster serio: metrics-server porque el HPA lo necesita, Prometheus porque hace falta saber qué pasó ayer.

Una tercera confusión frecuente: metrics-server no sustituye a las sondas de 07-01. Las sondas responden "¿está sano?"; metrics-server responde "¿cuánto gasta?". Un pod puede consumir 5 milicores y estar completamente colgado.

  1. De dónde salen los datos: kubelet, cAdvisor y la Summary API

Para no tratar metrics-server como una caja negra, hay que seguir el camino del dato desde el kernel hasta tu terminal.

El origen: los cgroups del kernel

Cuando el runtime de contenedores crea el contenedor de api-reservas, el kernel de Linux lo mete en un grupo de control (cgroup). El kernel contabiliza ahí, entre otras cosas:

  • cpuacct.usage: nanosegundos de CPU acumulados por ese cgroup desde que arrancó.
  • memory.current: bytes de memoria en uso ahora mismo por ese cgroup.

Esos son los datos primarios. Todo lo demás son capas de transporte.

cAdvisor: el lector de cgroups

cAdvisor (Container Advisor) es un componente de Google que lee los cgroups y los traduce a métricas de contenedor con sentido. Desde hace años está integrado dentro del binario del kubelet: no es un pod aparte y no hay que instalarlo. Cada kubelet lleva su propio cAdvisor que examina los contenedores de su nodo.

La Summary API del kubelet

El kubelet expone lo que cAdvisor recoge en un endpoint HTTPS autenticado:

https://<ip-del-nodo>:10250/stats/summary

Devuelve un JSON con el consumo del nodo y de cada pod y contenedor. Una porción simplificada:

{
  "node": {
    "nodeName": "rutas-norte-worker-1",
    "cpu": { "time": "2026-08-06T09:14:20Z", "usageNanoCores": 842000000 },
    "memory": { "workingSetBytes": 3221225472 }
  },
  "pods": [
    {
      "podRef": { "name": "api-reservas-7d9f8c4b5-x2klm", "namespace": "rutas-norte-pro" },
      "containers": [
        {
          "name": "api",
          "cpu": { "usageNanoCores": 187000000 },
          "memory": { "workingSetBytes": 312475648 }
        }
      ]
    }
  ]
}

Dos campos merecen atención especial, porque explican todo lo que verás después:

  • usageNanoCores: nanocores consumidos. Un core = 1 000 000 000 nanocores. Los 187 000 000 del ejemplo son 187 milicores, es decir, 187m en la notación de Kubernetes.
  • workingSetBytes: el "conjunto de trabajo". Es la memoria residente menos las páginas de caché de fichero que el kernel podría recuperar sin problema. Es la métrica que el kubelet usa para decidir a quién desalojar por presión de memoria, y es la que verás en kubectl top. No es lo mismo que el RSS que te da ps, y por eso a veces los números no cuadran con lo que ves dentro del contenedor.

metrics-server: el agregador

metrics-server es un único Deployment (una réplica en clústeres pequeños) que:

  1. Descubre los nodos consultando la API de Kubernetes.
  2. Consulta la Summary API de cada kubelet cada 15 segundos (parámetro --metric-resolution).
  3. Guarda los dos últimos puntos de cada serie en memoria.
  4. Calcula la tasa de CPU dividiendo el incremento de nanocores acumulados entre el tiempo transcurrido.
  5. Sirve esos valores a través de la API de Kubernetes.

Punto clave: no escribe nada en disco y no guarda histórico. Si el pod de metrics-server se reinicia, kubectl top deja de funcionar durante unos 30 segundos y luego vuelve, sin ningún dato del pasado.

Esa división también explica por qué la CPU tarda en aparecer: como es una tasa, hacen falta dos muestras. Un pod recién creado no muestra CPU hasta que metrics-server tiene dos lecturas suyas.

  1. Cómo se publica en la API: la capa de agregación y el APIService

Aquí está la parte elegante del diseño. metrics-server no expone un puerto propio al que tú te conectes. En su lugar, se registra dentro de la API de Kubernetes mediante la capa de agregación (API Aggregation Layer).

El mecanismo es el mismo que vimos en 06-06 para las CRDs, pero por otra vía: en lugar de que el API Server guarde los objetos en etcd, delega las peticiones de un grupo de API concreto a un servicio externo.

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1beta1.metrics.k8s.io
spec:
  group: metrics.k8s.io          # el grupo de API que se delega
  version: v1beta1
  groupPriorityMinimum: 100
  versionPriority: 100
  service:
    name: metrics-server         # a qué Service se reenvían las peticiones
    namespace: kube-system
    port: 443
  insecureSkipTLSVerify: true    # en clústeres de prueba; en producción, caBundle

Esto significa que cuando ejecutas:

kubectl top pods -n rutas-norte-pro

lo que ocurre por debajo es una petición REST normal y corriente a la API de Kubernetes:

kubectl get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/rutas-norte-pro/pods" | jq

El API Server ve que la ruta pertenece al grupo metrics.k8s.io, consulta su tabla de APIService, y reenvía la petición al Service metrics-server de kube-system. La respuesta vuelve al cliente como si la hubiera generado el propio API Server.

flowchart TD
    U["kubectl top pods"] --> API[API Server]
    API -->|"grupo metrics.k8s.io<br/>delegado por APIService"| MS[metrics-server<br/>Deployment en kube-system]
    MS -->|"HTTPS :10250<br/>/stats/summary<br/>cada 15 s"| K1[kubelet nodo 1]
    MS -->|"HTTPS :10250"| K2[kubelet nodo 2]
    MS -->|"HTTPS :10250"| K3[kubelet nodo 3]
    K1 --> CA1[cAdvisor integrado]
    K2 --> CA2[cAdvisor integrado]
    K3 --> CA3[cAdvisor integrado]
    CA1 --> CG1[(cgroups del kernel)]
    CA2 --> CG2[(cgroups del kernel)]
    CA3 --> CG3[(cgroups del kernel)]
    API -.->|"misma API"| HPA[HorizontalPodAutoscaler]

Las consecuencias prácticas de este diseño son importantes:

  • Autenticación y autorización unificadas: el RBAC que estudiaremos en 08-01 se aplica igual a kubectl top que a kubectl get pods. Quien no tenga permiso get sobre pods.metrics.k8s.io no verá nada.
  • Un solo punto de entrada: no hay que abrir puertos ni exponer servicios adicionales.
  • Fragilidad concreta: si el APIService está False (por ejemplo, porque el pod de metrics-server no arranca), algunas operaciones de descubrimiento de la API se ralentizan para todo el clúster. Verás avisos como couldn't get resource list for metrics.k8s.io/v1beta1. Es un efecto secundario molesto que conviene reconocer.

Comprobar el estado del APIService:

kubectl get apiservice v1beta1.metrics.k8s.io
NAME                     SERVICE                      AVAILABLE   AGE
v1beta1.metrics.k8s.io   kube-system/metrics-server   True        41d

AVAILABLE: True es la señal de que todo está bien. Si pone False (MissingEndpoints) o False (FailedDiscoveryCheck), ve directo al apartado 10.

  1. Instalación: el addon de minikube y el manifiesto oficial

En nuestro clúster de prácticas

Desde el módulo 1 venimos trabajando con minikube en el perfil rutas-norte, y ya declaramos metrics-server entre los addons necesarios. Si no lo has activado todavía:

# Activar el addon en nuestro perfil
minikube addons enable metrics-server -p rutas-norte

# Comprobar que el addon aparece habilitado
minikube addons list -p rutas-norte | grep metrics-server
| metrics-server              | rutas-norte | enabled ✅   | Kubernetes |

El addon despliega el Deployment en kube-system con la configuración ya adaptada a minikube (incluido el flag del apartado siguiente).

kubectl -n kube-system get deploy,pod -l k8s-app=metrics-server
NAME                             READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/metrics-server   1/1     1            1           2m18s

NAME                                  READY   STATUS    RESTARTS   AGE
pod/metrics-server-7bf7d58749-qk4hn   1/1     Running   0          2m18s

Espera entre 30 y 60 segundos antes de esperar datos: metrics-server necesita al menos dos ciclos de recolección.

En un clúster real

En un clúster que no sea minikube se instala aplicando el manifiesto oficial del proyecto:

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

Ese manifiesto crea, en kube-system: un ServiceAccount, un ClusterRole con permisos de lectura sobre nodos y pods, los ClusterRoleBinding correspondientes (incluida la delegación de autenticación al API Server), el Service, el Deployment y el APIService que vimos antes.

El flag --kubelet-insecure-tls y su advertencia

Este es el punto que más veces bloquea una instalación. metrics-server se conecta por HTTPS al puerto 10250 de cada kubelet. Por defecto verifica el certificado del kubelet contra la CA del clúster. El problema: en muchas instalaciones (minikube, kubeadm sin rotación de certificados de servidor del kubelet, algunos clústeres gestionados) el kubelet usa un certificado autofirmado que no está firmado por la CA del clúster, o cuyo nombre no coincide con la IP del nodo.

Resultado: metrics-server no puede conectarse y kubectl top no devuelve nada. La solución que aparece en todos los foros:

# Fragmento del Deployment de metrics-server
spec:
  template:
    spec:
      containers:
        - name: metrics-server
          args:
            - --cert-dir=/tmp
            - --secure-port=10250
            - --kubelet-preferred-address-types=InternalIP,Hostname,InternalDNS,ExternalDNS
            - --kubelet-use-node-status-port
            - --metric-resolution=15s
            - --kubelet-insecure-tls        # <-- el flag en cuestión

Advertencia de seguridad. --kubelet-insecure-tls desactiva la verificación del certificado del kubelet. La conexión sigue cifrada, pero metrics-server ya no comprueba que el interlocutor sea realmente el kubelet legítimo del nodo. Un atacante con capacidad de interponerse en la red del plano de control podría suplantar a un kubelet y devolver métricas falsas. Eso, en un clúster con HPA, es un vector de ataque real: métricas de CPU inventadas pueden provocar un escalado masivo (coste) o impedir un escalado necesario (denegación de servicio).

Es aceptable en minikube y en entornos de desarrollo. No lo es en rutas-norte-pro. La solución correcta en producción es habilitar la rotación de certificados de servidor del kubelet (--rotate-server-certificates=true en el kubelet y aprobación de las CSR kubernetes.io/kubelet-serving), de modo que los certificados los firme la CA del clúster y la verificación funcione. En un clúster gestionado (EKS, AKS, GKE, que veremos en 10-06), el proveedor ya lo trae resuelto y el flag no hace falta.

El resto de argumentos, brevemente:

Argumento Para qué sirve
--metric-resolution=15s Cada cuánto se consulta a los kubelets. No bajar de 10 s: satura los kubelets
--kubelet-preferred-address-types En qué orden intentar direccionar al nodo. InternalIP primero suele ser lo correcto
--kubelet-use-node-status-port Usar el puerto que el propio nodo declara en su estado, en vez de asumir 10250
--cert-dir=/tmp Dónde guarda su certificado de servicio propio

  1. kubectl top nodes: leer correctamente cada columna

Con metrics-server funcionando, ya podemos mirar.

kubectl top nodes
NAME                     CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
rutas-norte-control      412m         20%    1854Mi          48%
rutas-norte-worker-1     1834m        45%    5218Mi          67%
rutas-norte-worker-2     2701m        67%    6944Mi          89%
rutas-norte-worker-3     287m         7%     1102Mi          14%

Cómo se lee esto correctamente, columna a columna:

  • CPU(cores): milicores consumidos ahora mismo por todo lo que corre en el nodo (pods del sistema incluidos). 1834m significa 1,834 núcleos ocupados de forma continua.
  • CPU%: el porcentaje sobre la capacidad asignable del nodo (node.status.allocatable.cpu), no sobre la capacidad total ni sobre la suma de requests. Es un detalle importante: allocatable = capacidad total menos lo reservado para el sistema operativo y para el kubelet.
  • MEMORY(bytes): el workingSetBytes del nodo. Repito el matiz del apartado 2: no incluye la caché de página recuperable, así que casi siempre es menor que lo que muestra free -m dentro del nodo.
  • MEMORY%: porcentaje sobre allocatable.memory.

Comprobar el allocatable de un nodo para verificar el cálculo:

kubectl get node rutas-norte-worker-2 \
  -o jsonpath='{.status.allocatable.cpu}{"\t"}{.status.allocatable.memory}{"\n"}'
4        7808036Ki

Con 4 núcleos asignables y 2701m consumidos: 2701 / 4000 = 67,5 %. Cuadra.

Lectura operativa de la tabla anterior para Rutas Norte:

  • rutas-norte-worker-2 está al 89 % de memoria. Peligroso: cuando el kubelet detecta presión de memoria empieza a desalojar pods, empezando por los de QoS BestEffort y Burstable que exceden sus requests (repasa 03-05). Hay que investigar qué corre ahí y probablemente reequilibrar.
  • rutas-norte-worker-3 está prácticamente vacío al 7 % y 14 %. Eso apunta a un problema de planificación: quizá tiene un taint que no habíamos previsto, o los pods tienen reglas de afinidad que lo excluyen (06-05).

Flags útiles:

# Ordenar por consumo de CPU descendente
kubectl top nodes --sort-by=cpu

# Ordenar por memoria
kubectl top nodes --sort-by=memory

# Solo los nodos con una etiqueta concreta
kubectl top nodes -l node-role.kubernetes.io/worker=

# Sin cabeceras, para procesar con scripts
kubectl top nodes --no-headers

  1. kubectl top pods: --containers, -A, --sort-by y sus matices

kubectl top pods -n rutas-norte-pro
NAME                                      CPU(cores)   MEMORY(bytes)
api-reservas-7d9f8c4b5-x2klm              187m         305Mi
api-reservas-7d9f8c4b5-mn8pq              203m         318Mi
api-reservas-7d9f8c4b5-vr4tz              174m         297Mi
postgres-reservas-0                       340m         1720Mi
redis-cache-0                             28m          412Mi
tienda-web-6c8b9d7f4-hj3ks                12m          38Mi
tienda-web-6c8b9d7f4-lp9wx                9m           36Mi
worker-notificaciones-5f7c8b9d4-tz2mv     45m          186Mi

Diferencia crítica respecto a top nodes: aquí NO hay columnas de porcentaje. kubectl top pods no muestra ningún %, porque no sabría respecto a qué calcularlo (¿sobre el limit? ¿sobre la request? ¿sobre el nodo?). Esa comparación tienes que hacerla tú, y en el apartado 8 la haremos.

--containers: desglose por contenedor

Con la llegada de los sidecars en 06-04, un pod ya no es un contenedor. postgres-reservas-0 tiene el contenedor de PostgreSQL y el exportador de métricas; worker-notificaciones tiene el worker y el adaptador de logs. Sumarlos oculta información valiosa:

kubectl top pods -n rutas-norte-pro --containers
POD                                     NAME              CPU(cores)   MEMORY(bytes)
postgres-reservas-0                     postgres          327m         1698Mi
postgres-reservas-0                     exportador-pg     13m          22Mi
worker-notificaciones-5f7c8b9d4-tz2mv   worker            41m          171Mi
worker-notificaciones-5f7c8b9d4-tz2mv   adaptador-logs    4m           15Mi
api-reservas-7d9f8c4b5-x2klm            api               187m         305Mi

Ahora sabemos que el sidecar exportador cuesta 13m de CPU y 22Mi: barato, como esperábamos. Y que el adaptador de logs cuesta 4m y 15Mi. Estos números importan porque, multiplicados por el número de réplicas, un sidecar aparentemente inocuo puede consumir un nodo entero en un clúster grande.

Otras opciones

# Todos los namespaces del clúster, ordenado por memoria descendente
kubectl top pods -A --sort-by=memory

# Solo los pods de un componente concreto (usa el selector de etiquetas de 02-07)
kubectl top pods -n rutas-norte-pro -l app=api-reservas

# Todos los componentes de la plataforma, en los tres entornos
kubectl top pods -A -l app.kubernetes.io/part-of=rutas-norte --sort-by=cpu

# Un pod concreto, desglosado
kubectl top pod postgres-reservas-0 -n rutas-norte-pro --containers

# Sin cabeceras y ordenado, para un script de informe
kubectl top pods -n rutas-norte-pro --no-headers --sort-by=memory

--sort-by solo admite cpu o memory. Ojo con un comportamiento poco intuitivo: el ordenamiento se hace sobre los valores numéricos, pero la salida se imprime en las unidades de Kubernetes, y 1024Mi va después de 2Gi alfabéticamente aunque sea menor. Confía en el orden que da --sort-by, no en la lectura visual.

Comparativa rápida de comandos

Comando Qué responde
kubectl top nodes ¿Qué nodos están saturados? ¿Hay desequilibrio?
kubectl top pods -A --sort-by=memory ¿Quién se está comiendo la memoria del clúster?
kubectl top pods -n X --containers ¿Cuánto cuesta cada sidecar?
kubectl top pods -l app=api-reservas ¿Consumen igual todas las réplicas de un componente?

Esa última pregunta es más útil de lo que parece. Si de seis réplicas de api-reservas una consume 800m y las demás 190m, tienes un problema de reparto de tráfico o una petición patológica atendida por ese pod concreto.

  1. Las limitaciones esenciales y por qué hace falta Prometheus

Este apartado es el más importante de la lección, porque define cuándo no debes usar esta herramienta.

Limitación 1: no hay histórico. Ninguno.

metrics-server guarda los dos últimos puntos de cada serie en memoria. No hay base de datos, no hay fichero, no hay retención configurable. Preguntas que no puedes responder con kubectl top:

  • "¿Cuánta memoria consumía api-reservas anoche a las 03:00, cuando cayó?"
  • "¿Ha ido creciendo el consumo de postgres-reservas en las últimas dos semanas?"
  • "¿Cuál fue el pico de CPU durante el puente de mayo?"

Todas ellas son exactamente las preguntas que se hacen después de un incidente, y para todas la respuesta de metrics-server es silencio.

Limitación 2: son datos instantáneos con resolución gruesa

Con --metric-resolution=15s y ventanas de cálculo de la tasa de CPU de decenas de segundos, un pico de CPU de 3 segundos no aparece. Se promedia y desaparece. Para api-reservas, cuyos picos de latencia se miden en segundos, esa resolución es insuficiente para diagnosticar.

También significa que dos ejecuciones consecutivas de kubectl top pueden dar el mismo valor: si ambas caen dentro del mismo ciclo de recolección, estás leyendo la misma muestra.

Limitación 3: solo CPU y memoria

Nada de disco, nada de red, nada de métricas de aplicación. No sabrás cuántas peticiones por segundo atiende api-reservas, ni cuántas reservas se han confirmado, ni cuántas conexiones tiene abiertas PostgreSQL. Nada del negocio.

Limitación 4: no se puede alertar

No hay consultas, no hay umbrales, no hay notificaciones. kubectl top es una herramienta interactiva: alguien tiene que estar delante escribiéndola. A las tres de la madrugada no hay nadie delante.

Limitación 5: no distingue el throttling

En 03-04 vimos que un contenedor que alcanza su limits.cpu no se mata: se le estrangula (throttling). El síntoma es latencia alta con un consumo de CPU que se queda pegado al límite. kubectl top te muestra los milicores, pero no el contador de periodos estrangulados (container_cpu_cfs_throttled_periods_total), que es el dato que confirma el diagnóstico. Ese contador solo lo verás en Prometheus.

Tabla resumen: qué preguntar a quién

Pregunta metrics-server Prometheus
¿Cuánto consume api-reservas ahora?
¿Cuánto consumía ayer a las 03:00?
¿Está creciendo su consumo con el tiempo?
¿Cuántas peticiones por segundo atiende?
¿Está sufriendo throttling de CPU?
¿Cuál es el percentil 95 de latencia?
Avísame si sube de X
Escalar automáticamente por CPU ✅ (vía adaptador)
¿Qué nodo está más cargado ahora mismo?
Coste de instalación y operación Trivial Considerable

La conclusión honesta: metrics-server es una fotografía; Prometheus es la película. Necesitas las dos cosas, y por razones distintas. La foto es gratis y siempre está ahí; la película cuesta dinero, disco y mantenimiento, pero es la única que te dice qué pasó.

  1. El uso central: recalibrar las requests y limits de Rutas Norte

Este es el uso legítimo y de más valor de kubectl top, y el que cierra la promesa de 03-04.

Recordatorio de por qué importa

  • requests: lo que el planificador reserva en el nodo. Determina dónde cabe el pod y qué QoS tiene. Reservar de más desperdicia clúster (nodos con el 30 % de uso real y "llenos" según el planificador); reservar de menos hace que los pods vayan a nodos donde no caben de verdad y sufran.
  • limits: el techo. Superarlo en CPU provoca throttling; superarlo en memoria provoca OOMKilled.

El método de recalibración

No se recalibran las requests mirando una sola vez. El procedimiento honesto:

  1. Observar durante un periodo representativo: al menos una semana, incluyendo un pico. Para Rutas Norte hay que incluir un puente o el inicio de las vacaciones.
  2. Tomar muestras periódicas, no una sola lectura. Como metrics-server no guarda histórico, esto significa un script que ejecute kubectl top cada pocos minutos y acumule.
  3. Calcular percentiles, no medias. La media miente cuando hay picos.
  4. Aplicar los criterios:
Recurso Criterio de cálculo
requests.cpu Percentil 50-70 del uso observado. No hace falta reservar el pico: la CPU es comprimible
limits.cpu 2-4 veces la request, o sin límite si el componente tolera bien la variabilidad
requests.memory Percentil 95-99 del uso observado, más margen. La memoria no es comprimible
limits.memory Igual o muy cercano a la request, más un 20-30 % de margen

La asimetría entre CPU y memoria es fundamental y hay que entenderla: si te pasas de CPU, el sistema te ralentiza y sobrevives. Si te pasas de memoria, el kernel te mata. Por eso la memoria se dimensiona por el percentil alto y la CPU por el central.

Script de recolección para una semana

#!/bin/bash
# recolecta-consumo.sh
# Muestrea el consumo de los pods de Rutas Norte cada 2 minutos.
# Limitación deliberada: esto es un apaño porque metrics-server no guarda
# histórico. En 07-03 lo sustituiremos por Prometheus, que hace esto de serie.

SALIDA="/var/log/rutasnorte/consumo-$(date +%Y%m%d).csv"
echo "marca_tiempo,namespace,pod,contenedor,cpu_milicores,memoria_mib" > "$SALIDA"

while true; do
  AHORA=$(date --iso-8601=seconds)
  kubectl top pods -A --containers --no-headers \
    -l app.kubernetes.io/part-of=rutas-norte 2>/dev/null |
  while read -r ns pod contenedor cpu mem; do
    # Limpiar sufijos: 187m -> 187, 305Mi -> 305
    cpu_num="${cpu%m}"
    mem_num="${mem%Mi}"
    echo "$AHORA,$ns,$pod,$contenedor,$cpu_num,$mem_num" >> "$SALIDA"
  done
  sleep 120
done

Nota importante para principiantes: el comando anterior con -A devuelve el namespace como primera columna, por eso read toma cinco campos. Si lo ejecutas sin -A, quita la variable ns.

Y el análisis de los datos recogidos:

# Percentil 95 de memoria de api-reservas durante la semana
awk -F, '$4=="api" {print $6}' /var/log/rutasnorte/consumo-*.csv |
  sort -n |
  awk '{v[NR]=$1} END {print "p95 memoria: " v[int(NR*0.95)] " Mi"}'

# Percentil 70 de CPU de api-reservas
awk -F, '$4=="api" {print $5}' /var/log/rutasnorte/consumo-*.csv |
  sort -n |
  awk '{v[NR]=$1} END {print "p70 CPU: " v[int(NR*0.70)] "m"}'

El antes y el después de Rutas Norte

Tras dos semanas de observación (incluido el puente del 1 de mayo), estos son los datos y las decisiones:

Componente requests de 03-04 Uso p50 Uso p95 requests nuevas Veredicto
tienda-web cpu 200m / mem 256Mi 11m / 37Mi 24m / 42Mi cpu 50m / mem 64Mi Sobredimensionado 4x. nginx sirviendo estáticos gasta muy poco
api-reservas cpu 100m / mem 128Mi 185m / 302Mi 640m / 458Mi cpu 200m / mem 512Mi Infradimensionado. Explica el throttling que nadie sabía diagnosticar
postgres-reservas cpu 500m / mem 1Gi 335m / 1710Mi 1250m / 1980Mi cpu 500m / mem 2560Mi Memoria muy corta: riesgo real de OOMKilled en picos
redis-cache cpu 100m / mem 512Mi 27m / 410Mi 55m / 486Mi cpu 50m / mem 768Mi CPU sobrada; memoria justa y creciendo con las plazas cacheadas
worker-notificaciones cpu 200m / mem 256Mi 43m / 180Mi 380m / 240Mi cpu 100m / mem 320Mi Muy variable: ráfagas al enviar lotes de correos
informes-ocupacion (CronJob) cpu 500m / mem 512Mi 890m / 720Mi 1100m / 810Mi cpu 1 / mem 1Gi Se quedaba corto: los informes tardaban el doble por throttling

Tres hallazgos que merece la pena comentar, porque son los típicos:

  1. api-reservas estaba pidiendo 100m y usando 185m de media. Con QoS Burstable, eso funciona mientras el nodo tenga hueco, pero el planificador estaba colocando pods como si cada uno ocupara 100m: sobrevendía el nodo. Y el limits.cpu de 500m provocaba throttling en los picos de 640m, lo que explica los picos de latencia que nadie sabía atribuir. Este es el hallazgo más valioso de todo el ejercicio.
  2. tienda-web reservaba 4 veces lo que usa. Multiplicado por sus réplicas y por tres entornos, eso son varios núcleos y varios GB de clúster reservados para nada, contando además contra la ResourceQuota del namespace (03-04).
  3. El CronJob informes-ocupacion sufría throttling silencioso. Su limits.cpu de 1000m con un uso de 1100m deseados hacía que el informe nocturno tardara 40 minutos en vez de 18. Nadie se quejaba porque nadie estaba despierto.

Los manifiestos recalibrados

# k8s/entornos/pro/api-reservas-recursos.yaml
# Valores basados en dos semanas de observación con kubectl top
# CPU: p70 observado como request; limit generoso para absorber picos
# Memoria: p95 + 12% de margen, con limit igual a request (QoS Guaranteed
# para el componente crítico del negocio)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reservas
  namespace: rutas-norte-pro
spec:
  template:
    spec:
      containers:
        - name: api
          resources:
            requests:
              cpu: "200m"          # antes 100m — p50 observado 185m
              memory: "512Mi"      # antes 128Mi — p95 observado 458Mi
            limits:
              cpu: "1500m"         # antes 500m — permitía llegar a los 640m del p95
              memory: "512Mi"      # igual que request → QoS Guaranteed
# k8s/entornos/pro/tienda-web-recursos.yaml
# Componente sobredimensionado: liberamos clúster reduciendo las requests
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tienda-web
  namespace: rutas-norte-pro
spec:
  template:
    spec:
      containers:
        - name: nginx
          resources:
            requests:
              cpu: "50m"           # antes 200m — p95 observado 24m
              memory: "64Mi"       # antes 256Mi — p95 observado 42Mi
            limits:
              cpu: "300m"
              memory: "128Mi"

Una advertencia sobre el proceso: recalibra en rutas-norte-pre primero, con carga sintética representativa, y solo después lleva los valores a rutas-norte-pro. Bajar una requests.memory de golpe en producción puede provocar que el planificador meta más pods en un nodo del que caben de verdad, con desalojos posteriores.

Y no lo hagas a mano indefinidamente. En 09-02 veremos el VPA (Vertical Pod Autoscaler), que hace exactamente este análisis de forma continua y puede recomendar o aplicar los valores automáticamente.

  1. Por qué el HPA y el VPA dependen de este componente

Aunque el módulo 9 desarrolla el autoescalado, hay que dejar clara aquí la dependencia, porque es la razón de ser de metrics-server.

Cuando en 09-01 escribamos un HorizontalPodAutoscaler como este:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-reservas
  namespace: rutas-norte-pro
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-reservas
  minReplicas: 4
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70      # 70 % de la REQUEST, no del limit

el controlador del HPA, que vive dentro del kube-controller-manager, consulta exactamente la misma API que kubectl top:

GET /apis/metrics.k8s.io/v1beta1/namespaces/rutas-norte-pro/pods

Cada 15 segundos por defecto. Con el resultado, calcula el uso medio de CPU de los pods del Deployment, lo divide entre la suma de sus requests para obtener el porcentaje de utilización, y decide cuántas réplicas debe haber.

Dos consecuencias que hay que grabar a fuego:

Consecuencia 1: sin metrics-server, el HPA no funciona. No falla ruidosamente: se queda con el estado unknown y no escala. El síntoma:

kubectl -n rutas-norte-pro get hpa api-reservas
NAME           REFERENCE                 TARGETS              MINPODS   MAXPODS   REPLICAS   AGE
api-reservas   Deployment/api-reservas   <unknown>/70%        4         20        4          12m

Ese <unknown> es la firma de que metrics-server no está disponible. Y kubectl describe hpa lo confirma:

Conditions:
  Type            Status  Reason                   Message
  ----            ------  ------                   -------
  AbleToScale     True    SucceededGetScale        the HPA controller was able to get the target's current scale
  ScalingActive   False   FailedGetResourceMetric  failed to get cpu utilization: unable to get metrics for
                                                   resource cpu: unable to fetch metrics from resource metrics
                                                   API: the server could not find the requested resource

Durante el puente de mayo, con el HPA "instalado" pero sin metrics-server, api-reservas se habría quedado en 4 réplicas mientras el tráfico se multiplicaba por seis.

Consecuencia 2: el porcentaje del HPA se calcula sobre las requests. Un averageUtilization: 70 con requests.cpu: 100m significa escalar al llegar a 70 milicores. Si recalibras la request a 200m, el mismo HPA escalará al llegar a 140 milicores. Cambiar las requests cambia el comportamiento del autoescalado. Por eso el orden correcto es: primero calibrar con kubectl top (esta lección), después configurar el HPA (09-01).

El VPA (09-02) usa la misma fuente, pero con un histórico propio que él mismo mantiene, precisamente porque metrics-server no lo tiene.

  1. Diagnóstico de los fallos típicos

error: Metrics API not available

kubectl top nodes
error: Metrics API not available

Significa que el API Server no puede servir el grupo metrics.k8s.io. Secuencia de diagnóstico:

# 1. ¿Existe y está disponible el APIService?
kubectl get apiservice v1beta1.metrics.k8s.io
NAME                     SERVICE                      AVAILABLE                  AGE
v1beta1.metrics.k8s.io   kube-system/metrics-server   False (MissingEndpoints)   3m
# 2. ¿Está el pod corriendo y listo?
kubectl -n kube-system get pods -l k8s-app=metrics-server
NAME                              READY   STATUS    RESTARTS   AGE
metrics-server-7bf7d58749-qk4hn   0/1     Running   4          3m

0/1 con reinicios: el pod arranca pero su propia readiness falla. Al log:

kubectl -n kube-system logs -l k8s-app=metrics-server --tail=30

Error de certificado del kubelet

E0806 09:22:14.882134  1 scraper.go:149] "Failed to scrape node" err="Get
\"https://192.168.49.2:10250/metrics/resource\": tls: failed to verify certificate:
x509: cannot validate certificate for 192.168.49.2 because it doesn't contain any IP SANs"
node="rutas-norte-worker-1"

Este es el caso descrito en el apartado 4. Solución en desarrollo:

kubectl -n kube-system patch deployment metrics-server --type=json \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'

Recuerda la advertencia: en rutas-norte-pro, la solución correcta es la rotación de certificados de servidor del kubelet, no este parche.

Valores vacíos recién arrancado

kubectl top pods -n rutas-norte-pro
W0806 09:15:02.114 Metrics not available for pod rutas-norte-pro/api-reservas-7d9f8c4b5-x2klm, age: 22.4s
error: metrics not available yet

No es un fallo. Es el comportamiento esperado durante los primeros 30-60 segundos tras arrancar metrics-server o tras crear un pod nuevo: hacen falta dos muestras para calcular la tasa de CPU. Espera un minuto y vuelve a probar. Muchísima gente reinstala metrics-server innecesariamente por no esperar.

El nodo no es alcanzable

E0806 09:24:01 "Failed to scrape node" err="Get \"https://rutas-norte-worker-3:10250/metrics/resource\":
dial tcp: lookup rutas-norte-worker-3 on 10.96.0.10:53: no such host" node="rutas-norte-worker-3"

metrics-server intenta llegar al nodo por su nombre de host y el DNS no lo resuelve. Solución: forzar el uso de la IP interna.

args:
  - --kubelet-preferred-address-types=InternalIP,Hostname,InternalDNS,ExternalDNS

Un pod concreto no aparece

Causas habituales, en orden de frecuencia:

  1. El pod lleva menos de 30 segundos vivo.
  2. El pod está en Pending: no tiene contenedores ejecutándose, así que no hay nada que medir.
  3. El nodo donde vive el pod no es alcanzable para metrics-server (mira los logs).
  4. No tienes permisos RBAC sobre ese namespace.

Tabla resumen de diagnóstico

Síntoma Causa probable Comprobación
Metrics API not available Pod caído o APIService no disponible kubectl get apiservice v1beta1.metrics.k8s.io
metrics not available yet Menos de 60 s desde el arranque Esperar y reintentar
x509: cannot validate certificate Certificado autofirmado del kubelet Logs de metrics-server; flag --kubelet-insecure-tls
no such host al escanear nodo Resolución DNS del nombre del nodo --kubelet-preferred-address-types=InternalIP,...
HPA con <unknown> metrics-server no responde kubectl top pods en el mismo namespace
Falta un pod concreto Pod nuevo o en Pending kubectl get pod <nombre> -o wide
Error from server (Forbidden) RBAC insuficiente kubectl auth can-i get pods.metrics.k8s.io -n <ns>

Errores Comunes y Consejos

1. Esperar que metrics-server sea un sistema de monitorización. Es la confusión número uno. No guarda histórico, no alerta y solo mide dos cosas. Instalar metrics-server y dar la observabilidad por resuelta es dejar la plataforma casi tan ciega como antes.

2. Confundir kubectl top con kubectl describe node. Son datos completamente distintos y responden a preguntas distintas:

kubectl describe node rutas-norte-worker-2 | grep -A 8 "Allocated resources"
Allocated resources:
  Resource           Requests      Limits
  --------           --------      ------
  cpu                3200m (80%)   6500m (162%)
  memory             6144Mi (78%)  9216Mi (118%)

Esto son requests y limits declarados, es decir, lo que el planificador tiene reservado. kubectl top muestra el consumo real. Un nodo puede estar al 80 % de reservas y al 20 % de uso real: eso es precisamente lo que estamos corrigiendo en el apartado 8.

3. Recalibrar con una sola lectura. Un kubectl top un martes a las 11:00 no te dice nada sobre el puente de mayo. Mide durante al menos una semana y usa percentiles.

4. Bajar --metric-resolution por debajo de 10 s. Se satura la Summary API de los kubelets, que en clústeres grandes es una operación cara. No mejora la calidad de los datos y sí puede degradar el nodo.

5. Dejar --kubelet-insecure-tls en producción. Lo repito porque se hace constantemente: se pone para desatascar la instalación y nadie vuelve a quitarlo. En rutas-norte-pro es un riesgo real, no teórico.

6. Ejecutar más de una réplica de metrics-server sin ajustar la configuración. Con alta disponibilidad hace falta añadir --enable-aggregator-routing=true en el API Server y ajustar la anti-afinidad. Con una réplica, la única consecuencia de que se caiga es perder kubectl top unos segundos: aceptable en la mayoría de casos.

7. Interpretar MEMORY(bytes) como el RSS del proceso. Es workingSetBytes, que excluye caché de página recuperable. No cuadrará con ps ni con free dentro del contenedor, y eso es normal.

8. Usar kubectl top para investigar un incidente pasado. No hay pasado. Si el incidente terminó, kubectl top te enseña el presente sano y no aporta nada. Esa es literalmente la razón de la próxima lección.

9. Olvidar que las requests afectan al HPA. Cambiar requests.cpu sin revisar el averageUtilization del HPA cambia silenciosamente el umbral de escalado.

10. No comprobar los sidecars con --containers. Desde 06-04 tenemos sidecars en varios componentes. Sumarlos al contenedor principal esconde su coste y complica el dimensionado.

Ejercicios

Ejercicio 1 — Instalar, verificar y recorrer el camino del dato

En tu minikube rutas-norte:

  1. Activa el addon metrics-server y verifica que el APIService queda AVAILABLE: True.
  2. Obtén las métricas de api-reservas sin usar kubectl top, hablando directamente con la API agregada.
  3. Explica de dónde salió exactamente ese número: qué componente lo leyó, de dónde y por qué ruta llegó a tu terminal.

Ejercicio 2 — Detectar el desequilibrio y proponer una recalibración

Estos son los datos de dos semanas de worker-notificaciones en rutas-norte-pro:

Percentil 50 CPU:    43m       Percentil 50 memoria:    180Mi
Percentil 70 CPU:    95m       Percentil 95 memoria:    240Mi
Percentil 95 CPU:   380m       Percentil 99 memoria:    268Mi
Percentil 99 CPU:   520m       Máximo memoria:          291Mi

Su configuración actual:

resources:
  requests:
    cpu: "200m"
    memory: "256Mi"
  limits:
    cpu: "250m"
    memory: "256Mi"
  1. ¿Qué clase de QoS tiene ahora este pod? (repasa 03-05)
  2. Identifica dos problemas graves en esta configuración a la vista de los datos.
  3. Propón una configuración nueva y justifica cada valor.

Ejercicio 3 — Diagnosticar un HPA que no escala

Durante un pico de tráfico, api-reservas sigue en 4 réplicas pese a tener un HPA configurado. Un compañero te pasa esta información:

$ kubectl -n rutas-norte-pro get hpa
NAME           REFERENCE                 TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
api-reservas   Deployment/api-reservas   <unknown>/70%   4         20        4          2d

$ kubectl top pods -n rutas-norte-pro
error: Metrics API not available

$ kubectl -n kube-system get pods -l k8s-app=metrics-server
NAME                              READY   STATUS    RESTARTS      AGE
metrics-server-7bf7d58749-qk4hn   0/1     Running   0             18m
  1. ¿Cuál es la relación de causalidad entre los tres síntomas?
  2. Escribe la secuencia de comandos que ejecutarías para llegar a la causa raíz.
  3. Suponiendo que el log muestre x509: cannot validate certificate ... because it doesn't contain any IP SANs, ¿qué harías en rutas-norte-dev y qué harías en rutas-norte-pro?

Soluciones

Solución 1

1. Activación y verificación.

minikube addons enable metrics-server -p rutas-norte

# Esperar a que el pod esté listo
kubectl -n kube-system wait --for=condition=Ready pod \
  -l k8s-app=metrics-server --timeout=120s

# Verificar el APIService
kubectl get apiservice v1beta1.metrics.k8s.io
NAME                     SERVICE                      AVAILABLE   AGE
v1beta1.metrics.k8s.io   kube-system/metrics-server   True        58s

2. Consulta directa a la API agregada.

kubectl get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/rutas-norte-pro/pods" \
  | jq '.items[] | select(.metadata.name | startswith("api-reservas")) |
        {pod: .metadata.name,
         cpu: .containers[0].usage.cpu,
         memoria: .containers[0].usage.memory}'
{
  "pod": "api-reservas-7d9f8c4b5-x2klm",
  "cpu": "187m",
  "memoria": "312476Ki"
}

También se puede consultar un pod concreto:

kubectl get --raw \
  "/apis/metrics.k8s.io/v1beta1/namespaces/rutas-norte-pro/pods/api-reservas-7d9f8c4b5-x2klm" | jq

3. El camino del dato, paso a paso.

  1. El kernel de Linux contabiliza el uso del cgroup del contenedor api en cpuacct.usage y memory.current.
  2. cAdvisor, integrado en el binario del kubelet del nodo donde vive ese pod, lee esos ficheros de cgroup.
  3. El kubelet expone el agregado en https://<ip-nodo>:10250/stats/summary.
  4. metrics-server consulta ese endpoint cada 15 segundos, guarda las dos últimas muestras en memoria y calcula la tasa de CPU como (nanocores_t2 - nanocores_t1) / (t2 - t1).
  5. metrics-server sirve el resultado en el grupo de API metrics.k8s.io/v1beta1, expuesto a través de un Service en kube-system.
  6. El APIService v1beta1.metrics.k8s.io hace que el API Server delegue en ese Service cualquier petición a ese grupo.
  7. kubectl pide /apis/metrics.k8s.io/v1beta1/... al API Server, que actúa de proxy, y devuelve el JSON.

El detalle esencial: para kubectl esto es indistinguible de un kubectl get pods. Mismo endpoint, misma autenticación, mismo RBAC.

Solución 2

1. Clase de QoS.

Para que un pod sea Guaranteed, requests y limits deben coincidir en todos los recursos de todos sus contenedores. Aquí la memoria sí coincide (256Mi = 256Mi), pero la CPU no (200m ≠ 250m). Por tanto la clase de QoS es Burstable: en caso de presión de memoria en el nodo, este pod es candidato al desalojo antes que cualquier pod Guaranteed.

2. Los dos problemas graves.

Problema A: throttling severo de CPU. El limits.cpu es 250m, pero el percentil 95 del uso deseado es 380m y el percentil 99 llega a 520m. Eso significa que en el 5 % del tiempo el worker está estrangulado, y en ese 5 % es precisamente cuando envía las ráfagas de correos de confirmación. Efecto real para el negocio: los correos de confirmación de compra se retrasan justo cuando más compras hay. Además, kubectl top mostraría el consumo pegado a 250m sin decir que hay estrangulamiento: haría falta Prometheus para confirmarlo.

Problema B: riesgo de OOMKilled y request de CPU inflada. El limits.memory es 256Mi y el máximo observado es 291Mi. Ya se ha superado el límite: ese worker está siendo matado por OOM periódicamente, probablemente sin que nadie lo relacione con los correos que no llegan. Y en el otro sentido, la requests.cpu: 200m está muy por encima del p50 de 43m: se reservan 200m de los que se usan 43m de media, desperdiciando clúster.

3. Configuración propuesta.

resources:
  requests:
    # CPU: p70 observado = 95m. La CPU es comprimible: reservar el central
    # y dejar que el limit absorba las ráfagas de envío de correos.
    cpu: "100m"
    # Memoria: p99 = 268Mi, máximo 291Mi. Reservamos por encima del máximo
    # observado porque la memoria NO es comprimible.
    memory: "320Mi"
  limits:
    # 6x la request: cubre holgadamente el p99 de 520m y da margen a las
    # ráfagas. Al ser comprimible, no hay riesgo de que "gaste de más".
    cpu: "600m"
    # Un 20% sobre la request: margen sobre el máximo observado (291Mi)
    # sin permitir una fuga descontrolada.
    memory: "384Mi"

Justificación resumida:

  • requests.cpu baja de 200m a 100m → se liberan 100m por réplica en la ResourceQuota del namespace.
  • limits.cpu sube de 250m a 600m → desaparece el throttling en las ráfagas.
  • requests.memory sube de 256Mi a 320Mi → por encima del máximo observado de 291Mi: se acaban los OOMKilled.
  • limits.memory a 384Mi → margen del 20 % que absorbe un pico inesperado pero corta una fuga de memoria antes de que afecte al nodo.

Nota de proceso: aplicarlo primero en rutas-norte-pre y confirmar con kubectl top pods --containers durante una semana que el consumo se mantiene en el rango previsto. Confirmar también que el contador de reinicios (RESTARTS) se queda a cero, que es la prueba de que los OOMKilled han cesado.

Solución 3

1. Relación de causalidad.

Los tres síntomas son el mismo problema visto desde tres sitios, y la cadena va de abajo arriba:

metrics-server 0/1 (no listo, no puede escanear los kubelets)
        ↓
El Service de metrics-server no tiene Endpoints (readiness fallando)
        ↓
El APIService v1beta1.metrics.k8s.io pasa a AVAILABLE: False
        ↓
kubectl top → "Metrics API not available"
        ↓
El controlador del HPA no puede leer las métricas de CPU
        ↓
HPA con TARGETS <unknown> → NO ESCALA
        ↓
api-reservas se queda en 4 réplicas durante el pico de tráfico

Detalle importante: el HPA no falla ruidosamente. No genera un evento de error visible en el Deployment ni deja el objeto en estado de error obvio. Simplemente no hace nada. Esa es la razón por la que en 07-04 configuraremos una alerta específica sobre la condición ScalingActive del HPA.

2. Secuencia de diagnóstico.

# 1. Confirmar el estado del APIService (la conexión entre síntomas)
kubectl get apiservice v1beta1.metrics.k8s.io

# 2. Ver por qué el pod no está listo: los eventos y la condición Ready
kubectl -n kube-system describe pod -l k8s-app=metrics-server | tail -25

# 3. La causa raíz suele estar en el log
kubectl -n kube-system logs -l k8s-app=metrics-server --tail=50

# 4. Confirmar qué está diciendo el HPA exactamente
kubectl -n rutas-norte-pro describe hpa api-reservas | grep -A 10 Conditions

# 5. Verificar conectividad al kubelet desde dentro del clúster (opcional)
kubectl -n kube-system exec deploy/metrics-server -- \
  wget -q -O- --no-check-certificate https://192.168.49.2:10250/healthz

3. Actuación diferenciada por entorno.

En rutas-norte-dev — desbloquear rápido, riesgo aceptable:

kubectl -n kube-system patch deployment metrics-server --type=json \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'

kubectl -n kube-system rollout status deployment/metrics-server
sleep 45
kubectl top nodes

En rutas-norte-pro — el flag no es aceptable. La solución correcta es que el certificado del kubelet lo firme la CA del clúster:

  1. Habilitar en cada kubelet la rotación de certificados de servidor:
# /var/lib/kubelet/config.yaml en cada nodo
serverTLSBootstrap: true
rotateCertificates: true
  1. Reiniciar el kubelet en cada nodo (systemctl restart kubelet), lo que genera una CSR por nodo.

  2. Aprobar las CSR pendientes de tipo kubernetes.io/kubelet-serving:

kubectl get csr
kubectl certificate approve <nombre-de-la-csr>

En un clúster real esto se automatiza con un aprobador (por ejemplo, kubelet-csr-approver), porque las CSR se renuevan periódicamente.

  1. Verificar que metrics-server funciona sin el flag inseguro:
kubectl -n kube-system get deploy metrics-server \
  -o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n'
# No debe aparecer --kubelet-insecure-tls

kubectl top nodes

Medida preventiva imprescindible: que este fallo pasara 18 minutos desapercibido durante un pico de tráfico es el verdadero problema. En 07-04 crearemos una alerta que dispare cuando el APIService de métricas no esté disponible o cuando un HPA lleve más de cinco minutos con ScalingActive=False. Nadie debería enterarse de esto porque el tráfico se cae.

Conclusión

En esta lección hemos instalado la primera fuente de datos de consumo real de Rutas Norte y hemos aprendido a leerla con criterio:

  • metrics-server no es un sistema de monitorización, sino un componente de infraestructura cuyo propósito es alimentar al HPA y al VPA. Que kubectl top funcione es una consecuencia agradable.
  • El dato viaja desde los cgroups del kernelcAdvisor dentro del kubelet → la Summary API en el puerto 10250 → metrics-server cada 15 segundos → el APIService de la capa de agregación → tu terminal. Entender ese camino convierte cada mensaje de error en un diagnóstico.
  • kubectl top nodes da porcentajes sobre el allocatable del nodo; kubectl top pods no da porcentajes y el desglose por contenedor con --containers es imprescindible desde que tenemos sidecars.
  • Su uso más valioso hoy ha sido cerrar la deuda de 03-04: hemos recalibrado las requests y los limits de los seis componentes con datos reales, descubriendo que api-reservas estaba infradimensionada y sufriendo throttling, que tienda-web reservaba cuatro veces lo que usa, y que el CronJob nocturno tardaba el doble de lo necesario por un límite demasiado bajo.
  • Y hemos visto sus límites infranqueables: sin histórico, sin consultas, sin alertas, solo CPU y memoria, con una resolución de decenas de segundos. Para "¿qué pasó anoche a las tres?" no sirve.

Esa última limitación es exactamente el gancho de la próxima lección. Rutas Norte necesita un sistema que recuerde, que permita preguntar y que sepa avisar. En 07-03 desplegaremos Prometheus con el operador que anunciamos en 06-07, conectaremos por fin el sidecar exportador de métricas de postgres-reservas que añadimos en 06-04, instrumentaremos api-reservas con métricas de negocio, y aprenderemos PromQL para responder a preguntas que hoy no podemos ni formular: cuántas reservas por minuto se confirman, cuál es el percentil 95 de latencia y cuánto falta para que se llene el disco de la base de datos.

Curso de Kubernetes

Módulo 1: Introducción a Kubernetes

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados