Volver al blog

KubeBolt en OpenShift 4.20: cuando un timeout de 25 segundos escondía 138 MiB de Events

KubeBolt alcanzaba Kubernetes, autenticaba y consultaba recursos, pero declaraba el clúster inaccesible. Una prueba A/B aisló 138 MiB de Events como el outlier.

Ya tenemos KubeBolt 1.23.1 funcionando como agente in-cluster sobre OpenShift 4.20, con Kubernetes 1.33.6. Llegar hasta el estado Active, sin embargo, exigió algo más que instalar el chart Helm oficial.

Durante la integración aparecieron dos problemas independientes. El primero estaba en el arranque de kubebolt-web bajo las restricciones de seguridad de OpenShift. El segundo era menos evidente: KubeBolt alcanzaba el API Server, autenticaba y consultaba recursos con normalidad, pero unos 25 segundos después declaraba el clúster inaccesible.

La pista decisiva apareció al medir cada recurso por separado: el endpoint de Events respondía con HTTP 200, pero entregaba 144.641.645 bytes, aproximadamente 138 MiB.

El entorno que estábamos probando

La instalación combinaba:

  • KubeBolt 1.23.1 mediante su chart Helm oficial;
  • OpenShift 4.20 y Kubernetes 1.33.6;
  • ejecución de KubeBolt dentro del propio clúster;
  • 5 nodos, 77 namespaces, 373 pods observados y 67 deployments;
  • restricted-v2 como SCC original.

El objetivo era que KubeBolt descubriese y analizase el entorno mediante su propio ServiceAccount. Separar los dos fallos desde el principio fue importante: que ambos apareciesen durante el mismo despliegue no significaba que compartiesen causa.

Primer problema: kubebolt-web frente a restricted-v2

El entrypoint del contenedor web intentaba modificar este archivo durante el arranque:

/etc/nginx/conf.d/default.conf

Bajo restricted-v2, la operación terminaba con un error equivalente a:

sed: can't create temp file ... Permission denied

La prueba se limitó a kubebolt-web: creamos un ServiceAccount dedicado y lo autorizamos para utilizar anyuid. El componente arrancó, mientras kubebolt-api y VictoriaMetrics continuaron ejecutándose bajo restricted-v2.

El resultado no convierte anyuid en una recomendación universal. Fue una excepción controlada para un componente concreto cuyo entrypoint necesitaba un comportamiento incompatible con el contexto original. Red Hat documenta que anyuid permite ejecutar con cualquier UID y GID, mientras restricted-v2 elimina capabilities, aplica runtime/default y bloquea la elevación de privilegios. Esa diferencia justifica conceder la excepción con cautela y con el menor alcance posible en lugar de modificar las SCC predeterminadas.

kubebolt-web      → ServiceAccount dedicado y excepción controlada
kubebolt-api      → restricted-v2
VictoriaMetrics  → restricted-v2

Que una aplicación esté diseñada para Kubernetes no implica que deba recibir permisos amplios para funcionar en OpenShift. En este caso no fue necesario relajar la seguridad de toda la instalación.

Kubernetes respondía, pero el clúster aparecía como unreachable

Una vez aislado el problema del frontend, KubeBolt detectaba correctamente la configuración in-cluster. El backend alcanzaba kubernetes.default.svc, ejecutaba SelfSubjectAccessReview y completaba su comprobación de permisos.

26/31 resource types accessible

La secuencia parecía normal hasta que, aproximadamente 25 segundos después, aparecía:

connect timed out after 25s for context in-cluster — agent may be stuck

La interfaz traducía ese estado como Cluster unreachable. El mensaje hacía razonable revisar DNS, certificados, NetworkPolicy, token del ServiceAccount y RBAC, pero los datos todavía no demostraban cuál de esas hipótesis era correcta.

Validar antes de cambiar RBAC

Hicimos las comprobaciones desde un pod que utilizaba el mismo ServiceAccount kubebolt. kubernetes.default.svc resolvía correctamente y las peticiones autenticadas funcionaban contra:

/version
/api/v1/nodes
/api/v1/pods

Nodes, Pods, Deployments, Services, StatefulSets, DaemonSets y Jobs respondían normalmente y, en estas pruebas, por lo general en menos de un segundo.

La evidencia era suficiente para dejar de tratar la conectividad general, DNS, autenticación o un fallo RBAC global como hipótesis principal. No demostraba que cualquier posible problema de red fuese imposible; demostraba algo más útil para la investigación: el mismo cliente, identidad y ruta al API Server funcionaban con varios recursos reales.

Kubernetes recomienda asignar a cada workload un ServiceAccount específico y otorgarle mediante RBAC solo los permisos necesarios. Ese era también el límite de nuestra prueba: no ampliar el ClusterRole de forma indiscriminada, sino comprobar qué recurso se comportaba de manera diferente (documentación oficial sobre ServiceAccounts).

El outlier estaba en Kubernetes Events

Al consultar los recursos individualmente apareció una diferencia clara:

oc get events -A

La operación tardaba entre 32,4 y 32,5 segundos. Después hicimos una petición HTTP directa a:

/api/v1/events

El resultado observado fue:

HTTP 200
Tiempo aproximado: 6,35 s
Transferencia: 144.641.645 bytes

~138 MiB de Events en una sola respuesta. El endpoint no estaba roto: respondía correctamente, pero devolvía un volumen extraordinariamente mayor que los demás recursos medidos.

La diferencia entre los 6,35 segundos de la petición directa y los más de 32 segundos de oc get events -A también aconsejaba no equiparar “tiempo de respuesta HTTP” con “tiempo total hasta que un cliente termina de recibir, decodificar, representar o sincronizar los objetos”. Eran mediciones de operaciones distintas.

Por qué una respuesta correcta puede retrasar un informer

Un informer de Kubernetes mantiene una vista local de recursos y recibe cambios posteriores. De forma simplificada, su arranque suele implicar:

  1. ejecutar un LIST inicial;
  2. recibir y deserializar los objetos;
  3. almacenarlos en la caché local;
  4. establecer el WATCH;
  5. esperar a que la caché informe de que está sincronizada.

Kubernetes utiliza operaciones list consistentes y streams watch para que los clientes mantengan estado sincronizado. Para colecciones grandes, la API soporta paginación mediante limit y continue, precisamente para evitar respuestas monolíticas que carguen al servidor y al cliente (Kubernetes API concepts).

No inspeccionamos el código interno de KubeBolt ni demostramos cómo implementa exactamente su paginación, sus informers o el límite de inicialización. La interpretación debe mantenerse en el nivel que permiten los datos:

Los resultados apuntan con fuerza a que la carga inicial de Events retrasaba la sincronización del informer o de su caché hasta superar el límite de 25 segundos utilizado durante la inicialización del contexto.

Esta explicación es consistente con la prueba A/B. No equivale a afirmar que exista un bug confirmado en el informer de KubeBolt.

La prueba A/B: retirar una sola variable

Para comprobar la relación modificamos exclusivamente el ClusterRole de KubeBolt. Retiramos events de la regla que concedía:

get
list
watch

No retiramos permisos sobre Pods, Nodes, Deployments ni los demás recursos relevantes. Después reiniciamos el backend.

El permission probe pasó de:

26/31 resource types accessible

a:

25/31 resource types accessible

Ese descenso era el esperado. El cambio decisivo apareció unos 1,8 segundos después:

Informer caches synced

El timeout desapareció y la interfaz cambió de Cluster unreachable a Active.

Con Events
KubeBolt inicia contexto

Permission probe

Inicialización de informers

LIST global de Events (~138 MiB)

Recepción / deserialización / caché / sincronización

Se supera el límite de 25 s

Context timeout
Sin Events durante la prueba
KubeBolt inicia contexto

Permission probe

Informers principales

Caches sincronizadas (~1,8 s)

Cluster Active

Solo cambió una variable. Por eso la comparación aporta más evidencia que ampliar timeouts, permisos o excepciones de red al mismo tiempo.

De unreachable a Active

Tras la prueba, KubeBolt mostró correctamente el inventario y el estado general del entorno:

  • 5/5 Nodes;
  • 67/67 Deployments;
  • 77 Namespaces;
  • Pods, workloads y almacenamiento;
  • métricas y capacidad del clúster.
Dashboard de KubeBolt conectado a OpenShift 4.20 con cinco nodos
Estado del clúster después de completar correctamente la sincronización de los informers principales.

La conclusión observacional es precisa: con Events autorizado aparecía el timeout; al retirar únicamente ese recurso, las cachés principales sincronizaron y el contexto quedó activo. Determinar qué parte exacta del ciclo interno consumía el tiempo requeriría instrumentación o revisión del código de KubeBolt.

Este matiz es el mismo que aplicamos al diseñar alertas accionables en Kubernetes: detectar el síntoma y explicar el mecanismo son fases distintas.

El timeout no siempre está donde dice el error

connect timed out empuja la investigación hacia DNS, firewall, NetworkPolicy, certificados, ServiceAccount o RBAC. Es una primera lectura razonable, pero un timeout también puede representar una operación que empezó correctamente y no terminó dentro de la ventana esperada.

En este entorno coexistían estos hechos:

API Server reachable
Authentication OK
RBAC operational para los recursos probados
Resources accessible

con este estado de interfaz:

Cluster unreachable

Una explicación consistente es que “conectado” no dependiese solo de alcanzar el API Server, sino también de completar una fase posterior de inicialización y sincronización. La distinción importa: una etiqueta global de indisponibilidad puede ocultar que el control plane responde y que el retraso pertenece a un recurso o fase concretos.

Mejoras que merece la pena evaluar

Las siguientes son propuestas derivadas del laboratorio, no afirmaciones sobre la implementación actual ni requisitos para KubeBolt.

Paginar el LIST inicial de Events

Si el listado inicial se realiza actualmente sin paginación, limit y continue permitirían recibir la colección en bloques. Esto no elimina el volumen total, pero reduce el tamaño de cada respuesta y puede hacer más predecible el trabajo de red, memoria y deserialización.

Sincronizar Events después de los recursos esenciales

Nodes, Pods, Deployments y Namespaces pueden ser suficientes para declarar disponible el contexto básico. Events podría sincronizarse después:

Core informers synced

Cluster Active

Events syncing in background

Separar conexión y sincronización

Mensajes distintos ayudarían a localizar la fase:

Kubernetes API reachable
Informer sync pending: Events

es operativamente más preciso que Cluster unreachable cuando el API Server ya ha respondido.

Hacer configurable la ventana de inicialización

El tiempo necesario puede variar según el tamaño del clúster, el número de objetos, la latencia del API Server y el volumen de Events. Un timeout configurable permitiría adaptar esa ventana, aunque por sí solo no sustituye a identificar el recurso lento.

Permitir degradación parcial

Si Events falla o tarda demasiado, podría evaluarse mantener disponibles Nodes, Pods, Deployments y métricas. Una degradación explícita conserva información útil y evita que un informer secundario determine necesariamente el estado global.

Lo aprendido al llevar la integración a OpenShift

OpenShift añade controles concretos sobre UID dinámicos, escritura en el filesystem, capabilities, SCC, ServiceAccounts y RBAC. Ignorarlos conduce a dos extremos poco útiles: asumir que cualquier chart Kubernetes funcionará sin adaptación o conceder permisos amplios hasta que arranque.

La solución aplicada fue más limitada:

kubebolt-web      → excepción controlada
kubebolt-api      → restricted-v2
VictoriaMetrics  → restricted-v2

La misma disciplina se aplicó al segundo problema: no ampliar RBAC ni cambiar la red sin evidencia. En entornos donde el modelo de seguridad y la operabilidad deben convivir, este trabajo de aislamiento forma parte de nuestra consultoría y soporte en Red Hat OpenShift, el soporte especializado de Kubernetes y una práctica de Platform Engineering con límites explícitos.

Resultado y método

KubeBolt 1.23.1 quedó ejecutándose como agente local dentro de OpenShift 4.20. El clúster aparecía activo y la plataforma mostraba nodos, pods, deployments, namespaces, workloads, almacenamiento, métricas y estado general.

El hallazgo útil no fue solo conseguir que el despliegue arrancase. Fue demostrar que detrás de connect timed out había un API Server accesible entregando aproximadamente 138 MiB de Kubernetes Events.

La secuencia de diagnóstico fue deliberadamente sencilla:

aislar → medir → comparar → eliminar una variable → volver a medir

Validamos resolución, autenticación y acceso al API Server; comprobamos RBAC sobre recursos reales; medimos cada colección; identificamos el outlier y ejecutamos una prueba A/B. Eso permitió cambiar de hipótesis sin modificar la configuración de forma indiscriminada.

Desplegar Kubernetes es una parte del trabajo. Entender qué ocurre cuando los casos reales dejan de parecerse a un laboratorio es donde empieza la ingeniería de plataforma.