Expediente 4/5: la capa que llegó tarde
El caso de la config que nombró una capa que su imagen no llevaba, del aviso que se imprimió correcto durante días, y del diagnóstico que dedujo porque no podía mirar
🕵️ Mostrar expediente completo → 📋 Protocolo de sesión →
Expediente · Depto. de Homicidios de Código
Un día entero convirtiendo copias en cascada compartida, y al final el sitio caído dos veces por lo mismo: una config que nombra una capa que su artefacto no lleva. La segunda caída, provocada por quien acababa de escribir el guard contra la primera.
Mostrar glosario
- cascada de tres capas
- rustelo (framework) → website-htmx-rustelo (implementación) → site consumidor. Un fichero presente en una capa superior gana; uno ausente se hereda de la de abajo. La config de un site importa `htmx-site/
.ncl`, que es la capa intermedia, y esa capa vive en un directorio que el binario monta al arrancar. - clase rebuild
- La clasificación que dice «estos bytes solo son válidos contra una imagen que los resuelva». Su comportamiento declarado era «viajan inertes y se reportan como debidos», y ahí está el patógeno: inerte solo mientras nada reinicie. El mismo `publish` reinicia.
- leído al arrancar
- Un fichero que el servidor evalúa UNA vez, al iniciar el proceso. Entregarlo no cambia nada visible; el siguiente reinicio lo activa. Es lo que separa un error que se ve al instante de uno que espera días con el detonador puesto.
- entrega por exec
- El contenido viaja al PV mediante `kubectl exec` dentro del contenedor. La consecuencia es cruel y define este caso: cuando el contenedor deja de arrancar, la herramienta pierde la vía por la que deshacer su propio cambio.
- preguntar al artefacto
- La regla que este caso compró. Un gate no consulta una lista, una fecha ni un razonamiento sobre qué debería llevar la imagen: le pregunta a la imagen. El contenido de un artefacto es propiedad del artefacto y cambia debajo de ti.
- no puedo mirar
- El estado que ningún check debe reportar como verde. Con el contenedor caído no había `exec`, y el hueco se rellenó con una deducción cronológicamente impecable y falsa. Un «no pude mirar» es un hallazgo.
Protocolo para declarar, versionar y verificar esto → ontoref.dev
El doble balance — lo que costó, y lo que dejó
Conviene no adornar la parte incómoda. La segunda caída la provocó quien acababa de escribir el guard contra la primera, y el error de diagnóstico que la siguió —afirmar que dos sites corrían imágenes distintas— se cometió exactamente por el vicio que este repositorio lleva un día quitando de todas partes: no podía mirar, así que deduje.
Lo que costó que la capa llegara después
- Caídas de ontoref.dev el mismo día 2 — misma clase, ~19 h de diferencia
- Reinicios en CrashLoopBackOff (segunda) 6 en el primer pod, 3 en su reemplazo
- Capas que la config nombraba / que la imagen llevaba 5 / 4
- Ficheros realmente rotos / revertidos en caliente 1 / 4
- Recuperación escalar a 0 + pod temporal montando el PVC — dos veces, porque
execmuere con el contenedor - Afirmación falsa emitida bajo presión «los dos sites corren imágenes distintas» — un solo digest, 097bf919
- Sondas descartadas al construir el gate 3 (ConfigMap, MANIFEST horneado,
host) - Mecanismo final 105 líneas de preflight + 115 del gate de compatibilidad
Lo que quedó en pie
- Copias de mecanismo eliminadas 3 scripts × 3 sites, y 5 stems de config que 3 sites escribían a mano
- Gates que preguntan al artefacto 2 — uno en el bucle de desarrollo, otro en la entrega
- Sites que nacen con la defensa puesta todos: el gate entra en la plantilla del generador
Los sospechosos — las pistas falsas
| El script de publicación se saltó el clasificador | “Falso: `update-content.sh` llama a content.nu, y el clasificador marcó los cuatro ficheros como rebuild, correctamente. El guard funcionó; lo que fallaba era lo que la etiqueta significaba.” | DESCARTADO |
| Los dos sites corren imágenes distintas | “Falso, y lo afirmé con el contenedor caído y sin poder mirar. Ambos pods corrían el mismo digest. La deducción era cronológicamente impecable: el clúster no va por fechas, va por digests.” | REFUTADO POR EL DIGEST |
| Montar las capas del perfil con un ConfigMap | “Insuficiente: las capas del perfil importan bases del framework que la imagen también precedía. Dos overlays a ciegas sobre una imagen que nadie podía inspeccionar.” | INSUFICIENTE |
| Hornear un MANIFEST del perfil dentro de la imagen | “Innecesario: el directorio `htmx-site/` YA es el manifiesto. Un fichero habría sido una segunda copia de un listado — la forma exacta que llevaba un día borrando.” | REDUNDANTE |
| `host $cp_node` para saber si el destino responde | “Equivocada: `cp_node` es un alias de ~/.ssh/config, no un nombre DNS. Daba por irresoluble el control-plane real y saltaba el check justo en el único site verificable.” | PROBADA CON UNA SOLA ENTRADA |
El arma — Dos líneas del mismo `publish`. La primera declara la deuda; la segunda la detona.
[publish] 4 file(s) still owe a rebuild — bytes ship inert,
the rest of this changeset does not wait on them
[publish] 21 files → class=restart → rolling restart
El aviso llevaba días imprimiéndose correcto. Una deuda que llora en cada ejecución es una deuda que se aprende a saltar, y así se leyó la noche que importaba.
El giro — Tres preguntas, ninguna a una lista.
# el gate le pregunta al pod, nunca a una lista kubectl exec deploy/$D -- printenv NICKEL_IMPORT_PATH # su orden, no el mío kubectl exec deploy/$D -- nickel export …/index.ncl # su nickel, su imagen kubectl exec deploy/$D -- ls …/nickel/htmx-site/ # el directorio ES el manifiesto
Falsificado en las tres ramas: verde con las cinco capas, rojo nombrando la que falta, y SKIP dicho en voz alta cuando el destino aún no está configurado.
El veredicto
El instrumento que habría respondido —config-check— validaba contra las raíces instaladas
en el portátil, que van siempre por delante de cualquier imagen. Por eso un site podía estar
verde en local y ser inarrancable en un pod, y por eso el fallo no se sintió como un
descuido: cada pieza contestaba, y ninguna contestaba eso.
Lo que queda escrito no es una advertencia. Es que la pregunta ahora la hace una máquina, dos veces —en el bucle de desarrollo y en la entrega—, y las dos se la hacen al artefacto.
| Una config nombra una capa que la imagen no lleva | profile-compat-check: los imports contra el ls del pod, en el bucle de desarrollo. |
| Bytes irresolubles llegan al PV | preflight: exporta index.ncl dentro del pod. Rojo no entrega nada. |
| «rebuild» leído como puerta cuando era etiqueta | El aviso se redacta desde la prueba, no desde la clasificación. |
| La lista de ficheros dependientes de la imagen | Regla derivada del import, jamás una lista: la lista nace una entrada corta. |
| No poder mirar el artefacto | SKIP dicho en voz alta · ROJO si no responde · veredicto si responde. Ningún verde por ignorancia. |
La jurisprudencia — qué exige hoy la lección
- ✓Una config que importa una capa compartida se verifica contra la imagen del destino, no contra las raíces del portátil
gate:just profile-compat-check - ✓La config del site debe exportar contra las tres raíces que monta el servidor, no contra las del desarrollador
gate:just config-check - ✓Las dos anteriores corren en cada
check, incluido el primero de un site recién generadogate:just check - ✓El preflight de entrega rehúsa publicar bytes que el pod no puede cargar, y ahora alguien puede apuntarlo
test:provisioning/tests/test_preflight_refuses.nu
Un «no pude mirar» es un hallazgo. Lo escribimos en un gate esa misma tarde, y no me lo apliqué.
Del vocabulario del proyecto (2)
- Gate
- Prerrequisitos tipados y políticas que controlan las transiciones de estado de la máquina de estados de un proyecto.
- ontoref
- El protocolo en sí: una superficie tipada y consultable en la que un proyecto declara LO QUE ES (ontología) y CÓMO ACTÚA (reflexión), de modo que una afirmación sobre el proyecto pueda ser contradicha por una máquina y no sólo por un lector.