commit&push

~/blog $ git show 2026-09-15

The Fix Took Production Down for Three Days

· 5 min read · Victor Benavides

--incidents--architecture

En agosto escribí aquí sobre catorce servicios que levantaron sin poder hablar con nada, reportando salud perfecta. La causa era un webhook de inyección configurado con failurePolicy: Ignore, que le decía al clúster que si no se podía alcanzar al inyector, los pods arrancaran igual. Arrancaron. Estaban sordos.

Lo arreglé poniendo la política en Fail.

Ese arreglo es lo que tumbó producción durante tres días y trece horas la semana pasada.

Por qué el arreglo estaba bien

Fail-open es una promesa rara cuando la piensas. Dice que cuando el componente responsable de hacer funcional a un pod no está disponible, la respuesta correcta es crear el pod de todas formas y dejar que esté roto de una manera que nada va a reportar.

Fail-closed dice lo contrario. Si el inyector no puede responder por un pod, el pod no se crea. Cambias disponibilidad por correctitud, y para algo como la inyección de sidecars ese cambio es el correcto. Un servicio que no existe es un problema que vas a notar. Un servicio que existe y no puede hablar es un problema que vas a notar a las tres horas de un incidente.

Sigo pensando que cambiarlo estuvo bien. Lo que no había pensado a fondo era la obligación que venía con ese cambio.

Lo que fail-closed cuesta de verdad

La semana pasada la actualización automática del proveedor de nube reemplazó todos los nodos del clúster. Eso es rutina, y se supone que es sobrevivible.

El inyector corría como una sola réplica, sin presupuesto de interrupción, en un pool que estaba rotando. Se cayó con su nodo, que es exactamente lo que cualquiera esperaría. Lo que pasó después es la parte que vale la pena mirar con calma.

reemplazo del inyectorno puede arrancarwebhook de admisiónfailurePolicy: Failintercepta la creación de todo poddebe alcanzar al inyectorEl inyector es el pod que se está creando. No arranca nada, en ningún lado.La única excepción es un namespace etiquetado control-plane=true. El del propio inyector nunca lo estuvo.3 días, 13 horas
Fail-closed significa que el webhook rechaza lo que no puede validar, lo cual es correcto hasta el momento exacto en que eso que no puede validar es su propio reemplazo. El ciclo no se puede romper desde dentro del clúster.

El webhook del propio inyector intercepta la creación de pods en todo el clúster. Así que cuando el planificador intentó crear el inyector de reemplazo, se consultó al webhook, el webhook intentó alcanzar al inyector, el inyector no existía, y fail-closed hizo precisamente lo que yo le había pedido. Se negó.

No se pudo crear nada en ningún lugar de ese clúster. Ni el inyector, ni ninguna otra cosa. Cincuenta y dos de cincuenta y siete servicios siguieron caídos hasta que etiqueté un namespace a mano cuatro días después.

Existe una salida de emergencia, y es la única razón por la que esto es sobrevivible. El webhook se salta los namespaces marcados como plano de control. El inyector había sido instalado con un comando que crea su namespace sin etiquetarlo, así que la única excepción que le habría permitido curarse solo nunca se había aplicado.

Nada estaba en Pending

Este es el detalle que más me gustaría que otra gente se lleve, porque está calladamente presente en muchísimas herramientas.

Mi instinto durante una caída es buscar pods que no estén en Running. No había ninguno. Toda verificación de esa forma volvía limpia mientras la plataforma entera estaba caída.

Los pods se rechazaban en admisión, lo que significa que nunca se creaban. Un pod que no existe no puede estar en Pending, no puede estar en CrashLoopBackOff, no puede aparecer en ninguna lista de cosas enfermas. La ausencia se veía igual que la salud.

Lo que sí lo mostraba era contar los deployments con cero réplicas disponibles, y leer los eventos de fallo de creación. Esas son preguntas distintas de "¿hay algo roto?", y yo no las tenía conectadas a nada.

La cuenta que no pagué

Acá está la tesis, y me costó una caída verla.

Fail-open y fail-closed no son correcto e incorrecto. Son una elección sobre qué falla prefieres tener, y cada una viene con una obligación que asumes en el momento de elegirla.

Elige fail-open y te debes detección a ti mismo, porque acabas de aceptar que cosas rotas van a arrancar y se van a ver bien. Esa fue la lección de agosto.

Elige fail-closed y has creado un componente capaz de detenerlo todo, así que le debes a ese componente disponibilidad de verdad. Más de una réplica. Un presupuesto de interrupción. Una excepción para que siempre pueda recrearse. Un lugar donde correr que no sea la cosa misma que está custodiando.

Cambié la política en agosto y no pagué nada de eso. La configuración era correcta y el trabajo alrededor faltaba, lo que se sintió como un arreglo y en realidad fue una transferencia de riesgo de un modo de falla a otro.

La nota honesta al pie

No se rompió nada para nadie afuera, porque esa plataforma todavía no tiene clientes en producción. Tres días y medio de caída total me costaron un fin de semana y una cantidad considerable de dignidad.

Prefiero aprender esto ahora. La versión de este post donde hay locales escaneando credenciales en una puerta mientras yo averiguo qué namespace necesita una etiqueta es una que preferiría muchísimo no escribir.

$ grep -rl --tag ~/blog