Kubernetes · Networking · 2026-09-03 · 6 min de lectura
HA de egress en Cilium OSS sin pagar Enterprise
Un cluster Kubernetes on-premise (kubeadm sobre VMware) necesitaba una IP de egreso fija: los terceros whitelistean una sola dirección, y todo el tráfico saliente hacia ellos tiene que originarse desde ahí. El egress gateway de Cilium hace exactamente eso — dirige el tráfico que matchea a través de un nodo gateway, con SNAT a la IP designada.
El problema es la palabra un. En el Cilium que corre este cluster — 1.17 OSS — el egress gateway no hace failover. Lo verificamos en pruebas: con varios nodos candidatos etiquetados, el datapath elige uno de forma determinística, y cuando ese nodo muere, el tráfico que matchea se descarta hasta que alguien interviene. El HA del gateway con leader election viene en el egress gateway enterprise de Isovalent; la versión open-source deja la silla vacía. Así que tu IP de egreso cuidadosamente whitelisteada es ahora un single point of failure con un pager atado.
El diseño: flotar la IP, seguir al líder
Dos piezas móviles, cada una haciendo lo que sabe hacer:
kube-vip flota la IP de egreso. Corre en los workers candidatos, elige un líder a través de un lease de Kubernetes, y el ganador anuncia la VIP con ARP gratuito. Muere el nodo → expira el lease → otro nodo toma la IP y hace GARP a la red. Es plomería probada en batalla; el mismo mecanismo que muchos clusters ya usan para la VIP del control plane.
Un pequeño controller de label-sync sigue al lease. A Cilium no le importa dónde vive la VIP — rutea el tráfico de egress al nodo que tenga el label de gateway. Así que la pieza faltante es vergonzosamente específica: mantener exactamente un nodo etiquetado, y que sea el que hoy tiene el lease de kube-vip. El controller observa el lease y parchea los labels de los nodos en consecuencia. Esa es toda la descripción del puesto.
kube-vip solo no lo resuelve (el label seguiría apuntando al nodo muerto) y re-etiquetar solo tampoco (la IP no se movería). Juntos le dan a Cilium OSS lo que Enterprise vende: egress que sobrevive a la caída de un nodo.
Del PoC a algo por lo que se puede paginar gente
La prueba de concepto era un script bash corriendo en una sesión de tmux — validó el approach y habría muerto con mi conexión SSH. La versión productiva es un operator en Go sobre controller-runtime:
- Dos réplicas con leader election y anti-affinity de pods, así las réplicas viven en nodos distintos y una caída de nodo nunca puede voltear las dos.
- RBAC mínimo: leases get/list/watch, nodes get/list/watch/patch. Nada más.
- Contenedor endurecido: non-root, filesystem raíz read-only, sin capabilities.
La parte interesante son las invariantes, porque mover labels tiene filos:
- Agregar antes de quitar. El nuevo holder recibe el label antes de que el viejo lo pierda. Unos milisegundos con dos nodos etiquetados son inofensivos; una ventana con cero nodos etiquetados es un corte de egress. El orden de las operaciones es la diferencia.
- Sin líder → no tocar nada. Si el lease queda momentáneamente sin dueño en medio de una transición, el controller se congela en lugar de borrar el label existente. Viejo-pero-funcionando le gana a limpio-pero-roto.
- Level-triggered, no edge-triggered. Cada evento dispara un recálculo completo del estado deseado, más un resync cada ≤60 s aunque no pase nada. El drift manual de labels se cura solo.
El susto del cold-start
Una observación del PoC casi mata el diseño entero: el primer failover hacia un nodo que nunca había tenido la IP de egreso tardó más de 30 minutos en converger — el agente de Cilium tardaba en reconocer la egress IP como local. Si eso se reproducía, un failover real cayendo en un nodo "virgen" significaba media hora de corte de egress, y el diseño no servía para producción.
Eso se convirtió en el spike que gateaba todo, por delante del trabajo de empaquetado: reproducirlo con un nodo genuinamente limpio, o enterrarlo. Resultado: no se reprodujo — un nodo virgen convergió en ~5 segundos, consistentemente. Lo que fuera que produjo la convergencia de 30 minutos era un artefacto del entorno del PoC, no una propiedad del diseño.
Dos lecciones en una: perseguí tu peor observación antes de construir nada encima del diseño que cuestiona — y acordate de que el entorno de un proof-of-concept es en sí mismo un sospechoso de primera. Podríamos haber shippeado maquinaria de pre-warming (rotar la VIP por todos los candidatos en el setup) para mitigar un problema que no existía.
Modos de falla, dichos en voz alta
Escribir esto explícitamente valió más que cualquier línea de código:
| Qué muere | Qué pasa |
|---|---|
| Una réplica del controller | La otra toma el relevo en ≤15 s; el egress ni se entera (el estado ya está aplicado en los nodos) |
| Las dos réplicas | El label queda congelado donde está; el egress sigue funcionando. Solo un failover de kube-vip durante esa ventana quedaría sin seguir |
| kube-vip se queda sin líderes | La VIP no vive en ningún lado y el egress se degrada al estado pre-HA. El controller no puede suplirlo — pero salta la alerta |
Cuatro alertas de Prometheus cubren el sistema: holder del lease ≠ nodo etiquetado, cero-o-dos nodos etiquetados, controller caído, lease sin dueño. Cada una mapea a una fila de esa tabla.
Resultados
Failover medido end-to-end — desde matar el nodo gateway hasta que el tráfico de egress vuelve a salir con la IP whitelisteada como source, verificado con captura de paquetes en el nuevo gateway:
nodo C → nodo B: 5 s
nodo B → nodo C: 4 s
Para tráfico HTTP de vida corta, las conexiones en vuelo simplemente reintentan y nadie lo nota. El lado VMware necesitó su propia validación (el port group tiene que aceptar MAC changes / forged transmits para que la migración de IP por GARP funcione), que una VIP de control plane con keepalived ya existente convenientemente había probado. Y los dos planos quedan aislados: la peor falla posible de kube-vip devuelve el egress a su viejo comportamiento de IP estática — la VIP del apiserver, propiedad de keepalived en los nodos de control plane, nunca está en el blast radius.
Lecciones
- 1. Cuando el vendor pone el HA detrás de Enterprise, medí qué tan chica es la pieza faltante. Acá era un sincronizador de labels que observa un lease — un fin de semana de controller-runtime, no una re-arquitectura.
- 2. Invariantes primero, código después. Agregar-antes-de-quitar y sin-líder-no-tocar son lo que hace al controller seguro de correr sin supervisión; se diseñaron antes de la reescritura en Go, en el pizarrón.
- 3. Gateá el diseño con su observación más aterradora. Un cold-start de >30 minutos era descalificante. Se ganó un spike dedicado — y resultó ser ruido del PoC.
- 4. Escribí tus modos de falla en una tabla. "Mueren las dos réplicas y el egress sigue funcionando" es una oración que querés tener escrita y alarmada antes de que pase a las 3 AM.