<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>On-Premise on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/on-premise/</link><description>Recent content in On-Premise on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Sun, 31 May 2026 08:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/on-premise/index.xml" rel="self" type="application/rss+xml"/><item><title>Siete fases de despliegue greenfield de una plataforma LLM on-premise: del hardware en la sala al primer token productivo</title><link>https://blog.lo0.es/posts/siete-fases-despliegue-plataforma-llm-on-premise/</link><pubDate>Sun, 31 May 2026 08:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/siete-fases-despliegue-plataforma-llm-on-premise/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Los dos posts anteriores de esta trilogía arquitectónica fijaron las piezas: &lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">las siete capas del stack de inferencia LLM&lt;/a> describen los componentes encima del cluster, y &lt;a href="https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/">los cinco niveles de madurez de la plataforma&lt;/a> describen los estratos por debajo. Este post fija el &lt;strong>cuándo&lt;/strong>: en qué orden se despliega cada cosa cuando se parte de cero —hardware comprado, racks instalados, cableado físico hecho— y se quiere llegar a un cluster sirviendo el primer token productivo a un cliente. Siete fases nominales &lt;strong>F0 a F6&lt;/strong> sin compromisos de calendario, organizadas por &lt;strong>dependencias técnicas&lt;/strong> (no se entra en F3 sin gate de F2) y con un &lt;strong>camino crítico&lt;/strong> identificable. F0 inventario hardware y conectividad eléctrica/red. F1 OS bare metal + drivers + container runtime. F2 cluster Kubernetes con CNI y storage Ceph operativos. F3 GitOps y observabilidad de infraestructura. F4 identidad, TLS, secretos y políticas. F5 plataforma GPU con observabilidad LLM-aware. F6 stack LLM operativo y abierto a tráfico productivo. Para cada fase: qué se monta, qué tiene que estar listo antes (dependencias entre fases), &lt;strong>gate&lt;/strong> que valida el cierre, y la trampa típica que retrasa el camino crítico. La tesis: una plataforma LLM on-premise se hunde mucho más a menudo por &lt;strong>secuenciar mal&lt;/strong> que por &lt;strong>elegir mal&lt;/strong>. Las herramientas están todas inventadas; el orden es lo único que cada equipo redescubre.&lt;/p>
&lt;h2 id="estás-aquí-las-siete-fases-y-sus-dependencias">Estás aquí: las siete fases y sus dependencias&lt;/h2>
&lt;p>Las fases no se ejecutan en serie pura. F2 y F3 pueden empezarse a la vez para acelerar (instalar Kubernetes y preparar el repo GitOps en paralelo). F4 puede solaparse con la parte final de F3. F5 espera a que F4 cierre porque los pods GPU exigen NetworkPolicy y RBAC desde el primer día. F6 es &lt;strong>un único paso atómico&lt;/strong>: el cluster entra en producción o no.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="dag de fases F0 a F6 con dependencias y camino crítico">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.f0{fill:#f6e2e2;stroke:#a33}.f1{fill:#f4e3cf;stroke:#a63}.f2{fill:#eef0d0;stroke:#7a3}.f3{fill:#dfe9f5;stroke:#356}.f4{fill:#d8eecf;stroke:#373}.f5{fill:#f5e3d8;stroke:#763}.f6{fill:#ead8f5;stroke:#634}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#222}.tiny{font:600 10px sans-serif;fill:#222}.note{font:italic 10px sans-serif;fill:#555}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#a)}.crit{stroke:#c33;stroke-width:2.4;fill:none;marker-end:url(#ac)}&lt;/style>
&lt;defs>&lt;marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;marker id="ac" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#c33"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="22" text-anchor="middle" class="lbl">DAG de fases · camino crítico marcado en rojo&lt;/text>
&lt;rect x="40" y="50" width="110" height="48" class="b f0"/>&lt;text x="95" y="68" text-anchor="middle" class="tiny">F0&lt;/text>&lt;text x="95" y="84" text-anchor="middle" class="sm">Hardware&lt;/text>&lt;text x="95" y="98" text-anchor="middle" class="note">Inventario · red&lt;/text>
&lt;rect x="180" y="50" width="110" height="48" class="b f1"/>&lt;text x="235" y="68" text-anchor="middle" class="tiny">F1&lt;/text>&lt;text x="235" y="84" text-anchor="middle" class="sm">Bare metal&lt;/text>&lt;text x="235" y="98" text-anchor="middle" class="note">OS · drivers&lt;/text>
&lt;rect x="320" y="50" width="110" height="48" class="b f2"/>&lt;text x="375" y="68" text-anchor="middle" class="tiny">F2&lt;/text>&lt;text x="375" y="84" text-anchor="middle" class="sm">Cluster k8s&lt;/text>&lt;text x="375" y="98" text-anchor="middle" class="note">Cilium · Ceph&lt;/text>
&lt;rect x="460" y="50" width="110" height="48" class="b f3"/>&lt;text x="515" y="68" text-anchor="middle" class="tiny">F3&lt;/text>&lt;text x="515" y="84" text-anchor="middle" class="sm">GitOps + obs&lt;/text>&lt;text x="515" y="98" text-anchor="middle" class="note">Flux · VM/Loki&lt;/text>
&lt;rect x="600" y="50" width="110" height="48" class="b f4"/>&lt;text x="655" y="68" text-anchor="middle" class="tiny">F4&lt;/text>&lt;text x="655" y="84" text-anchor="middle" class="sm">Identidad&lt;/text>&lt;text x="655" y="98" text-anchor="middle" class="note">OIDC · Kyverno&lt;/text>
&lt;rect x="320" y="170" width="110" height="48" class="b f5"/>&lt;text x="375" y="188" text-anchor="middle" class="tiny">F5&lt;/text>&lt;text x="375" y="204" text-anchor="middle" class="sm">GPU plane&lt;/text>&lt;text x="375" y="218" text-anchor="middle" class="note">NVIDIA op · DCGM&lt;/text>
&lt;rect x="600" y="170" width="110" height="48" class="b f6"/>&lt;text x="655" y="188" text-anchor="middle" class="tiny">F6&lt;/text>&lt;text x="655" y="204" text-anchor="middle" class="sm">Stack LLM live&lt;/text>&lt;text x="655" y="218" text-anchor="middle" class="note">7 capas activas&lt;/text>
&lt;path class="crit" d="M150,74 L180,74"/>
&lt;path class="crit" d="M290,74 L320,74"/>
&lt;path class="crit" d="M430,74 L460,74"/>
&lt;path class="crit" d="M570,74 L600,74"/>
&lt;path class="arr" d="M655,98 L655,170"/>
&lt;path class="arr" d="M375,98 L375,170"/>
&lt;path class="crit" d="M430,194 L600,194"/>
&lt;text x="410" y="262" text-anchor="middle" class="sm" fill="#c33">Camino crítico: F0 → F1 → F2 → F3 → F4 → F5 → F6&lt;/text>
&lt;text x="410" y="282" text-anchor="middle" class="note">Solapes posibles: F2 ↔ F3 (preparar repo mientras se monta cluster) · F3 ↔ F4 (políticas en audit antes de enforce)&lt;/text>
&lt;text x="410" y="304" text-anchor="middle" class="note">No solapables: F4 antes de F5 (GPU sin RBAC = bomba) · F5 antes de F6 (stack LLM sin GPU plane no arranca)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Las flechas rojas son el &lt;strong>camino crítico&lt;/strong>: el cuello de botella secuencial que ningún paralelismo puede acortar. Las flechas grises son dependencias que admiten solape parcial. Reconocer dónde solapar y dónde no es la diferencia entre un despliegue de tres meses y uno de seis para el mismo perímetro.&lt;/p>
&lt;h2 id="la-analogía-la-expedición-a-una-cumbre-de-ocho-mil">La analogía: la expedición a una cumbre de ocho mil&lt;/h2>
&lt;p>Una expedición a una cumbre alpina alta no es un trekking largo. Es una serie de &lt;strong>campamentos&lt;/strong> que se montan en orden, cada uno con su altura, su función y su gate de validación: si no se aclimata bien en el campo base, no se puede subir al C1 sin riesgo; si el C2 no tiene su cocina y su radio en marcha, no se puede mandar gente arriba; si el ataque a cumbre se intenta sin los porteadores en los campamentos altos, no hay descenso seguro.&lt;/p>
&lt;p>El despliegue greenfield de una plataforma LLM funciona idéntico. &lt;strong>F0&lt;/strong> es la llegada del material al campamento base — cajas, sponsors, permisos, primera revisión. &lt;strong>F1&lt;/strong> es montar el campo base operativo: cocina, tiendas, generador. &lt;strong>F2&lt;/strong> es la subida al C1: ya hay altitud real (cluster k8s en marcha) y se respira distinto. &lt;strong>F3&lt;/strong> es C2: añade comunicaciones, planificación y aclimatación operativa. &lt;strong>F4&lt;/strong> es C3, la última noche antes del ataque: equipo cordado, oxígeno listo, todos los protocolos verificados. &lt;strong>F5&lt;/strong> es el día del ataque a cumbre — esfuerzo intenso, márgenes finos. &lt;strong>F6&lt;/strong> es la cumbre y el inicio del descenso seguro: a partir de aquí la expedición está en operación día a día, ya no en construcción.&lt;/p>
&lt;p>La analogía aguanta dos lecciones útiles: &lt;strong>no se salta un campo&lt;/strong> (subir directo del campo base a la cumbre mata al equipo), y &lt;strong>los gates son técnicos, no anímicos&lt;/strong> (si el barómetro pone tormenta, no se sale, aunque haya entusiasmo). El equipo de plataforma que sigue esas dos reglas llega a la cumbre. El que las negocia, no.&lt;/p>
&lt;h2 id="f0--hardware-en-la-sala-el-campamento-base">F0 — Hardware en la sala: el campamento base&lt;/h2>
&lt;p>&lt;strong>Lo que se monta en esta fase.&lt;/strong> Inventario del hardware recibido (servidores, switches, PDUs, BMC), racks montados, cableado eléctrico y de datos terminado, etiquetado físico de cada equipo (rack/U/función), conectividad a la red corporativa, IPs de gestión asignadas, BMC accesible vía VPN con MFA, primer ping de cada nodo desde el bastion.&lt;/p>
&lt;p>&lt;strong>Dependencias.&lt;/strong> Cero técnicas — esta es la fase &lt;strong>previa al software&lt;/strong>. Sí dependencias de procurement (servidores comprados, switches comprados), de obra civil (sala con climatización suficiente, suelo técnico) y administrativas (acceso al CPD para los técnicos).&lt;/p>
&lt;p>&lt;strong>Gate de validación que cierra F0.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Cada nodo aparece en el inventario con &lt;code>(hostname, MAC, IP gestión, IP datos, rack, U, función, owner)&lt;/code>.&lt;/li>
&lt;li>BMC de cada nodo responde a &lt;code>ipmitool power status&lt;/code> y a la UI HTTPS desde la VPN de gestión.&lt;/li>
&lt;li>El switch de top-of-rack tiene su configuración versionada en git (incluso si todavía no hay GitOps de cluster, los configs de switch sí).&lt;/li>
&lt;li>Un comando &lt;code>for h in $(cat hosts); do ping -c1 -W1 $h.mgmt; done&lt;/code> devuelve 100% de éxito.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Trampa típica.&lt;/strong> Cablear &amp;ldquo;como salga&amp;rdquo; sin etiquetado físico ni esquema. Cuando llega F4 y hay que troubleshootear una NetworkPolicy, no saber qué interfaz física lleva qué VLAN duplica el tiempo de diagnóstico de cada incidente para siempre.&lt;/p>
&lt;p>&lt;strong>Por qué F0 no se solapa con F1.&lt;/strong> Hasta que cada servidor tiene IP de gestión y BMC vivo, no se puede automatizar el bootstrap del OS. Toda hora invertida en F0 ahorra horas en cada fase posterior — es la fase con mejor ROI del proyecto y la única que no admite atajos.&lt;/p>
&lt;h2 id="f1--bare-metal-el-campamento-base-operativo">F1 — Bare metal: el campamento base operativo&lt;/h2>
&lt;p>&lt;strong>Lo que se monta.&lt;/strong> Imagen del sistema operativo (Debian estable o Ubuntu LTS) provisionada vía PXE o cloud-init con el &lt;code>cloud-config&lt;/code> versionado en git. Cada nodo tiene: hostname coherente, particiones LVM, kernel ≥ 6.6, container runtime &lt;code>containerd&lt;/code>, drivers NVIDIA para los nodos GPU, &lt;code>chrony&lt;/code> sincronizando contra servidores propios, SSH key del operador como única vía de acceso, &lt;code>nvidia-smi&lt;/code> pasando smoke test en los nodos GPU.&lt;/p>
&lt;p>&lt;strong>Dependencias.&lt;/strong> F0 cerrada. Necesita la red de gestión funcionando para que el PXE responda y para que el bastion alcance cada nodo.&lt;/p>
&lt;p>&lt;strong>Gate de validación que cierra F1.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>ansible -i inventory all -m ping&lt;/code> devuelve 100% de éxito (o equivalente con Salt / Pulumi / etc).&lt;/li>
&lt;li>Cada nodo GPU pasa &lt;code>nvidia-smi&lt;/code> mostrando las GPUs esperadas con driver consistente entre nodos.&lt;/li>
&lt;li>Reloj de cada nodo desviado &amp;lt; 50 ms del NTP de referencia.&lt;/li>
&lt;li>Reinicio físico de un nodo lo deja exactamente en el mismo estado tras boot (idempotencia).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Trampa típica.&lt;/strong> Drivers NVIDIA instalados manualmente con &lt;code>apt install&lt;/code> o con el script &lt;code>.run&lt;/code> de NVIDIA. Funciona el día uno y se rompe el día de la primera actualización de kernel. La regla operativa que ya quedó establecida en &lt;a href="https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/">el post de los cinco niveles&lt;/a>: los drivers acaban siendo gestionados por el GPU Operator en F5; lo que se haga ahora es solo para que &lt;code>nvidia-smi&lt;/code> pase el smoke test, no para producción.&lt;/p>
&lt;p>&lt;strong>Solape posible.&lt;/strong> F1 puede empezar para algunos nodos mientras todavía se finaliza F0 en otros (greenfield real raramente entrega todos los servidores el mismo día). El gate de F1 es por cluster, no por nodo individual.&lt;/p>
&lt;h2 id="f2--cluster-kubernetes-operativo">F2 — Cluster Kubernetes operativo&lt;/h2>
&lt;p>&lt;strong>Lo que se monta.&lt;/strong> RKE2 instalado con tres nodos de control plane HA, joining de todos los workers (CPU y GPU), Cilium como CNI con &lt;code>kubeProxyReplacement&lt;/code> habilitado y BGP control plane apuntando a los switches ToR del F0, Rook-Ceph desplegado en los nodos de storage para cubrir block (RBD), filesystem (CephFS) y object (RGW S3-compatible), &lt;code>kubectl get nodes&lt;/code> devolviendo todos los nodos &lt;code>Ready&lt;/code>, primer pod de prueba con PVC montando y datos persistiendo tras restart del pod.&lt;/p>
&lt;p>&lt;strong>Dependencias.&lt;/strong> F1 cerrada (drivers + container runtime). Switches con BGP configurado (lo cerrado en F0). Discos NVMe particionados o disponibles raw para Ceph OSDs.&lt;/p>
&lt;p>&lt;strong>Gate de validación que cierra F2.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>kubectl get nodes -o wide&lt;/code> muestra todos los nodos &lt;code>Ready&lt;/code> con la versión esperada de Kubernetes.&lt;/li>
&lt;li>Un Deployment con replicas=3 y antiAffinity por nodo arranca y los pods caen en nodos distintos.&lt;/li>
&lt;li>Una PVC RWO (RBD) crea un volumen, el pod escribe datos, el pod se borra, otro pod la monta y lee los datos.&lt;/li>
&lt;li>Una PVC RWX (CephFS) hace lo mismo con dos pods escribiendo simultáneamente.&lt;/li>
&lt;li>Un bucket RGW vía &lt;code>s3cmd&lt;/code> o &lt;code>mc&lt;/code> acepta &lt;code>put&lt;/code> y &lt;code>get&lt;/code> con TLS.&lt;/li>
&lt;li>Hubble (lado lectura del CNI) muestra flow logs entre dos pods de namespaces distintos.&lt;/li>
&lt;li>Test de chaos: drain de un nodo worker no GPU; las cargas se reschedulean automáticamente.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Trampa típica.&lt;/strong> Empezar a &lt;code>kubectl apply&lt;/code> cargas reales en F2 sin GitOps. El backlog de cosas-aplicadas-a-mano crece más rápido que la capacidad de migrarlo a git después. La regla: en F2 sólo se aplican los &lt;strong>prerrequisitos&lt;/strong> del cluster (CNI, CSI, storage class por defecto). Cualquier carga de aplicación espera a F3.&lt;/p>
&lt;p>&lt;strong>Solape posible.&lt;/strong> F2 ↔ F3. Mientras se monta el cluster, se prepara en paralelo el repo GitOps (estructura de directorios, primeras Helm releases). Cuando F2 cierra, Flux se enchufa al repo y todo lo que iba a ser &lt;code>kubectl apply&lt;/code> ya está como manifest reconciliado.&lt;/p>
&lt;h2 id="f3--gitops-y-observabilidad-de-infraestructura">F3 — GitOps y observabilidad de infraestructura&lt;/h2>
&lt;p>&lt;strong>Lo que se monta.&lt;/strong> Forgejo desplegado primero (es prerrequisito de todo lo que viene). Repo &lt;code>gitops-infra&lt;/code> con la estructura inicial (&lt;code>apps/&lt;/code>, &lt;code>infrastructure/&lt;/code>, &lt;code>tenants/&lt;/code>, &lt;code>clusters/&lt;/code>). Flux instalado y reconciliando ese repo. Las cargas de prerequisito que se aplicaron a mano en F2 se mueven al repo y se reconcilian (deja de haber &lt;code>kubectl apply&lt;/code> operativo). VictoriaMetrics + vmagent scrapeando métricas. Grafana con dashboards iniciales (USE/RED + cluster + Ceph + Cilium). Loki recibiendo logs vía vector/fluent-bit. Alertmanager + Keep enrutando alertas a un canal de chat. Backups Barman Cloud para Postgres (futuro CNPG) y snapshots Ceph programados.&lt;/p>
&lt;p>&lt;strong>Dependencias.&lt;/strong> F2 cerrada. Bucket RGW para almacenar backups (lo cubre Ceph del F2).&lt;/p>
&lt;p>&lt;strong>Gate de validación que cierra F3.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Cambio aplicado al repo se refleja en el cluster en &amp;lt; 5 minutos sin intervención manual.&lt;/li>
&lt;li>Un cambio aplicado con &lt;code>kubectl edit&lt;/code> directamente al cluster es detectado por Flux y revertido (drift detection vinculante, no sólo observacional).&lt;/li>
&lt;li>Grafana muestra dashboards de cluster, Ceph, Cilium y nodos GPU (DCGM no llega hasta F5, pero las métricas básicas del nodo sí).&lt;/li>
&lt;li>Un alert de prueba enviado a Alertmanager llega al canal de chat en &amp;lt; 1 minuto.&lt;/li>
&lt;li>Restore de un backup Postgres en un cluster temporal devuelve datos coherentes (la prueba define el RPO real).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Trampa típica.&lt;/strong> Tener Helm charts en git pero seguir aplicando con &lt;code>helm install&lt;/code> desde la terminal. Eso es nivel 1 con disfraz de nivel 2. F3 sólo se cierra cuando Flux es &lt;strong>la única autoridad&lt;/strong> que aplica cambios y los humanos editan repo, no cluster.&lt;/p>
&lt;p>&lt;strong>Solape posible.&lt;/strong> F3 ↔ F4. Mientras se cierra F3, se puede preparar el manifest de Defguard y cert-manager en el repo. Cuando se reconcilien tienen donde aterrizar.&lt;/p>
&lt;h2 id="f4--identidad-certificados-secretos-políticas">F4 — Identidad, certificados, secretos, políticas&lt;/h2>
&lt;p>&lt;strong>Lo que se monta.&lt;/strong> Defguard desplegado con su Postgres dedicado (CNPG). Realm inicial con los operadores de plataforma enrolados con MFA y WireGuard. OIDC integrado en kube-apiserver (&lt;code>--oidc-issuer-url&lt;/code>, &lt;code>--oidc-client-id&lt;/code>, &lt;code>--oidc-username-claim&lt;/code>), en Forgejo, en Grafana, en Alertmanager — un solo SSO. cert-manager instalado con CA interna emitiendo certs internos para mTLS y con Let&amp;rsquo;s Encrypt ACME para certs de borde. SOPS configurado con KMS (puede ser un HSM físico, una clave age en un cofre, o un Vault externo) y External Secrets Operator sincronizando secretos al cluster. Kyverno desplegado con políticas iniciales en modo &lt;code>audit&lt;/code> durante una semana, después promovidas a &lt;code>enforce&lt;/code>. NetworkPolicy default-deny aplicada a cada namespace existente. Tetragon habilitado para runtime security. Audit log de kube-apiserver enviado a Loki con retención larga.&lt;/p>
&lt;p>&lt;strong>Dependencias.&lt;/strong> F3 cerrada (Flux aplica los manifests, VM/Loki ingieren métricas y logs).&lt;/p>
&lt;p>&lt;strong>Gate de validación que cierra F4.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>kubectl&lt;/code> con &lt;code>kubeconfig&lt;/code> admin compartido &lt;strong>deja de funcionar&lt;/strong>; cada operador usa su propio token OIDC con MFA.&lt;/li>
&lt;li>Un secret en &lt;code>data:&lt;/code> plano en un commit es rechazado por el pre-commit hook (o por Kyverno admission).&lt;/li>
&lt;li>Un pod sin &lt;code>securityContext.runAsNonRoot=true&lt;/code> es rechazado por Kyverno en admission.&lt;/li>
&lt;li>Una NetworkPolicy intencionalmente errónea (allow-all) en un namespace de tenant es rechazada.&lt;/li>
&lt;li>Un audit del último día devuelve la lista completa de actores y cambios (huella regulatoria mínima).&lt;/li>
&lt;li>Pen-test interno básico: un atacante con &lt;code>kubeconfig&lt;/code> falsificado falla en MFA y queda registrado.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Trampa típica.&lt;/strong> Kyverno en modo &lt;code>audit&lt;/code> permanente porque &amp;ldquo;no queremos romper cargas en producción&amp;rdquo;. F4 se cierra cuando las políticas están en &lt;code>enforce&lt;/code>. Hasta entonces, sigues en F3 con cara de F4.&lt;/p>
&lt;p>&lt;strong>Por qué F4 no se solapa con F5.&lt;/strong> F5 introduce pods GPU que mueven mucha VRAM y mucho cómputo. Sin NetworkPolicy default-deny, sin RBAC OIDC, sin Kyverno bloqueando configuraciones inseguras, los pods GPU son la superficie de ataque más jugosa del cluster. Cualquier compromiso en F5 sin F4 cerrada es un acceso casi-total al hardware caro.&lt;/p>
&lt;h2 id="f5--plataforma-gpu-con-observabilidad-llm-aware">F5 — Plataforma GPU con observabilidad LLM-aware&lt;/h2>
&lt;p>&lt;strong>Lo que se monta.&lt;/strong> NVIDIA GPU Operator vía Flux con la versión de driver decidida en F1 (ahora ya no se manipula a mano). DCGM Exporter expone métricas GPU a VictoriaMetrics. MIG manager configurado para los nodos donde tenga sentido (por ejemplo, en un cluster 4×H100 SXM: dos GPUs con passthrough completo para el LLM general TP=4, dos GPUs particionadas en 2×3g.40gb cada una para LLMs pequeños y embeddings). Topology Manager con política &lt;code>single-numa-node&lt;/code>. KEDA con Prometheus scaler instalado y un ScaledObject de ejemplo apuntando a una métrica vLLM (&lt;code>vllm:num_requests_running&lt;/code>). OpenTelemetry Collector con receivers OTLP, processors &lt;code>attributes&lt;/code> (enriquecen spans con &lt;code>tenant_id&lt;/code>, &lt;code>priority_tier&lt;/code>), exporters a Langfuse y a Tempo. LeaderWorkerSet API habilitada para topologías tensor parallel. OME (Operator Model Engine) o vLLM Production Stack desplegado como controller — todavía sin modelos cargados.&lt;/p>
&lt;p>&lt;strong>Dependencias.&lt;/strong> F4 cerrada (los pods GPU heredan NetworkPolicy default-deny, RBAC OIDC y políticas Kyverno).&lt;/p>
&lt;p>&lt;strong>Gate de validación que cierra F5.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Un pod de prueba pidiendo &lt;code>nvidia.com/gpu: 1&lt;/code> se programa en el nodo correcto y &lt;code>nvidia-smi&lt;/code> desde dentro del contenedor ve la GPU correcta (entera o un slice MIG).&lt;/li>
&lt;li>DCGM Exporter expone métricas en Grafana (utilization, VRAM, temperatura, NVLink bandwidth) para cada GPU.&lt;/li>
&lt;li>Un Deployment de vLLM de prueba arranca con un modelo pequeño (por ejemplo, un 7B FP16) cargado desde Ceph RGW.&lt;/li>
&lt;li>Un span OpenTelemetry generado por ese vLLM llega a Langfuse con atributos &lt;code>gen_ai.*&lt;/code> correctos.&lt;/li>
&lt;li>KEDA escala el Deployment de prueba de 1 a N réplicas bajo carga sintética y vuelve a 1 cuando cesa.&lt;/li>
&lt;li>Un upgrade del GPU Operator a una nueva versión drena y reprograma los pods GPU sin pérdida de servicio.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Trampa típica.&lt;/strong> Cargar el modelo grande &amp;ldquo;para probar&amp;rdquo; antes de que DCGM y OTel estén verdes. Cuando algo falle, no habrá métricas que distingan entre OOM, throttling térmico, mismatch de driver o problema de red — se diagnostica a ciegas. La regla: &lt;strong>modelo pequeño primero&lt;/strong>, golden path verde, &lt;strong>después&lt;/strong> modelo grande.&lt;/p>
&lt;p>&lt;strong>Solape posible.&lt;/strong> Ninguno con F6. F6 es atómico.&lt;/p>
&lt;h2 id="f6--stack-llm-en-producción">F6 — Stack LLM en producción&lt;/h2>
&lt;p>&lt;strong>Lo que se monta.&lt;/strong> Las &lt;strong>siete capas del stack de inferencia&lt;/strong> descritas en el &lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">post correspondiente&lt;/a>, desplegadas en este orden:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Vector store + datos relacionales&lt;/strong> (Qdrant, PostgreSQL CNPG, Ceph RGW para pesos y adapters, CephFS para datasets). Algunos componentes ya existían de F3 como datos; aquí se especializan para RAG con sus colecciones y schemas iniciales.&lt;/li>
&lt;li>&lt;strong>Embeddings + reranker&lt;/strong> (Infinity con &lt;code>multilingual-e5-large&lt;/code>, TEI con &lt;code>bge-reranker-v2-m3&lt;/code>). Es la capa que debe estar verde antes de cualquier modelo grande, porque el RAG depende de ella.&lt;/li>
&lt;li>&lt;strong>Inferencia LLM&lt;/strong> (vLLM Production Stack con el LLM general y el LLM código). Carga modelos desde Ceph RGW. Multi-LoRA pool inicial vacío.&lt;/li>
&lt;li>&lt;strong>Gateway&lt;/strong> (Envoy AI Gateway) con OAuth Defguard, routing por &lt;code>body.model&lt;/code>, rate-limit por tenant. Este es el punto que &lt;strong>abre tráfico al exterior&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Observabilidad LLM-aware&lt;/strong> (Langfuse enchufado al OTel del F5).&lt;/li>
&lt;li>&lt;strong>Control plane GitOps&lt;/strong> y &lt;strong>dependency tracking&lt;/strong> ya estaban activos desde F3 y F4 respectivamente; aquí simplemente se les añade el catálogo de los nuevos servicios LLM.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Dependencias.&lt;/strong> Todas las anteriores cerradas.&lt;/p>
&lt;p>&lt;strong>Gate de validación que cierra F6.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Curl al endpoint público con bearer token Defguard recibe respuesta de chat completion en castellano técnico correcta, con &lt;code>trace_id&lt;/code> propagado.&lt;/li>
&lt;li>La traza aparece en Langfuse con atributos &lt;code>gen_ai.*&lt;/code> completos, latencia desglosada y &lt;code>tenant_id&lt;/code> propio.&lt;/li>
&lt;li>Un canary 5% de tráfico al nuevo modelo durante 24 h no degrada métricas de calidad ni de latencia.&lt;/li>
&lt;li>Un golpe de tráfico controlado dispara KEDA, las réplicas escalan, la latencia P95 se mantiene dentro de presupuesto.&lt;/li>
&lt;li>Un fallo intencional de un pod vLLM no afecta a la disponibilidad del endpoint (réplicas + reschedule).&lt;/li>
&lt;li>El operador interno demuestra el camino completo de revocación de acceso a un tenant en &amp;lt; 5 minutos (Defguard → Kyverno → cierre de NetworkPolicy).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Trampa típica.&lt;/strong> Abrir tráfico de cliente real antes de tener el runbook de incidentes firmado, el SLO negociado y el plan de continuidad probado. F6 técnicamente está cerrada; operativamente, la plataforma sigue siendo experimento hasta que el primer postmortem real demuestre que el equipo sabe responder.&lt;/p>
&lt;h2 id="las-matemáticas-que-importan-peso-relativo-del-esfuerzo-por-fase">Las matemáticas que importan: peso relativo del esfuerzo por fase&lt;/h2>
&lt;p>Sin comprometernos con semanas calendario, sí podemos cuantificar el &lt;strong>peso relativo&lt;/strong> del esfuerzo de ingeniería por fase en un greenfield típico. La curva no es uniforme:&lt;/p>
&lt;p>$$
\text{esfuerzo}_{F_i} \approx \text{base}_i \cdot (1 + \epsilon_i)
$$&lt;/p>
&lt;p>donde $\text{base}_i$ es el esfuerzo nominal y $\epsilon_i$ es el factor de &lt;strong>sorpresas&lt;/strong> (cabling errado, drivers incompatibles, certificados mal emitidos, conflictos de versiones). La tabla siguiente da el peso relativo nominal y el factor típico de sorpresa observado:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Fase&lt;/th>
&lt;th>Peso nominal&lt;/th>
&lt;th>Factor sorpresa típico ε&lt;/th>
&lt;th>Peso efectivo medio&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>F0 — Hardware&lt;/td>
&lt;td>8 %&lt;/td>
&lt;td>0.5 (1× a 2×)&lt;/td>
&lt;td>&lt;strong>12 %&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F1 — Bare metal&lt;/td>
&lt;td>6 %&lt;/td>
&lt;td>0.3&lt;/td>
&lt;td>8 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F2 — Cluster k8s&lt;/td>
&lt;td>12 %&lt;/td>
&lt;td>0.4&lt;/td>
&lt;td>17 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F3 — GitOps + obs&lt;/td>
&lt;td>14 %&lt;/td>
&lt;td>0.5&lt;/td>
&lt;td>21 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F4 — Identidad + políticas&lt;/td>
&lt;td>18 %&lt;/td>
&lt;td>0.7&lt;/td>
&lt;td>&lt;strong>31 %&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F5 — GPU plane&lt;/td>
&lt;td>10 %&lt;/td>
&lt;td>0.4&lt;/td>
&lt;td>14 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F6 — Stack LLM live&lt;/td>
&lt;td>8 %&lt;/td>
&lt;td>0.3&lt;/td>
&lt;td>10 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Buffer / integración&lt;/td>
&lt;td>24 %&lt;/td>
&lt;td>—&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Dos observaciones operativas. &lt;strong>F4 concentra más sorpresas que ninguna otra&lt;/strong> (federación OIDC entre cuatro o cinco apps con configuraciones distintas, políticas Kyverno que tumban cargas legítimas, secretos rotos por encriptación mal probada). &lt;strong>F0 tiene un coeficiente de sorpresa alto en relación a su tamaño&lt;/strong> porque cualquier error de cableado o etiquetado se descubre tarde y se paga caro. Las dos consecuencias prácticas: planificar &lt;strong>F4 con margen generoso&lt;/strong> y no escatimar tiempo en &lt;strong>F0&lt;/strong> porque cada hora ahorrada ahí cuesta cinco después.&lt;/p>
&lt;p>&lt;strong>Camino crítico y holguras.&lt;/strong> El camino crítico es lineal F0 → F1 → F2 → F3 → F4 → F5 → F6. Las únicas holguras reales son los solapes ya identificados:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>F2 ↔ F3 (holgura ~30 %)&lt;/strong>: preparar repo y dashboards iniciales mientras se monta cluster.&lt;/li>
&lt;li>&lt;strong>F3 ↔ F4 (holgura ~20 %)&lt;/strong>: manifests de identidad listos al cerrar F3, aplicación inmediata.&lt;/li>
&lt;li>&lt;strong>Dentro de F4&lt;/strong>: políticas en modo &lt;code>audit&lt;/code> corriendo en paralelo con setup de Defguard.&lt;/li>
&lt;/ul>
&lt;p>Nada acorta el camino crítico más de un ~15 % del total. Quien promete un greenfield productivo en la mitad del tiempo razonable está vendiendo otra cosa: probablemente saltarse F4 o cargar F6 con F5 verde-pero-no-validado.&lt;/p>
&lt;h2 id="diagrama-final-el-cronograma-de-despliegue-completo">Diagrama final: el cronograma de despliegue completo&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="cronograma completo del despliegue por fases con piezas y gates">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.bg{fill:#fafafa;stroke:#bbb;rx:8}.f0{fill:#f6e2e2;stroke:#a33}.f1{fill:#f4e3cf;stroke:#a63}.f2{fill:#eef0d0;stroke:#7a3}.f3{fill:#dfe9f5;stroke:#356}.f4{fill:#d8eecf;stroke:#373}.f5{fill:#f5e3d8;stroke:#763}.f6{fill:#ead8f5;stroke:#634}.gate{fill:#fffbe0;stroke:#a90}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#222}.tiny{font:600 10px sans-serif;fill:#222}.note{font:italic 10px sans-serif;fill:#555}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#a)}&lt;/style>
&lt;defs>&lt;marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="22" text-anchor="middle" class="lbl">Cronograma completo: piezas por fase y gates de validación&lt;/text>
&lt;rect x="40" y="40" width="740" height="68" class="b f0"/>&lt;text x="60" y="58" class="tiny">F0 · HARDWARE EN LA SALA&lt;/text>
&lt;text x="60" y="76" class="sm">Inventario · cableado · BMC TLS+MFA · IPs gestión · switches BGP versionados&lt;/text>
&lt;text x="60" y="92" class="note">Gate: `for h in hosts; ping $h.mgmt` 100% éxito · inventario completo&lt;/text>
&lt;rect x="40" y="116" width="740" height="68" class="b f1"/>&lt;text x="60" y="134" class="tiny">F1 · BARE METAL&lt;/text>
&lt;text x="60" y="152" class="sm">PXE/cloud-init · OS LTS · kernel ≥6.6 · containerd · drivers NVIDIA · chrony · LVM&lt;/text>
&lt;text x="60" y="168" class="note">Gate: `ansible all -m ping` 100% · `nvidia-smi` smoke OK · reboot idempotente&lt;/text>
&lt;rect x="40" y="192" width="740" height="68" class="b f2"/>&lt;text x="60" y="210" class="tiny">F2 · CLUSTER KUBERNETES&lt;/text>
&lt;text x="60" y="228" class="sm">RKE2 HA · Cilium (kube-proxy replacement + BGP) · Rook-Ceph (RBD + CephFS + RGW)&lt;/text>
&lt;text x="60" y="244" class="note">Gate: PVCs RWO/RWX OK · bucket RGW OK · drain node sin downtime&lt;/text>
&lt;rect x="40" y="268" width="740" height="68" class="b f3"/>&lt;text x="60" y="286" class="tiny">F3 · GITOPS + OBSERVABILIDAD INFRA&lt;/text>
&lt;text x="60" y="304" class="sm">Forgejo · Flux · VictoriaMetrics + Grafana + Loki · Alertmanager + Keep · backups&lt;/text>
&lt;text x="60" y="320" class="note">Gate: cambio en repo → cluster en &amp;lt;5min · drift revertido · restore backup OK&lt;/text>
&lt;rect x="40" y="344" width="740" height="68" class="b f4"/>&lt;text x="60" y="362" class="tiny">F4 · IDENTIDAD + POLÍTICAS&lt;/text>
&lt;text x="60" y="380" class="sm">Defguard OIDC+MFA+WG · cert-manager · SOPS+ESO · Kyverno enforce · NP default deny · Tetragon&lt;/text>
&lt;text x="60" y="396" class="note">Gate: kubeconfig admin compartido no funciona · políticas en enforce · audit log completo&lt;/text>
&lt;rect x="40" y="420" width="740" height="68" class="b f5"/>&lt;text x="60" y="438" class="tiny">F5 · PLATAFORMA GPU + OBSERVABILIDAD LLM-AWARE&lt;/text>
&lt;text x="60" y="456" class="sm">NVIDIA GPU Operator · DCGM · MIG manager · KEDA con métricas vLLM · OTel gen_ai.* · OME&lt;/text>
&lt;text x="60" y="472" class="note">Gate: pod GPU programado · DCGM verde · vLLM smoke con modelo pequeño · KEDA escala&lt;/text>
&lt;rect x="40" y="496" width="740" height="38" class="b f6"/>&lt;text x="60" y="514" class="tiny">F6 · STACK LLM LIVE&lt;/text>
&lt;text x="60" y="528" class="sm">7 capas activas · primer modelo verde · canary OK · runbook firmado · primer cliente con SLA&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>El cronograma no es decorativo: cada fila define lo que se monta en su fase &lt;strong>y el gate que la cierra&lt;/strong>. Una fase no se da por terminada hasta que su gate está verde. Una fase con gate amarillo arrastra todas las posteriores; intentar saltar a la siguiente con un gate parcialmente cumplido es lo que produce, varias semanas después, el incidente que obliga a &amp;ldquo;volver a F4 con producción rodando&amp;rdquo; — la situación más cara de toda la matriz de costes del &lt;a href="https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/">post de los cinco niveles&lt;/a>.&lt;/p>
&lt;h2 id="errores-típicos-de-planificación">Errores típicos de planificación&lt;/h2>
&lt;p>Patrones que retrasan o hunden el despliegue greenfield, independientemente de las herramientas elegidas:&lt;/p>
&lt;p>&lt;strong>1. Comprar el LLM antes que el cluster.&lt;/strong> Empezar el proyecto por &amp;ldquo;qué modelo vamos a servir&amp;rdquo; en vez de por &amp;ldquo;qué plataforma puede sostener cualquier modelo razonable&amp;rdquo;. El modelo es un parámetro intercambiable; la plataforma no.&lt;/p>
&lt;p>&lt;strong>2. Subestimar F0.&lt;/strong> &amp;ldquo;Eso lo hace el equipo de redes&amp;rdquo;. Sí, pero el resultado de F0 lo consumen todas las fases posteriores. Si el equipo de redes entrega tarde, el proyecto entero llega tarde — y nadie lo había marcado como camino crítico.&lt;/p>
&lt;p>&lt;strong>3. Solapar F4 con F5 &amp;ldquo;para ganar tiempo&amp;rdquo;.&lt;/strong> Es la única dependencia donde no hay holgura. Si se intenta solapar, F5 acaba operando con políticas en &lt;code>audit&lt;/code> permanente (no estás en F4) o sin OIDC integrado (operadores con kubeconfig compartido tocando GPU). Ambos antipatrones se quedan en producción.&lt;/p>
&lt;p>&lt;strong>4. Saltar el smoke test del modelo pequeño en F5.&lt;/strong> &amp;ldquo;Vamos a por el 70B directamente&amp;rdquo;. Cuando algo falle (y algo fallará), no habrá baseline contra el que diagnosticar.&lt;/p>
&lt;p>&lt;strong>5. Tratar F6 como &amp;ldquo;encender vLLM&amp;rdquo;.&lt;/strong> F6 incluye gateway, observabilidad LLM-aware, runbook, SLO, plan de continuidad. Encender vLLM es cinco minutos; cerrar F6 es semanas de validación y firma.&lt;/p>
&lt;p>&lt;strong>6. No definir gates por escrito.&lt;/strong> Si los gates no están escritos, son negociables a posteriori. &amp;ldquo;Esto ya cuenta como F4&amp;rdquo; es la frase que precede a los seis meses siguientes de retrofit.&lt;/p>
&lt;p>&lt;strong>7. Asignar la fase a un único responsable.&lt;/strong> Cada fase necesita al menos dos personas que la entiendan. La rotación de personal en proyectos largos destruye el conocimiento; los gates por escrito + revisión cruzada lo preservan.&lt;/p>
&lt;p>&lt;strong>8. Olvidar el camino de descenso.&lt;/strong> El post se centra en subir. La operación día a día (descenso, en la analogía) es otra historia que también merece planificación — runbooks, on-call, capacidad de upgrade, plan de fin de vida. Los equipos que sólo planifican la subida llegan a la cumbre y se quedan ahí sin oxígeno.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise-típico-4h100-sxm">Aplicado a hardware on-premise típico: 4×H100 SXM&lt;/h2>
&lt;p>Sobre el cluster genérico de referencia (4×H100 SXM 80 GB, NVLink, 640 GB RAM por nodo GPU, 3 nodos control plane, 3-5 nodos worker CPU, 2 nodos worker GPU), el reparto &lt;strong>temporal&lt;/strong> del trabajo se distribuye así:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">F0 (hardware)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ 8 servidores físicos racks + switches + BMC + IPs gestión
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ 3 nodos cp-01..03 — control plane (sin GPU)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ 3 nodos worker-cpu-01..03 — CPU plane (Forgejo, Ceph, observabilidad)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ 2 nodos worker-gpu-01..02 — GPU plane (4×H100 SXM cada uno)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F1 (bare metal)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ OS + drivers + containerd en los 8 nodos por igual
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (los drivers NVIDIA solo en los 2 nodos GPU, smoke `nvidia-smi`)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F2 (cluster k8s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ RKE2 control plane en cp-01..03 (HA con etcd embebido)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> workers joining: 3 CPU + 2 GPU
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Ceph OSDs en los 3 nodos worker CPU
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pools por defecto: RBD-replicated-3, CephFS-replicated-3, RGW
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F3 (GitOps + obs)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ Forgejo + Flux + VM/Grafana/Loki + Keep en CPU plane
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> primer repo `gitops-infra` reconcilia lo de F2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F4 (identidad)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ Defguard en CPU plane (StatefulSet con Postgres CNPG)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OIDC en kube-apiserver, Forgejo, Grafana, Alertmanager
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Kyverno como Deployment en control plane
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F5 (GPU plane)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ NVIDIA GPU Operator targetea workers GPU
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> MIG manager: 1ª GPU MIG 7g.80gb (= passthrough), 2ª 2×3g.40gb
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OTel Collector como DaemonSet en GPU plane + CPU plane
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> primer vLLM con modelo 7B FP16 verde
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F6 (stack LLM)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ Las 7 capas se reconcilian vía Flux desde un segundo repo `gitops-llm`
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> primer endpoint público con OAuth Defguard
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> primer cliente productivo enrolado bajo SLA
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La distribución física del cluster aprovecha el aislamiento entre planos definido en F0: el plano de control no toca GPU, el plano CPU concentra estado relevante (Forgejo, Ceph, Postgres CNPG, Langfuse, Defguard) y el plano GPU se especializa al máximo. Esa separación, decidida en F0 antes de instalar el primer servidor, condiciona el éxito del resto de fases — es otro recordatorio de por qué F0 importa más de lo que parece.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-posts">Lo que no hemos cubierto (próximos posts)&lt;/h2>
&lt;p>Este post recorre el &lt;strong>camino de subida&lt;/strong> a la cumbre. Quedan piezas que merecen su propio artículo:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>El descenso seguro&lt;/strong>: operación día a día, runbooks por componente, on-call, capacity planning continuo, ciclo de upgrades del cluster sin downtime.&lt;/li>
&lt;li>&lt;strong>Multi-site (segunda cumbre)&lt;/strong>: cómo se federan dos clusters con Cilium Cluster Mesh y qué fases extra introduce. F3.5 (Cluster Mesh) y F4.5 (replicación cross-site) son las fases que faltan.&lt;/li>
&lt;li>&lt;strong>El camino brownfield&lt;/strong>: lo que cambia cuando ya hay un cluster con cargas. Las fases siguen siendo las mismas, pero los gates se aplican retroactivamente y cada paso requiere planning de migración.&lt;/li>
&lt;li>&lt;strong>El coste calendario real&lt;/strong>: rangos típicos en semanas para un equipo de plataforma de 2-3 personas, separado por fase, con bandas de incertidumbre.&lt;/li>
&lt;li>&lt;strong>El handoff a operación&lt;/strong>: cómo se entrega la plataforma del equipo de despliegue al equipo de operación, qué documentos firman, qué se hereda y qué se renegocia.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">Anatomía de un stack de inferencia LLM on-premise&lt;/a> — las siete capas que se montan en F6. Los componentes, no el cronograma.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/">Cinco niveles de madurez de la plataforma debajo del LLM&lt;/a> — los niveles correspondientes a las fases F1→F5. Los estratos, no la secuencia.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/catalogo-herramientas-oss-llmops/">El catálogo OSS para LLMOps en seis etapas&lt;/a> — fichas individuales de las piezas citadas aquí.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a> — el OTel del F5 con detalle de las semantic conventions &lt;code>gen_ai.*&lt;/code>.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>RKE2 Documentation — &lt;a href="https://docs.rke2.io/">docs.rke2.io&lt;/a>&lt;/li>
&lt;li>Cilium documentation — &lt;a href="https://docs.cilium.io/">docs.cilium.io&lt;/a>&lt;/li>
&lt;li>Rook documentation — &lt;a href="https://rook.io/">rook.io/docs&lt;/a>&lt;/li>
&lt;li>Flux GitOps toolkit — &lt;a href="https://fluxcd.io/">fluxcd.io&lt;/a>&lt;/li>
&lt;li>Forgejo — &lt;a href="https://forgejo.org/">forgejo.org&lt;/a>&lt;/li>
&lt;li>cert-manager — &lt;a href="https://cert-manager.io/">cert-manager.io&lt;/a>&lt;/li>
&lt;li>External Secrets Operator — &lt;a href="https://external-secrets.io/">external-secrets.io&lt;/a>&lt;/li>
&lt;li>Kyverno — &lt;a href="https://kyverno.io/">kyverno.io&lt;/a>&lt;/li>
&lt;li>NVIDIA GPU Operator — &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/">docs.nvidia.com/datacenter/cloud-native/gpu-operator&lt;/a>&lt;/li>
&lt;li>DCGM Exporter — &lt;a href="https://github.com/NVIDIA/dcgm-exporter">github.com/NVIDIA/dcgm-exporter&lt;/a>&lt;/li>
&lt;li>KEDA — &lt;a href="https://keda.sh/">keda.sh&lt;/a>&lt;/li>
&lt;li>LeaderWorkerSet API — &lt;a href="https://github.com/kubernetes-sigs/lws">github.com/kubernetes-sigs/lws&lt;/a>&lt;/li>
&lt;li>OpenTelemetry Semantic Conventions for GenAI — &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">opentelemetry.io/docs/specs/semconv/gen-ai&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Cinco niveles de madurez de la plataforma debajo del LLM: del servidor con Linux al cluster listo para vLLM</title><link>https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/</link><pubDate>Sun, 31 May 2026 07:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El post de &lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">las siete capas del stack de inferencia LLM&lt;/a> daba por supuestas muchas piezas: un cluster Kubernetes operativo, GitOps reconciliando, identidades resueltas, GPUs visibles para el scheduler, observabilidad capaz de transportar &lt;code>gen_ai.*&lt;/code>. Antes de que vLLM tenga sentido, &lt;strong>hay que llegar a ese punto de partida&lt;/strong>, y se llega por niveles. Este post define &lt;strong>cinco niveles de madurez&lt;/strong> de la plataforma que vive debajo del LLM, desde un servidor bare metal con Linux instalado (nivel 0) hasta un cluster listo para correr la capa de inferencia (nivel 4) y el handoff al post anterior (nivel 5). Cada nivel &lt;strong>desbloquea una capacidad concreta&lt;/strong> —ejecutar contenedores con reproducibilidad, reconstruir el cluster desde git, autenticar humanos vía OIDC, programar GPUs con MIG y métricas DCGM, demostrar compliance sin intervención manual— y cada uno tiene un &lt;strong>test de validación&lt;/strong> que decide si estás de verdad ahí o solo te lo cuentas. Para cada nivel: qué piezas OSS lo cubren en 2026 (Cilium, RKE2, Flux, cert-manager, Defguard, NVIDIA GPU Operator, KEDA, Trivy, Kyverno…), &lt;strong>el orden de despliegue&lt;/strong> dentro del nivel, las decisiones que cuesta caro saltarse, y los antipatrones que te bajan de nivel cuando creías estar arriba. La tesis: &lt;strong>subir de nivel cuesta poco esfuerzo si lo haces a tiempo, y mucho refactor si pretendes saltártelo&lt;/strong>. La inferencia LLM exige al menos nivel 4; quien intenta servir LLMs desde un nivel 1 o 2 acaba pagando con incidentes nocturnos lo que se ahorró en plataforma.&lt;/p>
&lt;h2 id="estás-aquí-los-cinco-niveles-de-un-vistazo">Estás aquí: los cinco niveles de un vistazo&lt;/h2>
&lt;p>Antes del detalle, la escalera. Cada peldaño añade una capacidad ausente en el anterior. El test del nivel es la pregunta cuya respuesta honesta dice si ya estás en él.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="cinco niveles de madurez de la plataforma debajo del LLM">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.l0{fill:#f6e2e2;stroke:#a33}.l1{fill:#f4e3cf;stroke:#a63}.l2{fill:#eef0d0;stroke:#7a3}.l3{fill:#d8eecf;stroke:#373}.l4{fill:#dfe9f5;stroke:#356}.l5{fill:#ead8f5;stroke:#634}.title{font:600 13px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#222}.tiny{font:600 10px sans-serif;fill:#222}.note{font:italic 10px sans-serif;fill:#555}&lt;/style>
&lt;text x="410" y="20" text-anchor="middle" class="title">Cinco niveles de madurez (más el handoff al stack LLM en el nivel 5)&lt;/text>
&lt;rect x="40" y="38" width="740" height="44" class="b l0"/>&lt;text x="60" y="56" class="tiny">NIVEL 0 · CAÓTICO&lt;/text>&lt;text x="60" y="74" class="sm">Bare metal con Linux · docker / podman ad-hoc · sin orquestador · cambios manuales con SSH&lt;/text>
&lt;rect x="40" y="90" width="740" height="44" class="b l1"/>&lt;text x="60" y="108" class="tiny">NIVEL 1 · REPETIBLE&lt;/text>&lt;text x="60" y="126" class="sm">Cluster k8s instalado (RKE2 / kubeadm) · CNI · CSI · kubectl apply / Helm desde terminal · pods rodando&lt;/text>
&lt;rect x="40" y="142" width="740" height="44" class="b l2"/>&lt;text x="60" y="160" class="tiny">NIVEL 2 · DEFINIDO&lt;/text>&lt;text x="60" y="178" class="sm">GitOps (Flux) · registry interno · observabilidad infra · backups · el cluster se reconstruye desde el repo&lt;/text>
&lt;rect x="40" y="194" width="740" height="44" class="b l3"/>&lt;text x="60" y="212" class="tiny">NIVEL 3 · GESTIONADO&lt;/text>&lt;text x="60" y="230" class="sm">OIDC + RBAC · cert-manager · External Secrets · Kyverno · NetworkPolicy default deny · auditoría&lt;/text>
&lt;rect x="40" y="246" width="740" height="44" class="b l4"/>&lt;text x="60" y="264" class="tiny">NIVEL 4 · OPTIMIZADO PARA GPU&lt;/text>&lt;text x="60" y="282" class="sm">NVIDIA GPU Operator · DCGM Exporter · MIG / time-slicing · KEDA con métricas LLM · OTel listo para gen_ai.*&lt;/text>
&lt;rect x="40" y="298" width="740" height="32" class="b l5"/>&lt;text x="60" y="318" class="sm">&lt;tspan font-weight="700">NIVEL 5 · HANDOFF&lt;/tspan> — el cluster está preparado para que el stack LLM (las 7 capas) tenga sentido&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Los niveles &lt;strong>no son intercambiables&lt;/strong>. Un cluster en nivel 2 no puede correr LLMs en producción con garantías: técnicamente carga el pod de vLLM, pero al primer incidente nocturno se descubre que no hay TLS, ni identidades, ni alerting, ni métricas GPU, ni forma de saber quién cambió qué. Subir un nivel después de tener LLMs ya en producción cuesta &lt;strong>órdenes de magnitud&lt;/strong> más que subirlo cuando el cluster aún está vacío.&lt;/p>
&lt;h2 id="la-analogía-del-puesto-callejero-al-restaurante-con-estrella">La analogía: del puesto callejero al restaurante con estrella&lt;/h2>
&lt;p>Imagina la escala de un negocio de hostelería. &lt;strong>Nivel 0&lt;/strong> es el puesto callejero: una plancha, una bombona, un cocinero que improvisa. Puede vender comida — funciona — pero cualquier cosa que se desvíe del día normal (una inspección sanitaria, un cliente alérgico, un pedido de 200 raciones) le tira el negocio. &lt;strong>Nivel 1&lt;/strong> es el bar de tapas: cocina dimensionada, carta corta repetible, varios turnos. El cocinero ya no improvisa cada día; trabaja sobre un menú escrito, aunque las recetas viven en la cabeza del jefe. &lt;strong>Nivel 2&lt;/strong> es el restaurante con menú del día: hay procedimientos escritos, proveedores fijos, control de stock, libro de incidencias. Si el cocinero principal se cae enfermo, el segundo puede sacar el servicio sin estragos. &lt;strong>Nivel 3&lt;/strong> es el restaurante con carta y servicio formal: trazabilidad de cada ingrediente, alérgenos en la carta, certificación sanitaria, contrato con los proveedores, formación obligatoria del personal. &lt;strong>Nivel 4&lt;/strong> es la cocina especializada en un producto complejo (sushi, alta cocina, panadería artesanal): herramientas específicas que el restaurante normal no necesita (horno de leña, cuchillos especiales, cámara de fermentación), procesos calibrados, métricas de calidad. &lt;strong>Nivel 5&lt;/strong> es el restaurante con estrella Michelin: el sistema entero funciona, &lt;strong>el plato es el resultado de la organización, no del talento de una persona&lt;/strong>.&lt;/p>
&lt;p>La analogía aguanta hasta el final, incluido el detalle más interesante: &lt;strong>se puede operar a cualquier nivel&lt;/strong>, pero las promesas que se pueden cumplir son distintas. El puesto callejero no puede prometer una experiencia consistente a 80 comensales con reserva. El cluster en nivel 1 no puede prometer servicio LLM productivo multi-tenant con SLA. En ambos casos el problema no es de &lt;strong>capacidad técnica del último componente&lt;/strong> (la plancha cocina; el pod arranca); es de &lt;strong>capacidad organizativa del sistema entero&lt;/strong>.&lt;/p>
&lt;p>Vamos nivel por nivel.&lt;/p>
&lt;h2 id="nivel-0--caótico-el-servidor-con-linux-y-nada-más">Nivel 0 — Caótico: el servidor con Linux y nada más&lt;/h2>
&lt;p>&lt;strong>La capacidad que da.&lt;/strong> Ejecutar contenedores con &lt;code>docker&lt;/code>/&lt;code>podman&lt;/code>, ejecutar binarios, conectar el servidor a la red. El operador puede entrar por SSH, hacer cosas, y ver resultados.&lt;/p>
&lt;p>&lt;strong>El test del nivel.&lt;/strong> &lt;em>&amp;ldquo;Si reinstalo el servidor desde cero, ¿puedo dejarlo idéntico a como estaba en una tarde, usando sólo notas guardadas?&amp;rdquo;&lt;/em>. Si la respuesta es no (porque los pasos están en la cabeza del que lo montó, en &lt;code>.bash_history&lt;/code>, en un wiki desactualizado), estás en nivel 0.&lt;/p>
&lt;p>&lt;strong>Piezas mínimas que dejar resueltas antes de subir a nivel 1.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pieza&lt;/th>
&lt;th>Decisión sugerida en 2026&lt;/th>
&lt;th>Por qué importa al subir&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Distribución Linux&lt;/td>
&lt;td>Debian estable u Ubuntu LTS&lt;/td>
&lt;td>Soporte largo, predecible, kernel reciente disponible&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kernel&lt;/td>
&lt;td>LTS reciente (≥ 6.6) con BPF y schedulers modernos&lt;/td>
&lt;td>Cilium/eBPF, drivers NVIDIA recientes lo exigen&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Drivers NVIDIA&lt;/td>
&lt;td>Versión que casa con la CUDA del motor LLM que vas a servir&lt;/td>
&lt;td>Mismatch driver/CUDA bloquea vLLM antes de empezar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Container runtime&lt;/td>
&lt;td>&lt;code>containerd&lt;/code>&lt;/td>
&lt;td>Estándar CNCF, integrado con RKE2/kubeadm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Filesystem raíz&lt;/td>
&lt;td>XFS o ext4 + LVM thin pools&lt;/td>
&lt;td>Snapshots, ampliación en caliente&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sincronización horaria&lt;/td>
&lt;td>&lt;code>chrony&lt;/code> con servidores propios&lt;/td>
&lt;td>TLS, logs correlados, certificados cortos lo exigen&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Red de gestión&lt;/td>
&lt;td>VLAN dedicada, ACLs en switch&lt;/td>
&lt;td>Aislar plano de control del tráfico de carga&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Red de cluster&lt;/td>
&lt;td>LACP + jumbo frames + BGP (si vas a Cilium)&lt;/td>
&lt;td>NVLink intra-nodo no salva la red de servicio&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>BMC / IPMI&lt;/td>
&lt;td>Acceso fuera de banda con TLS y MFA&lt;/td>
&lt;td>Recuperación cuando el sistema operativo no arranca&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Antipatrones que te dejan clavado en nivel 0.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Servidores &lt;strong>mascota&lt;/strong> (con nombre propio, configurados a mano, no reemplazables).&lt;/li>
&lt;li>Cambios aplicados con &lt;code>vi&lt;/code> directo sobre &lt;code>/etc/...&lt;/code> sin commit a un repo.&lt;/li>
&lt;li>Despliegue con &lt;code>docker-compose&lt;/code> sin healthchecks ni reinicio automático.&lt;/li>
&lt;li>Inventario que vive en una hoja Excel que nadie actualiza.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Orden de despliegue dentro del nivel.&lt;/strong> Imagen del sistema desde PXE/cloud-init con configuración inicial (LVM, hostname, red, SSH key, chrony) → bootstrap de bastion/jump host → inventario en Ansible (o equivalente declarativo aunque luego se reemplace) → drivers NVIDIA + container runtime → smoke test (un contenedor CUDA pasa &lt;code>nvidia-smi&lt;/code>). En este punto, el servidor está listo para que entre Kubernetes.&lt;/p>
&lt;h2 id="nivel-1--repetible-cluster-kubernetes-operativo">Nivel 1 — Repetible: cluster Kubernetes operativo&lt;/h2>
&lt;p>&lt;strong>La capacidad que da.&lt;/strong> Programar contenedores con scheduler, abstracción de red entre pods, volúmenes persistentes, lifecycle de cargas, escalado horizontal manual.&lt;/p>
&lt;p>&lt;strong>El test del nivel.&lt;/strong> &lt;em>&amp;quot;¿Puedo perder un nodo y que las cargas se reprogramen sin intervención humana?&amp;quot;&lt;/em>. Si sí, estás en nivel 1. Si no — porque los pods están pinneados a nodos, porque no hay réplicas, porque las PVCs no se reattachean — sigues en 0 con Kubernetes encima.&lt;/p>
&lt;p>&lt;strong>Piezas mínimas del nivel.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pieza&lt;/th>
&lt;th>Decisión sugerida en 2026&lt;/th>
&lt;th>Alternativa principal&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Distribución k8s&lt;/td>
&lt;td>&lt;strong>RKE2&lt;/strong> (CIS-hardened por defecto, sin sobrecosto comercial)&lt;/td>
&lt;td>k3s para edge muy pequeño, kubeadm puro para casos custom&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>CNI&lt;/td>
&lt;td>&lt;strong>Cilium&lt;/strong> con kube-proxy replacement, BGP, Gateway API&lt;/td>
&lt;td>Calico (sin BGP no compite contra Cilium en 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>CSI block + filesystem + object&lt;/td>
&lt;td>&lt;strong>Rook-Ceph&lt;/strong> (RBD + CephFS + RGW S3-compatible)&lt;/td>
&lt;td>OpenEBS Mayastor + Garage para deployments pequeños&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ingress&lt;/td>
&lt;td>Cilium Gateway API (mejor unificar con CNI)&lt;/td>
&lt;td>NGINX Ingress, Traefik&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cert básico&lt;/td>
&lt;td>Self-signed bootstrap&lt;/td>
&lt;td>(cert-manager entra en nivel 3)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Manejo de cargas&lt;/td>
&lt;td>&lt;code>kubectl apply&lt;/code> + Helm desde terminal&lt;/td>
&lt;td>Sin GitOps todavía&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Container registry&lt;/td>
&lt;td>Cualquier registry interno (o externo de confianza) con TLS&lt;/td>
&lt;td>(registry interno gestionado entra en nivel 2)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Antipatrones que te bajan a nivel 0.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Servicios desplegados con &lt;code>kubectl apply&lt;/code> desde la terminal de una persona y &lt;strong>sin guardar el YAML en ninguna parte&lt;/strong>.&lt;/li>
&lt;li>Volúmenes persistentes sin política de backup.&lt;/li>
&lt;li>&amp;ldquo;Cluster de un nodo&amp;rdquo; como producción permanente — un solo punto de fallo arquitectónico.&lt;/li>
&lt;li>CNI sin NetworkPolicy disponible o sin BGP cuando la red lo requiere.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Orden de despliegue dentro del nivel.&lt;/strong> RKE2 instalado en al menos tres nodos para control plane HA → Cilium instalado en modo kube-proxy replacement + BGP control plane → Rook-Ceph en al menos tres nodos cubriendo block (RBD) + filesystem (CephFS) + object (RGW S3-compatible) con replicación 3× o Erasure Coding según pool → smoke test (un Deployment con PVC arranca, los pods se reschedulean al cordon de un nodo, los datos persisten).&lt;/p>
&lt;h2 id="nivel-2--definido-el-cluster-se-reconstruye-desde-git">Nivel 2 — Definido: el cluster se reconstruye desde git&lt;/h2>
&lt;p>&lt;strong>La capacidad que da.&lt;/strong> El estado del cluster vive en un repositorio. Cualquier cambio pasa por commit. Cualquier persona puede reconstruir el cluster (o uno equivalente) desde el repo y los backups. La observabilidad básica avisa cuando algo se rompe.&lt;/p>
&lt;p>&lt;strong>El test del nivel.&lt;/strong> &lt;em>&amp;ldquo;Si pierdo el cluster entero, ¿puedo recrearlo en X horas desde el repo + los backups, sin intervención manual fuera del bootstrap?&amp;rdquo;&lt;/em>. Las dos horas son negociables; lo que define el nivel es que &lt;strong>el repo + los backups bastan&lt;/strong>, no que la persona-que-sabe esté disponible.&lt;/p>
&lt;p>&lt;strong>Piezas mínimas del nivel.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pieza&lt;/th>
&lt;th>Decisión sugerida en 2026&lt;/th>
&lt;th>Por qué&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Forge&lt;/td>
&lt;td>&lt;strong>Forgejo&lt;/strong> (o Gitea, GitLab CE)&lt;/td>
&lt;td>OSS auto-alojado, fork comunitario de Gitea, gobernanza abierta&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reconciliador GitOps&lt;/td>
&lt;td>&lt;strong>Flux&lt;/strong>&lt;/td>
&lt;td>CNCF graduado, multi-tenancy nativo, lightweight&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Registry de imágenes&lt;/td>
&lt;td>&lt;strong>Forgejo Container Registry&lt;/strong>&lt;/td>
&lt;td>Junto al código, sin pieza extra&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TSDB métricas&lt;/td>
&lt;td>&lt;strong>VictoriaMetrics&lt;/strong> + vmagent&lt;/td>
&lt;td>Throughput superior a Prometheus puro, retención larga, compatible PromQL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Visualización&lt;/td>
&lt;td>&lt;strong>Grafana&lt;/strong>&lt;/td>
&lt;td>Estándar de facto&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Logs&lt;/td>
&lt;td>&lt;strong>Loki&lt;/strong> o &lt;strong>Vector&lt;/strong>&lt;/td>
&lt;td>OSS, integrado con Grafana&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Alerting&lt;/td>
&lt;td>&lt;strong>Alertmanager&lt;/strong> + &lt;strong>Keep&lt;/strong> (orquestador OSS)&lt;/td>
&lt;td>Keep añade enrutamiento multi-canal sin lock-in&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Backups DB&lt;/td>
&lt;td>&lt;strong>Barman Cloud&lt;/strong> (Postgres)&lt;/td>
&lt;td>Estándar para CNPG&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Backups objeto / dataset&lt;/td>
&lt;td>&lt;strong>Ceph RGW multisite&lt;/strong> + snapshots CephFS&lt;/td>
&lt;td>Cross-pool y cross-site&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Antipatrones que te bajan a nivel 1.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>kubectl apply&lt;/code> aplicado en producción &lt;strong>fuera&lt;/strong> del repo (drift no detectado).&lt;/li>
&lt;li>Branches &lt;code>main&lt;/code> con permisos de escritura para humanos sin revisión.&lt;/li>
&lt;li>Repo monolítico sin separación tenant/infra/apps (cambios cruzados no auditables).&lt;/li>
&lt;li>Métricas que no se conservan más de 7 días (sin SLO observable a un mes vista).&lt;/li>
&lt;li>Alerting que dispara para todo (fatiga) o para nada (silencio).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Orden de despliegue dentro del nivel.&lt;/strong> Forgejo desplegado primero (es prerrequisito de todo lo demás) → Flux instalado y apuntando al repo de manifests → repositorio inicial con Helm releases de Cilium y Rook-Ceph reconciliados por Flux (sustituyendo los &lt;code>kubectl apply&lt;/code> del nivel 1) → VictoriaMetrics + Grafana + Loki vía Helm/Flux → backups Postgres y snapshots Ceph programados → smoke test (tira el cluster, restaura desde repo + backup, los servicios vuelven).&lt;/p>
&lt;h2 id="nivel-3--gestionado-identidades-certificados-secretos-y-políticas">Nivel 3 — Gestionado: identidades, certificados, secretos y políticas&lt;/h2>
&lt;p>&lt;strong>La capacidad que da.&lt;/strong> Cualquier humano que opera el cluster lo hace con identidad propia (no &lt;code>kubeconfig&lt;/code> compartido), con MFA y con permisos limitados. TLS interno automático. Secretos versionados encriptados. Políticas que &lt;strong>rechazan&lt;/strong> configuraciones inseguras antes de que entren al cluster. Auditoría completa de quién hizo qué.&lt;/p>
&lt;p>&lt;strong>El test del nivel.&lt;/strong> &lt;em>&amp;ldquo;Si un atacante consigue el portátil de un administrador, ¿qué puede hacer en producción?&amp;rdquo;&lt;/em>. En nivel 3 la respuesta es &lt;em>&amp;ldquo;poco&amp;rdquo;&lt;/em>: MFA bloquea el segundo factor, las políticas Kyverno bloquean cambios destructivos sin aprobación, las NetworkPolicies impiden lateral movement, los secretos están encriptados con KMS externo, el audit log queda. En nivel 2, &lt;em>&amp;ldquo;todo&amp;rdquo;&lt;/em>.&lt;/p>
&lt;p>&lt;strong>Piezas mínimas del nivel.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pieza&lt;/th>
&lt;th>Decisión sugerida en 2026&lt;/th>
&lt;th>Por qué&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>IdP / OIDC&lt;/td>
&lt;td>&lt;strong>Defguard&lt;/strong>&lt;/td>
&lt;td>OSS español, WireGuard + OIDC + 2FA, multi-org&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Federación con cluster&lt;/td>
&lt;td>OIDC en kube-apiserver, OIDC en Forgejo, OIDC en Grafana&lt;/td>
&lt;td>SSO consistente&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PKI interna&lt;/td>
&lt;td>&lt;strong>cert-manager&lt;/strong> + Trust Manager&lt;/td>
&lt;td>Estándar de facto, ACME y CA interna&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ACME externo&lt;/td>
&lt;td>Let&amp;rsquo;s Encrypt para certs de borde&lt;/td>
&lt;td>Sin pago, automatizado&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Secretos en git&lt;/td>
&lt;td>&lt;strong>SOPS&lt;/strong> + age o KMS externo&lt;/td>
&lt;td>Versionable, encriptado en repo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sync de secretos&lt;/td>
&lt;td>&lt;strong>External Secrets Operator&lt;/strong>&lt;/td>
&lt;td>Pull desde KMS / Vault al cluster&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Policy as code&lt;/td>
&lt;td>&lt;strong>Kyverno&lt;/strong> (o OPA Gatekeeper)&lt;/td>
&lt;td>Kyverno tiene menos curva de aprendizaje&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NetworkPolicy&lt;/td>
&lt;td>&lt;strong>Cilium NetworkPolicy&lt;/strong> + L7&lt;/td>
&lt;td>Default deny per namespace&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Runtime security&lt;/td>
&lt;td>&lt;strong>Tetragon&lt;/strong> (Cilium)&lt;/td>
&lt;td>eBPF, complementa NetworkPolicy con detección&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vulnerability scanning&lt;/td>
&lt;td>&lt;strong>Trivy&lt;/strong> en pipeline CI + admission&lt;/td>
&lt;td>SBOM por imagen, bloqueo de CVE críticas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Audit log&lt;/td>
&lt;td>kube-apiserver con &lt;code>--audit-policy-file&lt;/code> enviado a Loki&lt;/td>
&lt;td>Trazabilidad regulatoria&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Políticas Kyverno mínimas a tener vivas.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Deny de imágenes &lt;code>:latest&lt;/code> o sin sha digest.&lt;/li>
&lt;li>Deny de pods sin &lt;code>securityContext.runAsNonRoot=true&lt;/code>.&lt;/li>
&lt;li>Deny de pods sin &lt;code>resources.limits&lt;/code> (CPU + memoria).&lt;/li>
&lt;li>Deny de Services sin label &lt;code>owner=&amp;lt;equipo&amp;gt;&lt;/code>.&lt;/li>
&lt;li>Deny de cambios en namespaces críticos (&lt;code>kube-system&lt;/code>, &lt;code>flux-system&lt;/code>) sin label de aprobación.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Antipatrones que te bajan a nivel 2.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>kubeconfig&lt;/code> compartido entre administradores.&lt;/li>
&lt;li>Secretos en &lt;code>data:&lt;/code> plano del manifest commiteado al repo.&lt;/li>
&lt;li>NetworkPolicy ausente en namespaces nuevos por defecto (allow-all implícito).&lt;/li>
&lt;li>&lt;code>kubectl edit&lt;/code> o &lt;code>kubectl patch&lt;/code> en producción sin pasar por el repo.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Orden de despliegue dentro del nivel.&lt;/strong> Defguard desplegado y enrolado con WireGuard / OIDC → integración OIDC con kube-apiserver, Forgejo y Grafana → cert-manager instalado y emitiendo certificados internos (CA propia para mTLS, Let&amp;rsquo;s Encrypt para borde) → SOPS configurado y External Secrets Operator instalado → migración de secretos plano → encriptado → Kyverno con políticas iniciales y modo &lt;em>audit&lt;/em>, después &lt;em>enforce&lt;/em> → NetworkPolicy default-deny por namespace → Tetragon habilitado → smoke test (intentar saltarse cada política y comprobar que las admisiones rechazan).&lt;/p>
&lt;h2 id="nivel-4--optimizado-para-gpu-el-cluster-ya-sabe-lo-que-es-una-h100">Nivel 4 — Optimizado para GPU: el cluster ya sabe lo que es una H100&lt;/h2>
&lt;p>&lt;strong>La capacidad que da.&lt;/strong> El scheduler de Kubernetes ve las GPUs, las distingue, las puede particionar (MIG) o multiplexar (time-slicing), exponer métricas DCGM, autoescalar con KEDA usando métricas de la propia carga LLM (&lt;code>vllm:num_requests_running&lt;/code>, &lt;code>vllm:gpu_cache_usage_perc&lt;/code>), transportar trazas con semantic conventions GenAI. Todo lo necesario para que el stack de inferencia LLM se apoye en una plataforma que entiende su naturaleza.&lt;/p>
&lt;p>&lt;strong>El test del nivel.&lt;/strong> &lt;em>&amp;ldquo;Si pongo un pod que pide &lt;code>nvidia.com/gpu: 1&lt;/code>, ¿se programa en la GPU correcta, con el slice correcto, con métricas DCGM expuestas, con observabilidad GenAI lista para recibir spans?&amp;rdquo;&lt;/em>. Si sí, estás en nivel 4. Si la respuesta requiere &amp;ldquo;depende de qué nodo y quién lo despliegue&amp;rdquo;, todavía no.&lt;/p>
&lt;p>&lt;strong>Piezas mínimas del nivel.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pieza&lt;/th>
&lt;th>Decisión sugerida en 2026&lt;/th>
&lt;th>Por qué&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>GPU device plugin&lt;/td>
&lt;td>&lt;strong>NVIDIA GPU Operator&lt;/strong>&lt;/td>
&lt;td>Despliega drivers, container toolkit, DCGM y MIG manager con un operator&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Particionamiento HW&lt;/td>
&lt;td>&lt;strong>MIG&lt;/strong> (Multi-Instance GPU) en H100 cuando aplique&lt;/td>
&lt;td>Aislamiento hardware real, no time-slicing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Métricas GPU&lt;/td>
&lt;td>&lt;strong>DCGM Exporter&lt;/strong>&lt;/td>
&lt;td>SM utilization, VRAM, temperatura, throttling, NVLink bandwidth&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Métricas LLM&lt;/td>
&lt;td>&lt;strong>vLLM Prometheus&lt;/strong> endpoint + scrape&lt;/td>
&lt;td>TTFT, TPOT, KV cache, prefix hit rate&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Autoscaling&lt;/td>
&lt;td>&lt;strong>KEDA&lt;/strong> con ScaledObject Prometheus&lt;/td>
&lt;td>Escala por métricas LLM, no por CPU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Operadores LLM&lt;/td>
&lt;td>&lt;strong>vLLM Production Stack&lt;/strong> / &lt;strong>OME&lt;/strong> (Operator Model Engine)&lt;/td>
&lt;td>Manejo declarativo de modelos / adapters&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Trazas&lt;/td>
&lt;td>&lt;strong>OpenTelemetry Collector&lt;/strong> con receivers OTLP + processors + exporters&lt;/td>
&lt;td>Semantic conventions &lt;code>gen_ai.*&lt;/code> (&lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">post&lt;/a>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LeaderWorkerSet&lt;/td>
&lt;td>API LeaderWorkerSet (k8s 1.30+)&lt;/td>
&lt;td>Topología tensor parallel coherente con NVLink&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Topology Manager&lt;/td>
&lt;td>habilitado con &lt;code>single-numa-node&lt;/code>&lt;/td>
&lt;td>Pin de pods GPU a NUMA correcta&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Decisión clave: MIG, time-slicing o pasthrough.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MIG&lt;/strong> divide una H100 en 1g.10gb, 2g.20gb, 3g.40gb, 7g.80gb (slices con aislamiento HW real). Útil para servir varios modelos pequeños o reservar capacidad por tenant con garantía. Limitación: hasta 7 instancias por GPU, perfiles predefinidos.&lt;/li>
&lt;li>&lt;strong>Time-slicing&lt;/strong> comparte una GPU entre varios pods sin aislamiento HW. Útil para dev/test, no para producción multi-tenant con SLA.&lt;/li>
&lt;li>&lt;strong>Passthrough&lt;/strong> asigna la GPU entera a un pod. Útil para tensor parallel sobre múltiples GPUs del mismo nodo (LLM grande con TP=4).&lt;/li>
&lt;/ul>
&lt;p>Para una plataforma LLM productiva, la regla práctica: &lt;strong>passthrough para los modelos grandes con TP&lt;/strong>, &lt;strong>MIG para embeddings y modelos pequeños que cohabitan&lt;/strong>, &lt;strong>nunca time-slicing en producción&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Antipatrones que te bajan a nivel 3.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Instalar drivers NVIDIA a mano fuera del GPU Operator (rotura silenciosa al actualizar Kubernetes).&lt;/li>
&lt;li>Servir un LLM con &lt;code>requests.gpu: 1&lt;/code> sin haber decidido MIG / passthrough (terminas con GPUs idle por fragmentación o pods que se pisan).&lt;/li>
&lt;li>KEDA autoscalando por CPU (&lt;code>HorizontalPodAutoscaler&lt;/code> clásico) en pods que están casi siempre al 10% de CPU pero al 95% de KV cache.&lt;/li>
&lt;li>OpenTelemetry desplegado pero sin semantic conventions &lt;code>gen_ai.*&lt;/code> (las trazas no son LLM-aware).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Orden de despliegue dentro del nivel.&lt;/strong> NVIDIA GPU Operator instalado vía Helm/Flux con la versión de driver que case con el motor LLM elegido → DCGM Exporter habilitado y métricas visibles en Grafana (dashboards NVIDIA importados) → MIG manager configurado para los nodos donde tenga sentido (mezcla typical en cluster 4×H100 SXM: dos GPUs con passthrough completo para el LLM general TP=4, dos GPUs particionadas en 2×3g.40gb cada una para LLMs pequeños + embeddings) → OpenTelemetry Collector con processors &lt;code>attributes&lt;/code> para enriquecer spans con etiquetas propias (&lt;code>tenant_id&lt;/code>, &lt;code>priority_tier&lt;/code>) + exporters a Langfuse y a Tempo → KEDA instalado con ScaledObject de ejemplo apuntando a &lt;code>vllm:num_requests_running&lt;/code> → vLLM Production Stack o OME para declarar modelos como CRD → smoke test (un Deployment de vLLM declarado vía CRD arranca, sirve un token, expone métricas, la traza llega a Langfuse, KEDA escala bajo carga sintética).&lt;/p>
&lt;h2 id="nivel-5--handoff-el-cluster-es-plataforma-llm-las-siete-capas-entran-encima">Nivel 5 — Handoff: el cluster es plataforma LLM, las siete capas entran encima&lt;/h2>
&lt;p>Llegado al nivel 4, el cluster cumple el contrato que el &lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">post de las siete capas&lt;/a> asumía como punto de partida. El nivel 5 no añade infraestructura: añade el &lt;strong>stack LLM&lt;/strong> propiamente dicho. Por completitud, los siete componentes del nivel 5 son:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Gateway&lt;/strong> (Envoy AI Gateway) — entra primero, dirige tráfico a inferencia LLM y embeddings.&lt;/li>
&lt;li>&lt;strong>Inferencia LLM&lt;/strong> (vLLM Production Stack o OME con vLLM) — sobre las GPUs ya descubiertas por el GPU Operator del nivel 4.&lt;/li>
&lt;li>&lt;strong>Embeddings + reranker&lt;/strong> (Infinity, TEI) — pod separado del LLM, ya cubierto en el post anterior.&lt;/li>
&lt;li>&lt;strong>Vector store + datos relacionales&lt;/strong> (Qdrant, PostgreSQL CNPG, Ceph RGW para pesos y adapters, CephFS para datasets) — la mayoría ya existía en nivel 2 como datos; ahora se especializa para RAG.&lt;/li>
&lt;li>&lt;strong>Observabilidad LLM-aware&lt;/strong> (Langfuse) — se enchufa a la cadena OTel del nivel 4.&lt;/li>
&lt;li>&lt;strong>Control plane GitOps&lt;/strong> — el del nivel 2 sigue siendo la única autoridad legítima.&lt;/li>
&lt;li>&lt;strong>Dependency tracking&lt;/strong> (Hubble flows + Otterize) — sobre Cilium que ya existía en nivel 1.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>El criterio para promocionar de nivel 4 a nivel 5&lt;/strong> no es técnico: es contractual. El cluster ya soporta LLMs; la decisión es cuándo abrir tráfico real de clientes. La promoción exige: golden eval del modelo verde, runbook de incidentes firmado, SLOs negociados, plan de continuidad, mapeo a ENS / NIS2 / 42001 si aplica.&lt;/p>
&lt;h2 id="las-matemáticas-que-importan-cuánto-cuesta-saltarse-un-nivel">Las matemáticas que importan: cuánto cuesta saltarse un nivel&lt;/h2>
&lt;p>Para cuantificar la tesis del post, una estimación con orden de magnitud del &lt;strong>coste de subir cada nivel a tiempo&lt;/strong> versus &lt;strong>subirlo después de tener producción&lt;/strong>. Las cifras son tiempo de ingeniería con un equipo de plataforma pequeño (2-3 personas), asumiendo plantillas y experiencia previa.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Nivel&lt;/th>
&lt;th>Tiempo a montar sobre cluster vacío&lt;/th>
&lt;th>Tiempo a retrofit con producción rodando&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0 → 1&lt;/td>
&lt;td>1-2 semanas&lt;/td>
&lt;td>1-2 semanas (poco refactor downstream)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1 → 2&lt;/td>
&lt;td>2-3 semanas&lt;/td>
&lt;td>4-8 semanas (migrar todo a git)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2 → 3&lt;/td>
&lt;td>2-4 semanas&lt;/td>
&lt;td>8-16 semanas (rebuild de imágenes, migración de secretos, RBAC retroactivo)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3 → 4&lt;/td>
&lt;td>1-2 semanas&lt;/td>
&lt;td>4-8 semanas (reconfigurar GPU, mover modelos a MIG, instrumentar &lt;code>gen_ai.*&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4 → 5&lt;/td>
&lt;td>1-2 semanas&lt;/td>
&lt;td>2-4 semanas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Total 0 → 5&lt;/strong>&lt;/td>
&lt;td>&lt;strong>~10-15 semanas&lt;/strong>&lt;/td>
&lt;td>&lt;strong>~20-40 semanas si se hace en orden equivocado&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Multiplicador típico observable en la práctica: &lt;strong>2× a 3×&lt;/strong> el coste si se hace en orden equivocado. Y eso asumiendo que &lt;strong>se llega a hacer&lt;/strong> — muchos proyectos no superan el nivel 2 nunca porque &amp;ldquo;lo de la identidad&amp;rdquo; siempre puede esperar a otro sprint. Cuando llega el incidente, ya es tarde para empezar.&lt;/p>
&lt;p>Más allá del tiempo, el coste &lt;strong>operativo&lt;/strong> (incidentes nocturnos, escapes de seguridad, deuda invisible) crece exponencialmente con el desfase entre el nivel real y el nivel necesario. Un cluster en nivel 2 sirviendo LLMs productivos en clientes regulados es una bomba de relojería: técnicamente funciona, organizativamente no.&lt;/p>
&lt;h2 id="diagrama-final-la-escalera-completa-con-piezas">Diagrama final: la escalera completa con piezas&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 520" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="diagrama final de los cinco niveles con sus piezas OSS y el handoff al stack LLM">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.bg{fill:#fafafa;stroke:#bbb;rx:8}.l0{fill:#f6e2e2;stroke:#a33}.l1{fill:#f4e3cf;stroke:#a63}.l2{fill:#eef0d0;stroke:#7a3}.l3{fill:#d8eecf;stroke:#373}.l4{fill:#dfe9f5;stroke:#356}.l5{fill:#ead8f5;stroke:#634}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#222}.tiny{font:600 10px sans-serif;fill:#222}.note{font:italic 10px sans-serif;fill:#555}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#a)}&lt;/style>
&lt;defs>&lt;marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="22" text-anchor="middle" class="lbl">Cinco niveles de madurez · piezas OSS · handoff al stack LLM&lt;/text>
&lt;rect x="40" y="38" width="740" height="76" class="b l0"/>&lt;text x="60" y="56" class="tiny">NIVEL 0 · CAÓTICO · un servidor con Linux&lt;/text>
&lt;text x="60" y="74" class="sm">Debian / Ubuntu LTS · kernel ≥6.6 · containerd · drivers NVIDIA · LVM · chrony · BMC TLS+MFA&lt;/text>
&lt;text x="60" y="92" class="sm">Red: VLAN gestión, LACP, jumbo frames, BGP en switch&lt;/text>
&lt;text x="60" y="108" class="note">Test: ¿puedo reconstruir el servidor desde notas?&lt;/text>
&lt;rect x="40" y="122" width="740" height="76" class="b l1"/>&lt;text x="60" y="140" class="tiny">NIVEL 1 · REPETIBLE · cluster Kubernetes operativo&lt;/text>
&lt;text x="60" y="158" class="sm">RKE2 (CIS-hardened) · Cilium (kube-proxy replacement + BGP) · Rook-Ceph (RBD + CephFS + RGW)&lt;/text>
&lt;text x="60" y="176" class="sm">Gateway API · kubectl/Helm desde terminal · pods rodando con HA&lt;/text>
&lt;text x="60" y="192" class="note">Test: ¿perder un nodo no requiere acción humana?&lt;/text>
&lt;rect x="40" y="206" width="740" height="76" class="b l2"/>&lt;text x="60" y="224" class="tiny">NIVEL 2 · DEFINIDO · el cluster se reconstruye desde git&lt;/text>
&lt;text x="60" y="242" class="sm">Forgejo + Flux · Forgejo Container Registry · VictoriaMetrics + Grafana + Loki&lt;/text>
&lt;text x="60" y="260" class="sm">Backups Barman Cloud + Ceph snapshots/RGW multisite · Alertmanager + Keep&lt;/text>
&lt;text x="60" y="276" class="note">Test: ¿puedo recrear el cluster desde repo + backups?&lt;/text>
&lt;rect x="40" y="290" width="740" height="76" class="b l3"/>&lt;text x="60" y="308" class="tiny">NIVEL 3 · GESTIONADO · identidad, certs, secretos, políticas&lt;/text>
&lt;text x="60" y="326" class="sm">Defguard (OIDC + WireGuard) · cert-manager · SOPS + ESO · Kyverno · Trivy&lt;/text>
&lt;text x="60" y="344" class="sm">NetworkPolicy default deny · Tetragon · audit log&lt;/text>
&lt;text x="60" y="360" class="note">Test: ¿qué puede hacer un atacante con un portátil de admin?&lt;/text>
&lt;rect x="40" y="374" width="740" height="86" class="b l4"/>&lt;text x="60" y="392" class="tiny">NIVEL 4 · OPTIMIZADO PARA GPU · el scheduler entiende H100&lt;/text>
&lt;text x="60" y="410" class="sm">NVIDIA GPU Operator · DCGM Exporter · MIG manager · Topology Manager NUMA&lt;/text>
&lt;text x="60" y="428" class="sm">KEDA con métricas vLLM · OTel Collector con gen_ai.* · LeaderWorkerSet · OME&lt;/text>
&lt;text x="60" y="446" class="sm">Decisión: passthrough TP=4 para LLM grande, MIG para LLMs pequeños + embeddings&lt;/text>
&lt;text x="60" y="460" class="note" fill="#373">Test: ¿un pod con nvidia.com/gpu:1 se programa con métricas y traza listas?&lt;/text>
&lt;rect x="40" y="468" width="740" height="44" class="b l5"/>&lt;text x="60" y="486" class="tiny">NIVEL 5 · HANDOFF&lt;/text>&lt;text x="60" y="504" class="sm">Stack LLM (7 capas del post anterior) entra encima · gateway, vLLM, embeddings, Qdrant, Langfuse...&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>La escalera no es decorativa: cada nivel &lt;strong>enable&lt;/strong> el siguiente. No se puede tener observabilidad LLM-aware (nivel 4) sin OTel desplegado vía Flux (nivel 2). No se puede tener TLS interno automático (nivel 3) sin un PKI raíz que viva en algún sitio (registro y certificados gestionados desde el nivel 2). No se puede tener KEDA escalando por métricas vLLM (nivel 4) sin Prometheus / VictoriaMetrics scrapeando (nivel 2). Los niveles &lt;strong>no son una jerarquía conceptual&lt;/strong>: son una jerarquía de &lt;strong>dependencias de instalación&lt;/strong>.&lt;/p>
&lt;h2 id="decisiones-de-diseño-típicas-que-rompen-el-progreso">Decisiones de diseño típicas que rompen el progreso&lt;/h2>
&lt;p>Errores que se ven repetidamente y que tiran el cluster atrás de nivel:&lt;/p>
&lt;p>&lt;strong>1. Saltar de nivel 1 a nivel 4 directamente.&lt;/strong> &amp;ldquo;Tenemos prisa por servir el LLM, lo de identidad y GitOps lo hacemos después&amp;rdquo;. Después es siempre dos órdenes de magnitud más caro y siempre llega después del primer incidente.&lt;/p>
&lt;p>&lt;strong>2. Confundir Helm con GitOps.&lt;/strong> Tener Helm charts no es nivel 2. Es nivel 1 con plantillas. Nivel 2 exige que un reconciliador (Flux/ArgoCD) &lt;strong>aplique&lt;/strong> las charts desde un repo, &lt;strong>detecte drift&lt;/strong> y &lt;strong>avise&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>3. cert-manager sin policy de uso.&lt;/strong> Tener certificados auto-renovados pero usar TLS sólo en el ingress, sin mTLS interno entre servicios, deja la promesa de TLS coja y baja el nivel 3 a un cosplay del 3.&lt;/p>
&lt;p>&lt;strong>4. NVIDIA drivers a mano.&lt;/strong> Funciona el día uno y se rompe el día del primer upgrade de kernel. La regla: drivers &lt;strong>siempre vía GPU Operator&lt;/strong>, nunca paquetes del sistema operativo.&lt;/p>
&lt;p>&lt;strong>5. Métricas Prometheus pero retención de 7 días.&lt;/strong> Sin retención larga (≥ 90 días) no hay SLO honesto. VictoriaMetrics con 1 año de retención cuesta poco más que Prometheus con 7 días, y desbloquea cumplimiento y postmortems serios.&lt;/p>
&lt;p>&lt;strong>6. OIDC sólo para kube-apiserver.&lt;/strong> Si Forgejo, Grafana, Defguard y vLLM cada uno tiene su propio sistema de auth, no tienes SSO, tienes islas. Un nivel 3 honesto exige &lt;strong>federación&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>7. Kyverno en modo &lt;em>audit&lt;/em> permanente.&lt;/strong> Las políticas que no rechazan no son políticas, son alertas. En algún momento hay que pasar a &lt;em>enforce&lt;/em>. Mientras tanto, sigues en nivel 2 con cara de 3.&lt;/p>
&lt;p>&lt;strong>8. MIG sin decisión consciente del perfil.&lt;/strong> Configurar MIG con el perfil por defecto sin haber medido el tamaño de los modelos que van a cohabitar deja GPUs fragmentadas con slices que nadie usa. La regla: MIG sólo si has medido y has decidido los perfiles por adelantado.&lt;/p>
&lt;p>Todos comparten una raíz: &lt;strong>declarar el nivel sin pasar el test del nivel&lt;/strong>. Decir &amp;ldquo;ya hicimos GitOps&amp;rdquo; cuando todavía se aplican cosas con &lt;code>kubectl edit&lt;/code> en prod. Decir &amp;ldquo;ya hicimos identidad&amp;rdquo; cuando hay un &lt;code>kubeconfig&lt;/code> admin compartido. Decir &amp;ldquo;estamos listos para LLM&amp;rdquo; cuando no hay DCGM Exporter ni Langfuse enchufado.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise-típico-cluster-4h100-sxm">Aplicado a hardware on-premise típico: cluster 4×H100 SXM&lt;/h2>
&lt;p>Sobre el cluster genérico de referencia (4×H100 SXM 80 GB, NVLink, 640 GB RAM), un setup razonable después de pasar los cinco niveles distribuye así los componentes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">control plane (3 nodos sin GPU, hostnames cp-01..03)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── kube-apiserver, etcd, controller-manager, scheduler
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── Flux, Forgejo, cert-manager, External Secrets, Kyverno
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── Tetragon (DaemonSet también aquí)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">worker plane (≥ 3 nodos sin GPU, hostnames worker-cpu-01..03)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── Cilium agent (DaemonSet)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── Rook-Ceph OSDs + MONs + MDS (CephFS) + RGW (S3)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── VictoriaMetrics + Grafana + Loki
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── Defguard (StatefulSet)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── Langfuse + OTel Collector
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">worker plane GPU (≥ 2 nodos con 4×H100 SXM, hostnames worker-gpu-01..02)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── NVIDIA GPU Operator (driver + container toolkit)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── DCGM Exporter (DaemonSet)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── MIG manager (configurando el perfil decidido)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── vLLM (Deployment) — LLM general TP=4 ocupa 4 GPUs (passthrough)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── vLLM (Deployment) — LLM código TP=2 ocupa 2 GPUs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── Infinity (embeddings) — 2 réplicas cohabitan en 2 slices MIG
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── KEDA scaler escuchando métricas vLLM
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La regla operativa: &lt;strong>el plano de control y el plano CPU se separan del plano GPU&lt;/strong>. Un incidente en el plano GPU no debe llevarse por delante el plano de control (que es lo que recupera el cluster). Y el plano CPU concentra todo lo que mueve estado relevante (Forgejo, Rook-Ceph, Postgres CNPG, Langfuse): es el corazón a proteger.&lt;/p>
&lt;p>El hardware GPU se especializa al máximo: pods GPU &lt;strong>solamente&lt;/strong> corren en nodos GPU, y los nodos GPU &lt;strong>no corren&lt;/strong> nada CPU-bound aparte del overhead operativo (Cilium, GPU Operator, DCGM). Esto se enforza con &lt;code>nodeSelector&lt;/code> + taints/tolerations + Kyverno policy que rechaza pods sin requests GPU programándose en nodos GPU.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-posts">Lo que no hemos cubierto (próximos posts)&lt;/h2>
&lt;p>Este post recorre el camino vertical hacia arriba. Quedan piezas horizontales y otras transversales que merecen su propio artículo:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Multi-site activo/standby&lt;/strong>: cómo se federan dos clusters con Cilium Cluster Mesh y qué cambia en cada nivel cuando hay dos sites en lugar de uno.&lt;/li>
&lt;li>&lt;strong>Migración entre niveles con tráfico real&lt;/strong>: cómo se retrofitea un cluster que ya está en producción al nivel siguiente sin downtime.&lt;/li>
&lt;li>&lt;strong>La operación día a día&lt;/strong>: runbooks por nivel, qué dashboards mirar cada mañana, qué SLOs definir por componente.&lt;/li>
&lt;li>&lt;strong>El plano de coste&lt;/strong>: cuánto cuesta cada nivel en hardware, energía, horas de ingeniería, licencias OSS opcionales (soporte comercial de Rancher, Cilium Enterprise, etc.) y cuándo cada gasto se justifica.&lt;/li>
&lt;li>&lt;strong>Cumplimiento operacionalizado&lt;/strong>: cómo se mapean los niveles 3 y 4 a controles ENS Alto, NIS2 e ISO/IEC 42001 sin convertir el cluster en un ejercicio de paperwork.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">Anatomía de un stack de inferencia LLM on-premise&lt;/a> — lo que se monta &lt;strong>encima&lt;/strong> de un cluster en nivel 4. Este post es su prequel arquitectónico.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/catalogo-herramientas-oss-llmops/">El catálogo OSS para LLMOps en seis etapas&lt;/a> — fichas individuales de muchas de las piezas citadas aquí.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a> — el OTel del nivel 4 con detalle de las semantic conventions &lt;code>gen_ai.*&lt;/code>.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">Pipeline LLMOps de seis etapas&lt;/a> — el marco operacional que vive sobre el cluster nivel 5.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>RKE2 Documentation — &lt;a href="https://docs.rke2.io/">docs.rke2.io&lt;/a>&lt;/li>
&lt;li>Cilium documentation — &lt;a href="https://docs.cilium.io/">docs.cilium.io&lt;/a>&lt;/li>
&lt;li>Rook-Ceph — &lt;a href="https://rook.io/">rook.io&lt;/a>&lt;/li>
&lt;li>Flux GitOps toolkit — &lt;a href="https://fluxcd.io/">fluxcd.io&lt;/a>&lt;/li>
&lt;li>Forgejo — &lt;a href="https://forgejo.org/">forgejo.org&lt;/a>&lt;/li>
&lt;li>cert-manager — &lt;a href="https://cert-manager.io/">cert-manager.io&lt;/a>&lt;/li>
&lt;li>External Secrets Operator — &lt;a href="https://external-secrets.io/">external-secrets.io&lt;/a>&lt;/li>
&lt;li>Kyverno — &lt;a href="https://kyverno.io/">kyverno.io&lt;/a>&lt;/li>
&lt;li>NVIDIA GPU Operator — &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/">docs.nvidia.com/datacenter/cloud-native/gpu-operator&lt;/a>&lt;/li>
&lt;li>DCGM Exporter — &lt;a href="https://github.com/NVIDIA/dcgm-exporter">github.com/NVIDIA/dcgm-exporter&lt;/a>&lt;/li>
&lt;li>KEDA — &lt;a href="https://keda.sh/">keda.sh&lt;/a>&lt;/li>
&lt;li>LeaderWorkerSet API — &lt;a href="https://github.com/kubernetes-sigs/lws">github.com/kubernetes-sigs/lws&lt;/a>&lt;/li>
&lt;li>vLLM Production Stack — &lt;a href="https://docs.vllm.ai/">docs.vllm.ai/en/latest/serving/production_stack.html&lt;/a>&lt;/li>
&lt;li>OpenTelemetry Semantic Conventions for GenAI — &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">opentelemetry.io/docs/specs/semconv/gen-ai&lt;/a>&lt;/li>
&lt;li>CIS Kubernetes Benchmark&lt;/li>
&lt;li>NIST SP 800-207 — Zero Trust Architecture&lt;/li>
&lt;/ul></description></item><item><title>Anatomía de un stack de inferencia LLM on-premise: las siete capas que tienen que sostenerse las unas a las otras</title><link>https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/</link><pubDate>Sat, 30 May 2026 16:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El stack de inferencia LLM &lt;strong>on-premise no es un servidor de modelos&lt;/strong>: es un edificio de &lt;strong>siete capas&lt;/strong> que se sostienen las unas a las otras. La capa de inferencia (vLLM / SGLang) sirve tokens, pero sin la capa de embeddings dedicada el RAG no funciona; sin la capa de gateway el cliente acopla su SDK al motor concreto; sin la capa de observabilidad LLM-aware (Langfuse + OpenTelemetry GenAI) cualquier degradación de calidad pasa inadvertida; sin la capa de control plane GitOps (Flux + Forgejo) cualquier cambio manual deja deuda invisible; y sin la capa de &lt;strong>dependency tracking&lt;/strong> (Hubble + intent-based policies) decomisionar un Service rompe en silencio aplicaciones que ya nadie recuerda que lo usaban. Este post nace de un incidente concreto: un pipeline que reportaba &lt;code>status: completed&lt;/code> y &lt;code>matched_jobs: 0&lt;/code> durante días porque seguía invocando un Ollama que ya había sido escalado a cero, mientras un &lt;code>except&lt;/code> mal escrito etiquetaba la &lt;code>ConnectError&lt;/code> genérica como &amp;ldquo;ChromaDB indexing error&amp;rdquo; y el vector store —inocente— se llevaba la culpa. Cada uno de los tres síntomas era el grito de una capa que faltaba o estaba mal diseñada. El cuerpo del post recorre las siete capas con su pieza canónica OSS, las decisiones de diseño que las rompen, las matemáticas de dimensionado sobre un cluster genérico &lt;strong>4×H100 SXM (320 GB de VRAM, NVLink)&lt;/strong> y un diagrama final del stack conectado. La tesis: un stack que pasa el test del incidente no se mide por su throughput pico, se mide por &lt;strong>cuánto tarda en gritar cuando algo se desvía del diseño&lt;/strong>.&lt;/p>
&lt;h2 id="estás-aquí-las-siete-capas-vistas-desde-arriba">Estás aquí: las siete capas vistas desde arriba&lt;/h2>
&lt;p>Antes de bajar al detalle, el mapa. Las siete capas no son siete servidores: son siete responsabilidades que el stack tiene que cubrir. Una capa puede colapsar en un pod (gateway) o repartirse entre varios componentes (observabilidad = traces + métricas + flow logs). Lo que no puede es &lt;strong>faltar&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="las siete capas del stack de inferencia LLM on-premise">&lt;style>
.layer{stroke:#333;stroke-width:1.4;rx:6}
.l1{fill:#ffd9b8;stroke:#a44}
.l2{fill:#ffe6c2;stroke:#a55}
.l3{fill:#fff0d0;stroke:#a66}
.l4{fill:#dfe9f5;stroke:#356}
.l5{fill:#d8eecf;stroke:#373}
.l6{fill:#f5e3d8;stroke:#763}
.l7{fill:#ead8f5;stroke:#634}
.title{font:600 13px sans-serif;fill:#222}
.sm{font:11px sans-serif;fill:#222}
.tiny{font:600 10px sans-serif;fill:#222}
.note{font:italic 10px sans-serif;fill:#555}
.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#a)}
&lt;/style>
&lt;defs>&lt;marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="22" text-anchor="middle" class="title">Stack de inferencia LLM on-premise — siete capas, una sola responsabilidad cada una&lt;/text>
&lt;rect x="40" y="40" width="740" height="38" class="layer l1"/>
&lt;text x="60" y="64" class="sm">&lt;tspan font-weight="700">1 · Gateway&lt;/tspan> — Envoy AI Gateway · Cilium Gateway API · LiteLLM &lt;tspan class="note">OpenAI-compatible · auth JWT · routing por modelo · rate limit&lt;/tspan>&lt;/text>
&lt;rect x="40" y="86" width="365" height="50" class="layer l2"/>
&lt;text x="60" y="106" class="sm">&lt;tspan font-weight="700">2 · Inferencia LLM&lt;/tspan>&lt;/text>
&lt;text x="60" y="122" class="tiny">vLLM · SGLang · TensorRT-LLM &lt;tspan class="note" fill="#555">multi-modelo · multi-LoRA · FP8 KV&lt;/tspan>&lt;/text>
&lt;rect x="415" y="86" width="365" height="50" class="layer l3"/>
&lt;text x="435" y="106" class="sm">&lt;tspan font-weight="700">3 · Embeddings&lt;/tspan>&lt;/text>
&lt;text x="435" y="122" class="tiny">Infinity · TEI · OVMS · sentence-transformers &lt;tspan class="note" fill="#555">dim fija · separado del LLM&lt;/tspan>&lt;/text>
&lt;rect x="40" y="144" width="740" height="50" class="layer l4"/>
&lt;text x="60" y="164" class="sm">&lt;tspan font-weight="700">4 · Vector store + datos relacionales&lt;/tspan>&lt;/text>
&lt;text x="60" y="180" class="tiny">Qdrant · pgvector · MinIO (pesos · adapters · corpus) &lt;tspan class="note" fill="#555">colección versionada por dim · bucket per tenant&lt;/tspan>&lt;/text>
&lt;rect x="40" y="202" width="740" height="50" class="layer l5"/>
&lt;text x="60" y="222" class="sm">&lt;tspan font-weight="700">5 · Observabilidad LLM-aware + infraestructura + red&lt;/tspan>&lt;/text>
&lt;text x="60" y="238" class="tiny">Langfuse · OTel GenAI · VictoriaMetrics · Grafana · Hubble · DCGM &lt;tspan class="note" fill="#555">trace_id end-to-end&lt;/tspan>&lt;/text>
&lt;rect x="40" y="260" width="740" height="40" class="layer l6"/>
&lt;text x="60" y="284" class="sm">&lt;tspan font-weight="700">6 · Control plane GitOps&lt;/tspan> — Flux · Forgejo · cert-manager · External Secrets &lt;tspan class="note" fill="#555">drift detection vinculante&lt;/tspan>&lt;/text>
&lt;rect x="40" y="308" width="740" height="40" class="layer l7"/>
&lt;text x="60" y="332" class="sm">&lt;tspan font-weight="700">7 · Dependency tracking&lt;/tspan> — Hubble flow logs · Otterize · NetworkPolicy as code &lt;tspan class="note" fill="#555">quién llama a qué, observado y declarado&lt;/tspan>&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Las capas 1–4 están en el &lt;strong>camino de la request&lt;/strong>: si caen, el cliente lo nota en segundos. Las capas 5–7 están en el camino del &lt;strong>diseño y la operación&lt;/strong>: si caen, no hay error visible inmediato — y por eso son las que producen incidentes silenciosos, que es como acaba la mayoría de los incidentes serios. Este post argumenta que &lt;strong>la calidad del stack se mide en las capas 5, 6 y 7&lt;/strong>, porque las 1–4 son commodities donde todo el mundo elige aproximadamente las mismas piezas.&lt;/p>
&lt;h2 id="la-analogía-el-edificio-de-oficinas-con-servicios-compartidos">La analogía: el edificio de oficinas con servicios compartidos&lt;/h2>
&lt;p>Imagina un edificio de oficinas de doce plantas con varios inquilinos. Tiene una &lt;strong>portería&lt;/strong> (gateway: filtra quién entra), tiene &lt;strong>ascensores&lt;/strong> (inferencia LLM: mueven la carga pesada), tiene &lt;strong>escaleras y montacargas&lt;/strong> (embeddings: tráfico ligero pero constante, casi nadie repara en ellos hasta que se rompen), tiene &lt;strong>fontanería y depósitos de agua&lt;/strong> (vector store: lo que de verdad guarda el estado), tiene &lt;strong>cuadro eléctrico y sensores&lt;/strong> (observabilidad: lo que avisa cuando algo está consumiendo más de lo previsto), tiene &lt;strong>un administrador de la finca&lt;/strong> (control plane GitOps: la única autoridad legítima para mover algo), y tiene un &lt;strong>libro de inquilinos&lt;/strong> (dependency tracking: quién está conectado a qué servicio compartido).&lt;/p>
&lt;p>Cuando entra un cliente nuevo en la planta tercera y pide instalar un servidor que necesita más amperaje del previsto, el problema no es eléctrico: es de &lt;strong>administración&lt;/strong>. Si el cliente puede ir directo al cuadro y enchufar lo que quiera, el edificio sobrevive un tiempo y luego salta un magnetotérmico a las tres de la mañana. Si el cliente tiene que pasar por el administrador, el administrador consulta el libro de inquilinos (¿hay alguien más colgando de ese mismo circuito?), revisa la planificación eléctrica (¿estamos al límite?) y autoriza o redirige. El edificio se mantiene en pie no por su instalación eléctrica sino por la &lt;strong>disciplina de paso por el administrador&lt;/strong>.&lt;/p>
&lt;p>El stack de inferencia LLM funciona idéntico. Las capas físicas (1–4) son las que se ven y las que la gente del marketing pone en la slide. Las capas de gobierno (5–7) son las que distinguen una plataforma de un montón de pods con suerte.&lt;/p>
&lt;p>Ahora vamos al incidente que motiva todo el post.&lt;/p>
&lt;h2 id="el-anzuelo-el-log-que-mentía-durante-seis-días">El anzuelo: el log que mentía durante seis días&lt;/h2>
&lt;p>La aplicación se llama internamente &lt;em>jobhunter&lt;/em>. Es un pipeline cron que cada seis horas barre fuentes públicas de ofertas de empleo, filtra por geo UE, embedde los anuncios nuevos, los indexa en un vector store y los hace match contra perfiles de búsqueda. El último paso dispara notificaciones.&lt;/p>
&lt;p>Durante seis días el pipeline reportó en cada run lo mismo:&lt;/p>
&lt;pre tabindex="0">&lt;code>status: completed
total_found: 756
new: 23
matched_jobs: 0 ← cero, run tras run
&lt;/code>&lt;/pre>&lt;p>Y en los logs:&lt;/p>
&lt;pre tabindex="0">&lt;code>[INFO] httpx: POST chromadb:8000/api/v2/.../collections/&amp;lt;id&amp;gt;/delete → 200 OK
[INFO] matcher: Purged 12 expired jobs from ChromaDB
[ERROR] pipeline: ChromaDB indexing error: All connection attempts failed
[INFO] httpx: POST chromadb:8000/api/v2/.../collections/&amp;lt;id&amp;gt;/get → 200 OK
[ERROR] pipeline: Matching error: All connection attempts failed
&lt;/code>&lt;/pre>&lt;p>Es un log que invita a culpar a ChromaDB. Y de hecho el primer post-mortem que se escribió internamente apuntaba a una incompatibilidad de versiones del cliente Python contra el servidor v2. Hipótesis razonable, técnicamente plausible, completamente falsa.&lt;/p>
&lt;p>La causa real: semanas atrás se había migrado el LLM general de &lt;strong>Ollama&lt;/strong> a &lt;strong>vLLM&lt;/strong> en la plataforma. La migración fue limpia para las dos aplicaciones que dependían directamente del modelo grande — sus manifests pasaron a apuntar al nuevo endpoint. Lo que nadie hizo fue mirar quién más estaba llamando al Service &lt;code>ollama.ollama.svc:11434&lt;/code>. &lt;em>jobhunter&lt;/em> lo invocaba para generar los embeddings de las ofertas. Cuando se escaló el deployment de Ollama a cero, el Service quedó vacío, y cualquier conexión saliente recibió un &lt;code>ConnectError(&amp;quot;All connection attempts failed&amp;quot;)&lt;/code> genérico de &lt;code>httpcore&lt;/code>. El &lt;code>try/except&lt;/code> que envolvía toda la etapa de matching capturó la excepción y la rotuló como &lt;em>&amp;ldquo;ChromaDB indexing error&amp;rdquo;&lt;/em> — porque ese era el envoltorio léxico del bloque, no porque ChromaDB tuviera nada que ver. ChromaDB respondía 200 a &lt;em>delete&lt;/em> y a &lt;em>get&lt;/em> en los mismos logs.&lt;/p>
&lt;p>Tres factores hicieron que el incidente sobreviviera seis días:&lt;/p>
&lt;ol>
&lt;li>El &lt;strong>&lt;code>except&lt;/code> nombraba el envoltorio en lugar del stage que falló&lt;/strong>. El log decía &lt;em>&amp;ldquo;ChromaDB indexing error&amp;rdquo;&lt;/em> cuando el error era de embeddings contra un Service inexistente.&lt;/li>
&lt;li>El pipeline retornaba &lt;strong>&lt;code>status: completed&lt;/code>&lt;/strong> aunque hubiera errores. Ningún alerting basado en &lt;code>status&lt;/code> se disparó. La métrica que sí habría disparado (matched_jobs sostenido en cero) no estaba instrumentada.&lt;/li>
&lt;li>La imagen del pipeline rodaba como &lt;strong>&lt;code>:latest&lt;/code> sin versionar&lt;/strong>, sin SBOM, sin reproducibilidad. Cuando empezó a fallar, era imposible saber con qué cliente de Ollama estaba compilada.&lt;/li>
&lt;/ol>
&lt;p>Cada uno de los tres síntomas es el grito de una capa que faltaba. El primero pide &lt;strong>observabilidad LLM-aware&lt;/strong> que distinga stages (capa 5). El segundo pide que la &lt;strong>lógica de pipeline&lt;/strong> y las métricas Prometheus se comporten como contratos (capa 5, dimensión SLI/SLO). El tercero pide &lt;strong>GitOps con imágenes pinneadas&lt;/strong> y SBOM auditable (capa 6). Y el incidente entero —decomisionar un Service sin saber quién lo consume— grita &lt;strong>dependency tracking&lt;/strong> (capa 7).&lt;/p>
&lt;p>El resto del post recorre las siete capas con esa lente: qué resuelve cada una, qué pieza OSS la implementa en 2026, y qué decisión típica de diseño la rompe.&lt;/p>
&lt;h2 id="capa-1--gateway-el-sdk-del-cliente-no-debe-acoplarse-al-motor">Capa 1 — Gateway: el SDK del cliente no debe acoplarse al motor&lt;/h2>
&lt;p>&lt;strong>Lo que tiene que resolver.&lt;/strong> Que el cliente envíe &lt;code>POST /v1/chat/completions&lt;/code> con el SDK OpenAI estándar y no se entere de qué motor (vLLM, SGLang, TensorRT-LLM), qué modelo concreto, qué adapter LoRA o incluso qué pool de GPUs está sirviendo la request. Autenticación, rate limit, routing por &lt;code>body.model&lt;/code> y por tenant, header injection para tracing.&lt;/p>
&lt;p>&lt;strong>Pieza canónica OSS en 2026.&lt;/strong> &lt;strong>Envoy AI Gateway&lt;/strong> (Envoy con extensiones GenAI: routing por modelo, token-based rate limit, fallback chains) o &lt;strong>Cilium Gateway API&lt;/strong> con filtros propios. Para casos donde el cliente quiere multi-provider sin distinguir on-prem y SaaS, &lt;strong>LiteLLM Proxy&lt;/strong> es el equivalente ligero.&lt;/p>
&lt;p>&lt;strong>Lo que rompe la capa.&lt;/strong> Exponer directamente el endpoint del motor de inferencia. Si los clientes llaman a &lt;code>http://vllm-prod.svc:8000&lt;/code>, cualquier cambio de motor, de modelo o de pool obliga a tocar el código de todas las apps. La regla: &lt;strong>el motor cambia, el contrato no&lt;/strong>. El SDK OpenAI es estándar; el routing detrás del gateway es donde vive la libertad de diseño.&lt;/p>
&lt;p>&lt;strong>Donde fallaba &lt;em>jobhunter&lt;/em>.&lt;/strong> No había gateway. La app llamaba directamente a &lt;code>ollama.ollama.svc:11434&lt;/code>. Cuando Ollama murió, no hubo capa intermedia que pudiera responder &lt;em>fallback&lt;/em>, &lt;em>retry contra otro pool&lt;/em>, o al menos &lt;em>error 503 con cuerpo descriptivo&lt;/em>.&lt;/p>
&lt;h2 id="capa-2--inferencia-llm-vllm-como-elección-por-defecto-sglang-cuando-el-prefix-caching-manda">Capa 2 — Inferencia LLM: vLLM como elección por defecto, SGLang cuando el prefix caching manda&lt;/h2>
&lt;p>&lt;strong>Lo que tiene que resolver.&lt;/strong> Servir tokens con throughput y latencia bajo control: continuous batching, PagedAttention para que el KV cache no fragmente la VRAM, FP8 para que un modelo de 32B quepa con margen, multi-LoRA para personalización por tenant sin replicar el base, structured output para function calling con garantía de schema.&lt;/p>
&lt;p>&lt;strong>Pieza canónica OSS en 2026.&lt;/strong> &lt;strong>vLLM&lt;/strong> es la elección por defecto: cubre el estado del arte (&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">PagedAttention&lt;/a>, &lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">continuous batching&lt;/a>, &lt;a href="https://blog.lo0.es/posts/flashattention-fundamentos/">FlashAttention&lt;/a>, &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">quantization FP8&lt;/a>, &lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">multi-LoRA&lt;/a>, &lt;a href="https://blog.lo0.es/posts/structured-output-fundamentos/">structured output&lt;/a>, &lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">speculative decoding&lt;/a>). &lt;strong>SGLang&lt;/strong> entra cuando el workload tiene prefix caching alto (chat largo con system prompts grandes, agentes con instrucciones repetidas) — su RadixAttention compone mejor que el prefix caching estándar. Para inferencia muy especializada con kernels propietarios NVIDIA, &lt;strong>TensorRT-LLM&lt;/strong>, asumiendo el lock-in de hardware.&lt;/p>
&lt;p>&lt;strong>Lo que rompe la capa.&lt;/strong> Asumir que un único modelo &amp;ldquo;lo hace todo&amp;rdquo;. Un LLM de chat de 32B no sirve &lt;code>/v1/embeddings&lt;/code> — si lo intentas, vLLM responde &lt;code>BadRequestError: &amp;quot;The model does not support Embeddings API&amp;quot;&lt;/code>. Asumir que sí lo hacía fue una de las heridas concretas del incidente: la app esperaba un endpoint que el modelo no implementaba.&lt;/p>
&lt;p>&lt;strong>Decisión de diseño que cuesta más adelante.&lt;/strong> Servir el LLM con cuantización agresiva (INT4) sin un eval de calidad calibrado para tu corpus. INT4 con AWQ o GPTQ ahorra VRAM, pero degrada respuestas en castellano técnico o jurídico de forma medible. La regla: cualquier cambio de cuantización pasa por el mismo golden eval que un cambio de modelo.&lt;/p>
&lt;h2 id="capa-3--embeddings-separados-del-llm-dimensión-fija-vida-propia">Capa 3 — Embeddings: separados del LLM, dimensión fija, vida propia&lt;/h2>
&lt;p>&lt;strong>Lo que tiene que resolver.&lt;/strong> Generar vectores densos para el corpus RAG y para las queries de retrieval. El modelo de embeddings es &lt;strong>otra cosa&lt;/strong> que el LLM de chat: arquitectura distinta (encoder, no decoder), tamaño distinto (cientos de millones de parámetros, no decenas de miles), API distinta (un endpoint &lt;code>/embeddings&lt;/code> que recibe texto y devuelve un vector de dimensión fija).&lt;/p>
&lt;p>&lt;strong>Pieza canónica OSS en 2026.&lt;/strong> &lt;strong>Infinity&lt;/strong> o &lt;strong>Hugging Face Text Embeddings Inference (TEI)&lt;/strong> para servir modelos de la familia &lt;code>bge-*&lt;/code>, &lt;code>multilingual-e5-*&lt;/code>, &lt;code>nomic-embed-*&lt;/code> con throughput alto y soporte multi-modelo. &lt;strong>OpenVINO Model Server&lt;/strong> cuando hay hardware Intel disponible. &lt;strong>sentence-transformers&lt;/strong> como fallback embebido en la propia aplicación cuando el corpus es pequeño y el deployment es restringido.&lt;/p>
&lt;p>&lt;strong>Lo que rompe la capa.&lt;/strong> Tratarla como &amp;ldquo;el LLM también lo hace&amp;rdquo;. No lo hace si es chat-only; y aunque lo haga (algunos modelos tienen endpoint dual), mezclar serving de chat y de embeddings en el mismo proceso castiga el throughput de ambos. La separación física &lt;strong>es&lt;/strong> el diseño.&lt;/p>
&lt;p>&lt;strong>Dato técnico que se olvida.&lt;/strong> El &lt;strong>vector store y el modelo de embeddings forman una unidad indivisible&lt;/strong>. Cambiar el modelo de &lt;code>multilingual-e5-large&lt;/code> (1024 dim) a &lt;code>multilingual-e5-small&lt;/code> (384 dim) &lt;strong>no es una sustitución&lt;/strong>: es crear una colección nueva (&lt;code>mi_corpus_v2&lt;/code>) y reembebir todo el corpus. Si haces &lt;code>upsert&lt;/code> sobre la colección antigua, te encuentras con un dim mismatch en runtime que tira el pod. Esto que parece obvio se viola constantemente porque el modelo de embeddings se elige una vez y se olvida.&lt;/p>
&lt;h2 id="capa-4--vector-store--datos-relacionales--storage-lo-que-de-verdad-guarda-el-estado">Capa 4 — Vector store + datos relacionales + storage: lo que de verdad guarda el estado&lt;/h2>
&lt;p>&lt;strong>Lo que tiene que resolver.&lt;/strong> Persistir los vectores con filtros eficientes (&lt;code>tenant_id&lt;/code>, &lt;code>created_at&lt;/code>, &lt;code>source&lt;/code>), persistir los metadatos relacionales (usuarios, configs, prompts versionados, traces), y persistir los pesos del modelo, los adapters LoRA, los datasets y los corpus originales.&lt;/p>
&lt;p>&lt;strong>Piezas canónicas OSS en 2026.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Qdrant&lt;/strong> para colecciones grandes (&amp;gt;200 k vectores), filtros payload-aware, multi-tenant via colección o via campo.&lt;/li>
&lt;li>&lt;strong>pgvector&lt;/strong> para colecciones pequeñas con joins relacionales obligatorios (querer hacer &lt;code>WHERE doc.author = ... AND vector &amp;lt;=&amp;gt; $1&lt;/code> en la misma SQL).&lt;/li>
&lt;li>&lt;strong>PostgreSQL operado por CloudNativePG (CNPG)&lt;/strong> para los relacionales: backups Barman Cloud, replicación, conexión vía pooler.&lt;/li>
&lt;li>&lt;strong>MinIO&lt;/strong> para objeto S3-compatible: bucket por tenant, replicación cross-site, pesos y adapters versionados por sha256.&lt;/li>
&lt;li>&lt;strong>Redis&lt;/strong> para queues, rate-limit counters y cache de retrievals frecuentes.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Lo que rompe la capa.&lt;/strong> Asumir que el vector store es &lt;em>stateless ephemeral&lt;/em>. Es justo lo contrario: es el componente donde más caro sale perder estado. Sin backups verificados del vector store, una corrupción de índice obliga a reembebir el corpus entero — y eso, en un corpus de millones de documentos, son horas o días de GPU.&lt;/p>
&lt;p>&lt;strong>Decisión de diseño que paga después.&lt;/strong> Olvidar versionar la colección por &lt;strong>dimensión&lt;/strong> y &lt;strong>modelo de embeddings&lt;/strong>. Convención sugerida: &lt;code>mi_corpus__embed-multie5l__1024d__v3&lt;/code>. El nombre lleva metadata; cualquier cambio en cualquiera de los tres atributos fuerza colección nueva. Es feo pero protege contra el &lt;code>upsert&lt;/code> accidental con dim incorrecta.&lt;/p>
&lt;h2 id="capa-5--observabilidad-traces-llm-aware--métricas-infra--flow-logs">Capa 5 — Observabilidad: traces LLM-aware + métricas infra + flow logs&lt;/h2>
&lt;p>&lt;strong>Lo que tiene que resolver.&lt;/strong> Que cualquier inferencia se pueda recuperar a partir de su &lt;code>trace_id&lt;/code>, con todos los atributos &lt;code>gen_ai.*&lt;/code> (&lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">semantic conventions&lt;/a>) + atributos propios (&lt;code>tenant_id&lt;/code>, &lt;code>adapter_id&lt;/code>, &lt;code>priority_tier&lt;/code>), latencia desglosada (queue → prefill → decode → red), tokens consumidos, tools invocados, modelo y adapter exactos. Y en paralelo: métricas Prometheus de vLLM (&lt;code>vllm:num_requests_running&lt;/code>, &lt;code>vllm:gpu_cache_usage_perc&lt;/code>, &lt;code>vllm:prefix_cache_hit_rate&lt;/code>), de GPU (DCGM Exporter), de red (Hubble flow logs con drops y NetworkPolicy enforcement).&lt;/p>
&lt;p>&lt;strong>Piezas canónicas OSS en 2026.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>OpenTelemetry Collector&lt;/strong> como transporte único de traces, métricas y logs, con receivers OTLP y exporters separados por destino.&lt;/li>
&lt;li>&lt;strong>Langfuse&lt;/strong> self-hosted para el lado LLM-aware: tracing, prompt versioning, evals con LLM-as-judge (&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">post&lt;/a> y &lt;a href="https://blog.lo0.es/posts/llm-as-judge-fundamentos/">post&lt;/a>).&lt;/li>
&lt;li>&lt;strong>VictoriaMetrics + Grafana&lt;/strong> para métricas TSDB de alto throughput y retención larga.&lt;/li>
&lt;li>&lt;strong>Hubble&lt;/strong> (Cilium) para flow logs L3/L4/L7 y visualización de NetworkPolicy.&lt;/li>
&lt;li>&lt;strong>DCGM Exporter&lt;/strong> para métricas GPU.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Lo que rompe la capa.&lt;/strong> Empaquetar todo el stage del pipeline en un único &lt;code>try/except&lt;/code> que rotule la excepción con el nombre del envoltorio. La regla operativa: &lt;strong>un &lt;code>try/except&lt;/code> por stage&lt;/strong>, con el rótulo del stage en el mensaje y una métrica Prometheus con &lt;code>labels={&amp;quot;stage&amp;quot;: &amp;quot;&amp;lt;name&amp;gt;&amp;quot;}&lt;/code>. Así &lt;em>&amp;ldquo;ChromaDB indexing error&amp;rdquo;&lt;/em> nunca habría sido el log para un fallo de embeddings; habría sido &lt;em>&amp;ldquo;Embeddings call failed: ConnectError(ollama:11434)&amp;rdquo;&lt;/em>.&lt;/p>
&lt;p>&lt;strong>Regla complementaria.&lt;/strong> El pipeline devuelve &lt;code>status: completed&lt;/code> si y solo si &lt;strong>no hubo errores&lt;/strong>. Con errores devuelve &lt;code>completed_with_errors&lt;/code> o &lt;code>failed&lt;/code>, y la métrica &lt;code>pipeline_errors_total{stage}&lt;/code> se incrementa. Un alert basado en &lt;code>increase(pipeline_errors_total[1h]) &amp;gt; 0&lt;/code> se dispara antes del segundo run fallido. Sin esta disciplina, la observabilidad existe pero no avisa.&lt;/p>
&lt;h2 id="capa-6--control-plane-gitops-la-única-autoridad-legítima">Capa 6 — Control plane GitOps: la única autoridad legítima&lt;/h2>
&lt;p>&lt;strong>Lo que tiene que resolver.&lt;/strong> Que el estado del cluster sea el estado declarado en git. Que cualquier divergencia entre git y el cluster sea visible y, en componentes críticos, &lt;strong>auto-reconciliada o auto-alertada&lt;/strong>. Que cada imagen desplegada tenga un tag inmutable (sha digest o semver pin), un SBOM (Trivy) y trazabilidad hasta el commit que la introdujo.&lt;/p>
&lt;p>&lt;strong>Piezas canónicas OSS en 2026.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Flux&lt;/strong> (o ArgoCD) como reconciliador.&lt;/li>
&lt;li>&lt;strong>Forgejo&lt;/strong> (o Gitea, GitLab CE) como forge OSS auto-alojado.&lt;/li>
&lt;li>&lt;strong>cert-manager + Trust Manager&lt;/strong> para PKI interna.&lt;/li>
&lt;li>&lt;strong>External Secrets Operator + SOPS&lt;/strong> para secretos versionados encriptados.&lt;/li>
&lt;li>&lt;strong>Kyverno&lt;/strong> (o OPA Gatekeeper) para policies vinculantes: deny de imágenes &lt;code>:latest&lt;/code>, deny de pods sin NetworkPolicy, deny de Services sin owner label.&lt;/li>
&lt;li>&lt;strong>Trivy&lt;/strong> para SBOM y vulnerability scanning de imágenes en el pipeline CI.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Lo que rompe la capa.&lt;/strong> Imágenes con tag mutable (&lt;code>:latest&lt;/code>, &lt;code>:main&lt;/code>). Cualquier &lt;code>kubectl edit&lt;/code> en producción que no se refleja en git. Branches &lt;code>main&lt;/code> con permisos de escritura para humanos sin revisión. La regla: &lt;strong>si un humano puede mutar el cluster sin pasar por un commit firmado, no tienes GitOps, tienes una pizarra de Pepe&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Cómo se aplica al incidente.&lt;/strong> Si la imagen del pipeline hubiera estado pinneada a un sha digest (&lt;code>registry.interno.local/jobhunter@sha256:9af2...&lt;/code>), el equipo habría podido auditar inmediatamente qué cliente de Ollama llevaba. Con &lt;code>:latest&lt;/code>, ni eso.&lt;/p>
&lt;h2 id="capa-7--dependency-tracking-la-capa-que-el-incidente-puso-de-manifiesto">Capa 7 — Dependency tracking: la capa que el incidente puso de manifiesto&lt;/h2>
&lt;p>&lt;strong>Lo que tiene que resolver.&lt;/strong> Saber quién llama a qué Service, tanto en &lt;strong>declarativo&lt;/strong> (qué dice el repo gitops) como en &lt;strong>observado&lt;/strong> (qué se ha visto pasar por la red en los últimos N días). Y, en plataformas maduras, propagar esa información como &lt;strong>policy&lt;/strong>: si nadie declara ni nadie observa tráfico al Service &lt;code>ollama.ollama.svc&lt;/code>, decomisionarlo es seguro; si alguien lo declara o lo usa, decomisionarlo abre un ticket.&lt;/p>
&lt;p>&lt;strong>Piezas canónicas OSS en 2026.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Hubble&lt;/strong> (Cilium) para los flow logs observados: &lt;code>hubble observe --to-namespace ollama --since 14d&lt;/code> da la lista de namespaces de origen que han hablado con Ollama en las últimas dos semanas.&lt;/li>
&lt;li>&lt;strong>Otterize&lt;/strong> para intent-based policy: cada Deployment declara &lt;em>&amp;ldquo;yo necesito hablar con &lt;code>ollama-svc&lt;/code>&amp;rdquo;&lt;/em>, y el operator genera la NetworkPolicy correspondiente y mantiene un catálogo navegable de quién intenta hablar con qué.&lt;/li>
&lt;li>&lt;strong>kubectl-grep manual&lt;/strong> como fallback: &lt;code>kubectl get deployments,cronjobs,statefulsets -A -o yaml | grep -E 'ollama[.-]'&lt;/code> saca la lista declarativa.&lt;/li>
&lt;li>&lt;strong>NetworkPolicy as code&lt;/strong> revisada en CI: cada PR que toca un Service requiere que la política asociada se mantenga o se actualice explícitamente.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Pre-decom checklist&lt;/strong> que el incidente sugiere codificar como hook en CI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">SVC&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;ollama.ollama.svc.cluster.local&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (a) grep declarativo en el repo gitops&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git -C &lt;span class="nv">$GITOPS_REPO&lt;/span> grep -l &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SVC&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;OK declarativo&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (b) grep observado en Hubble (últimos 14 días)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">hubble observe --to-fqdn &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SVC&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --since 336h --output json &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq -r &lt;span class="s1">&amp;#39;.source.namespace&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> sort -u &lt;span class="o">||&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;OK observado&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (c) grep live en el cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get all -A -o yaml &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SVC&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;OK live&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Si los tres devuelven vacío, el decom es seguro. Si cualquiera tiene contenido, hay deuda downstream sin cerrar. El incidente de &lt;em>jobhunter&lt;/em> es exactamente lo que pasa cuando este check no existe: el equipo que decomisionó Ollama miró la lista de aplicaciones que sabía que dependían directamente; nadie miró la lista de las que dependían en silencio.&lt;/p>
&lt;h2 id="las-matemáticas-que-importan-dimensionar-el-stack-sobre-4h100-sxm-320-gb">Las matemáticas que importan: dimensionar el stack sobre 4×H100 SXM (320 GB)&lt;/h2>
&lt;p>Cluster genérico de referencia para todo lo que sigue: &lt;strong>4×H100 SXM 80 GB&lt;/strong>, &lt;strong>NVLink&lt;/strong> entre las cuatro, &lt;strong>640 GB de RAM&lt;/strong> de sistema, &lt;strong>2×NVMe NVMe-oF&lt;/strong> para storage local de pesos y caches, &lt;strong>redundancia 25/100 GbE&lt;/strong> hacia el switch. Total VRAM agregada: &lt;strong>320 GB&lt;/strong>.&lt;/p>
&lt;p>El presupuesto VRAM no es libre. Una primera regla de reparto razonable para un stack que sirve un LLM general grande, un modelo especializado en código mediano, embeddings y reranker, con margen para multi-LoRA y para el KV cache:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Componente&lt;/th>
&lt;th>Modelo de referencia&lt;/th>
&lt;th>Quant&lt;/th>
&lt;th>Peso del modelo&lt;/th>
&lt;th>KV cache reservado&lt;/th>
&lt;th>VRAM total&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>LLM general (TP=4)&lt;/td>
&lt;td>70B-instruct&lt;/td>
&lt;td>FP8 W8A8&lt;/td>
&lt;td>70 GB&lt;/td>
&lt;td>60 GB&lt;/td>
&lt;td>&lt;strong>130 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LLM código (TP=2)&lt;/td>
&lt;td>32B-coder&lt;/td>
&lt;td>FP8 W8A8&lt;/td>
&lt;td>32 GB&lt;/td>
&lt;td>28 GB&lt;/td>
&lt;td>&lt;strong>60 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Embeddings&lt;/td>
&lt;td>multilingual-e5-large&lt;/td>
&lt;td>FP16&lt;/td>
&lt;td>1.3 GB&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>&lt;strong>8 GB&lt;/strong> (×2 réplicas)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reranker&lt;/td>
&lt;td>bge-reranker-v2-m3&lt;/td>
&lt;td>FP16&lt;/td>
&lt;td>0.6 GB&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>&lt;strong>4 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-LoRA pool (sobre LLM general)&lt;/td>
&lt;td>hasta 16 adapters&lt;/td>
&lt;td>bf16&lt;/td>
&lt;td>16 × 0.4 GB ≈ 6 GB&lt;/td>
&lt;td>reusa KV del LLM&lt;/td>
&lt;td>&lt;strong>6 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reservado para fragmentación + overhead&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;strong>~30 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Total comprometido&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;strong>~238 GB / 320 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Margen libre&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;strong>~82 GB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El margen libre del 26% no es desperdicio: es lo que permite que el scheduler de vLLM no preempte requests bajo presión moderada, que el continuous batching pueda agrupar lotes grandes sin abortar, y que un fallover desde el otro site pueda promocionar un standby sin OOM.&lt;/p>
&lt;p>&lt;strong>Throughput esperado&lt;/strong>, con el LLM general de 70B en FP8 y tensor parallel 4, en una H100 SXM con continuous batching activo y prefix caching del 35–55% (típico en chat multi-turno con system prompts compartidos):&lt;/p>
&lt;p>$$
\text{tokens/segundo agregado} \approx 1500 \text{ a } 2500
$$&lt;/p>
&lt;p>para concurrencia entre 32 y 64 requests, con TTFT P95 sub-segundo en prompts cortos (&amp;lt;2k tokens) y TPOT P95 alrededor de &lt;strong>40–60 ms/token&lt;/strong> percibido por el cliente. Estos números son &lt;strong>órdenes de magnitud razonables&lt;/strong>, no garantías: el throughput real depende del mix de prompts, de si el speculative decoding (EAGLE-3) está activo y burnt-in, y del coste de la red entre gateway y pods de inferencia.&lt;/p>
&lt;p>&lt;strong>Throughput de embeddings&lt;/strong> sobre dos réplicas de &lt;code>multilingual-e5-large&lt;/code> con batch dinámico:&lt;/p>
&lt;p>$$
\text{embeddings/segundo} \approx 3000 \text{ a } 6000 \quad (\text{batch óptimo} \sim 64)
$$&lt;/p>
&lt;p>Suficiente para reindexar un corpus de &lt;strong>1 millón de documentos&lt;/strong> en una hora aproximada, asumiendo chunks de 512 tokens y dos chunks por documento de media. Para corpus de decenas de millones de documentos, el reembebido se hace por delta vía CDC sobre la fuente (cubierto en &lt;a href="https://blog.lo0.es/posts/rag-corpus-curation-fundamentos/">RAG corpus curation&lt;/a>), no por barrido.&lt;/p>
&lt;p>&lt;strong>Latencia de retrieval&lt;/strong> sobre Qdrant con HNSW (M=16, ef_construct=200) en una colección de 5 millones de vectores 1024-dim filtrada por &lt;code>tenant_id&lt;/code>:&lt;/p>
&lt;p>$$
\text{P95 latencia retrieve top-50} \approx 8 \text{ a } 25 \text{ ms}
$$&lt;/p>
&lt;p>Por debajo del coste del reranking cross-encoder (&lt;code>bge-reranker-v2-m3&lt;/code> sobre top-50 = ~30–60 ms más), y por debajo de cualquier llamada al LLM. El cuello de botella en un pipeline RAG bien dimensionado nunca es el vector store: es la decodificación del LLM.&lt;/p>
&lt;h2 id="diagrama-final-el-stack-completo-conectado">Diagrama final: el stack completo conectado&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="diagrama completo del stack de inferencia LLM on-premise con sus siete capas conectadas">&lt;style>
.b{stroke:#333;stroke-width:1.4;rx:6}
.gw{fill:#ffd9b8;stroke:#a44}
.llm{fill:#ffe6c2;stroke:#a55}
.emb{fill:#fff0d0;stroke:#a66}
.data{fill:#dfe9f5;stroke:#356}
.obs{fill:#d8eecf;stroke:#373}
.ctrl{fill:#f5e3d8;stroke:#763}
.dep{fill:#ead8f5;stroke:#634}
.bg{fill:#fafafa;stroke:#bbb;rx:8}
.lbl{font:600 12px sans-serif;fill:#222}
.sm{font:11px sans-serif;fill:#222}
.tiny{font:600 10px sans-serif;fill:#222}
.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#a)}
.otel{stroke:#1a73e8;stroke-width:1.4;fill:none;stroke-dasharray:3 2;marker-end:url(#ab)}
.ctrlarr{stroke:#c66;stroke-width:1.2;fill:none;stroke-dasharray:5 3;marker-end:url(#ac)}
&lt;/style>
&lt;defs>
&lt;marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>
&lt;marker id="ab" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#1a73e8"/>&lt;/marker>
&lt;marker id="ac" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#c66"/>&lt;/marker>
&lt;/defs>
&lt;text x="410" y="22" text-anchor="middle" class="lbl">Stack de inferencia LLM on-premise — siete capas, contratos estables entre ellas&lt;/text>
&lt;!-- Cliente -->
&lt;rect x="340" y="36" width="140" height="32" class="b gw"/>
&lt;text x="410" y="56" text-anchor="middle" class="sm">Cliente · SDK OpenAI&lt;/text>
&lt;path class="arr" d="M410,68 L410,82"/>
&lt;!-- Gateway -->
&lt;rect x="80" y="86" width="660" height="40" class="b gw"/>
&lt;text x="100" y="103" class="tiny">CAPA 1 · GATEWAY&lt;/text>
&lt;text x="410" y="120" text-anchor="middle" class="sm">Envoy AI Gateway · routing por body.model · JWT (Defguard) · rate-limit por tenant · trace_id injection&lt;/text>
&lt;!-- Inferencia + Embeddings + Reranker -->
&lt;rect x="80" y="146" width="320" height="100" class="b llm"/>
&lt;text x="100" y="163" class="tiny">CAPA 2 · INFERENCIA LLM&lt;/text>
&lt;rect x="100" y="174" width="280" height="28" class="b" fill="#fff4e0"/>
&lt;text x="240" y="192" text-anchor="middle" class="sm">vLLM · LLM general 70B FP8 · TP=4 · multi-LoRA&lt;/text>
&lt;rect x="100" y="208" width="280" height="28" class="b" fill="#fff4e0"/>
&lt;text x="240" y="226" text-anchor="middle" class="sm">vLLM · LLM código 32B FP8 · TP=2 · structured out.&lt;/text>
&lt;rect x="420" y="146" width="320" height="100" class="b emb"/>
&lt;text x="440" y="163" class="tiny">CAPA 3 · EMBEDDINGS + RERANKER&lt;/text>
&lt;rect x="440" y="174" width="280" height="28" class="b" fill="#fffae8"/>
&lt;text x="580" y="192" text-anchor="middle" class="sm">Infinity · multilingual-e5-large 1024d · ×2&lt;/text>
&lt;rect x="440" y="208" width="280" height="28" class="b" fill="#fffae8"/>
&lt;text x="580" y="226" text-anchor="middle" class="sm">TEI · bge-reranker-v2-m3 (top-50 rerank)&lt;/text>
&lt;path class="arr" d="M250,126 L250,146"/>
&lt;path class="arr" d="M570,126 L570,146"/>
&lt;!-- Vector store + storage -->
&lt;rect x="80" y="266" width="660" height="80" class="b data"/>
&lt;text x="100" y="283" class="tiny">CAPA 4 · VECTOR STORE + DATOS RELACIONALES + STORAGE&lt;/text>
&lt;rect x="100" y="294" width="160" height="40" class="b" fill="#eef3fb"/>
&lt;text x="180" y="319" text-anchor="middle" class="sm">Qdrant (HNSW)&lt;/text>
&lt;rect x="280" y="294" width="160" height="40" class="b" fill="#eef3fb"/>
&lt;text x="360" y="319" text-anchor="middle" class="sm">PostgreSQL (CNPG)&lt;/text>
&lt;rect x="460" y="294" width="160" height="40" class="b" fill="#eef3fb"/>
&lt;text x="540" y="319" text-anchor="middle" class="sm">MinIO (pesos · adapters)&lt;/text>
&lt;rect x="640" y="294" width="100" height="40" class="b" fill="#eef3fb"/>
&lt;text x="690" y="319" text-anchor="middle" class="sm">Redis&lt;/text>
&lt;path class="arr" d="M580,246 L580,266"/>
&lt;!-- Observabilidad -->
&lt;rect x="80" y="366" width="660" height="60" class="b obs"/>
&lt;text x="100" y="383" class="tiny">CAPA 5 · OBSERVABILIDAD (traces LLM-aware · métricas · flow logs)&lt;/text>
&lt;rect x="100" y="394" width="150" height="24" class="b" fill="#eaf5e2"/>
&lt;text x="175" y="411" text-anchor="middle" class="sm">Langfuse&lt;/text>
&lt;rect x="260" y="394" width="150" height="24" class="b" fill="#eaf5e2"/>
&lt;text x="335" y="411" text-anchor="middle" class="sm">OTel Collector&lt;/text>
&lt;rect x="420" y="394" width="150" height="24" class="b" fill="#eaf5e2"/>
&lt;text x="495" y="411" text-anchor="middle" class="sm">VictoriaMetrics + Grafana&lt;/text>
&lt;rect x="580" y="394" width="160" height="24" class="b" fill="#eaf5e2"/>
&lt;text x="660" y="411" text-anchor="middle" class="sm">Hubble + DCGM Exporter&lt;/text>
&lt;!-- Líneas OTel desde capas a observabilidad -->
&lt;path class="otel" d="M740,106 L770,106 L770,396 L740,396"/>
&lt;path class="otel" d="M400,196 L770,196" opacity="0.6"/>
&lt;path class="otel" d="M400,310 L770,310" opacity="0.6"/>
&lt;!-- Control plane GitOps -->
&lt;rect x="80" y="446" width="380" height="40" class="b ctrl"/>
&lt;text x="100" y="463" class="tiny">CAPA 6 · CONTROL PLANE GITOPS&lt;/text>
&lt;text x="270" y="479" text-anchor="middle" class="sm">Forgejo → Flux → cert-manager · External Secrets · Kyverno&lt;/text>
&lt;!-- Dependency tracking -->
&lt;rect x="480" y="446" width="260" height="40" class="b dep"/>
&lt;text x="500" y="463" class="tiny">CAPA 7 · DEPENDENCY TRACKING&lt;/text>
&lt;text x="610" y="479" text-anchor="middle" class="sm">Hubble flows · Otterize intents&lt;/text>
&lt;!-- Reconciliación control plane → todas las capas -->
&lt;path class="ctrlarr" d="M270,446 L270,430 L40,430 L40,106 L80,106"/>
&lt;path class="ctrlarr" d="M610,446 L610,430 L780,430 L780,106 L740,106"/>
&lt;text x="60" y="505" class="tiny">Flujo de request&lt;/text>
&lt;line x1="155" y1="502" x2="180" y2="502" stroke="#666" stroke-width="1.4"/>
&lt;text x="240" y="505" class="tiny">Telemetría OTel&lt;/text>
&lt;line x1="335" y1="502" x2="360" y2="502" stroke="#1a73e8" stroke-width="1.4" stroke-dasharray="3 2"/>
&lt;text x="430" y="505" class="tiny">Reconciliación GitOps&lt;/text>
&lt;line x1="555" y1="502" x2="580" y2="502" stroke="#c66" stroke-width="1.2" stroke-dasharray="5 3"/>
&lt;/svg>
&lt;/div>
&lt;p>Las líneas continuas son el camino de la request: cliente → gateway → motor de inferencia → vector store/embeddings → respuesta. Las líneas azules discontinuas son la telemetría: cada componente emite OTel al collector, que enruta traces a Langfuse, métricas a VictoriaMetrics y logs a Loki. Las líneas rojas discontinuas son la reconciliación: el control plane GitOps mantiene cualquier capa en su estado declarado y avisa de divergencia.&lt;/p>
&lt;p>El diagrama no es decorativo: cada flecha es un &lt;strong>contrato estable&lt;/strong> entre dos capas. Si una capa cambia (vLLM → SGLang, multilingual-e5 → bge-m3, Qdrant → pgvector), las flechas se mantienen. Esa estabilidad de contratos es la propiedad arquitectónica que hace que un equipo pueda migrar componentes sin romper apps downstream.&lt;/p>
&lt;h2 id="decisiones-de-diseño-típicas-que-rompen-el-stack">Decisiones de diseño típicas que rompen el stack&lt;/h2>
&lt;p>Lista corta de errores que se ven repetidamente en stacks que parecían bien diseñados sobre el papel:&lt;/p>
&lt;p>&lt;strong>1. Acoplar el SDK del cliente al motor de inferencia.&lt;/strong> Quitar el gateway porque &amp;ldquo;vLLM ya habla OpenAI-compatible&amp;rdquo; funciona el día uno y duele el día que hay que poner un fallback, un canary o un segundo modelo.&lt;/p>
&lt;p>&lt;strong>2. Compartir el endpoint LLM y embeddings.&lt;/strong> Un &lt;code>qwen2.5-32b-Instruct&lt;/code> es chat-only; &lt;code>BadRequestError: &amp;quot;The model does not support Embeddings API&amp;quot;&lt;/code> es el grito del diseño que confundió las dos capas.&lt;/p>
&lt;p>&lt;strong>3. Reusar la colección del vector store al cambiar el modelo de embeddings.&lt;/strong> Dimensiones distintas no admiten &lt;code>upsert&lt;/code>. Versionar la colección por &lt;code>(modelo, dim, version)&lt;/code> es feo pero salva el día del cambio.&lt;/p>
&lt;p>&lt;strong>4. &lt;code>try/except&lt;/code> que envuelve un pipeline entero con un rótulo del envoltorio.&lt;/strong> El log miente porque el rótulo es léxico, no causal. Cada stage en su &lt;code>try/except&lt;/code> con su rótulo y su métrica.&lt;/p>
&lt;p>&lt;strong>5. &lt;code>status: completed&lt;/code> con errores.&lt;/strong> El pipeline tiene que distinguir &lt;code>completed&lt;/code>, &lt;code>completed_with_errors&lt;/code> y &lt;code>failed&lt;/code>, y el alerting tiene que disparar en los dos últimos. Sin esto, la observabilidad existe en teoría y no avisa en la práctica.&lt;/p>
&lt;p>&lt;strong>6. Imágenes con tag mutable.&lt;/strong> &lt;code>:latest&lt;/code> y &lt;code>:main&lt;/code> no son tags, son alias. Sin sha digest, no hay reproducibilidad ni SBOM auditable.&lt;/p>
&lt;p>&lt;strong>7. Decomisionar un Service sin pre-decom check.&lt;/strong> El check de tres greps (declarativo + observado + live) tarda dos minutos y cuesta seis días de incidente cuando se salta.&lt;/p>
&lt;p>&lt;strong>8. Limits.memory por defecto en pods que cargan modelos.&lt;/strong> Un sidecar que carga &lt;code>sentence-transformers + torch + tokenizer&lt;/code> necesita 2–4 GB; con &lt;code>limits.memory: 1Gi&lt;/code> te encuentras con OOM en el primer pod restart, y a veces sin alert si el liveness probe responde por otra ruta.&lt;/p>
&lt;p>Todas son variantes del mismo principio: el stack no falla en su capa más cara (la inferencia, donde nadie subestima el coste), falla en las capas baratas y aburridas (gateway, observabilidad, GitOps, dependency tracking) donde es tentador ahorrar.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise-típico-cluster-4h100-sxm">Aplicado a hardware on-premise típico: cluster 4×H100 SXM&lt;/h2>
&lt;p>Sobre el cluster genérico de referencia (4×H100 SXM 80 GB, NVLink, 640 GB RAM), el reparto en pods sugerido:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">nodo-gpu-01 (4×H100 SXM, NVLink intra-nodo)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── vllm-llm-general (TP=4) ~130 GB VRAM (4 GPUs)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── (comparte GPUs con multi-LoRA pool sobre el mismo deployment)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">nodo-gpu-02 (4×H100 SXM, segundo nodo)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── vllm-llm-codigo (TP=2) ~60 GB VRAM (2 GPUs)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── infinity-embeddings (×2) ~16 GB VRAM (compartido en 1 GPU con MIG opcional)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── tei-reranker ~4 GB VRAM (cohabitante)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── reserva fallover ~120 GB VRAM libre para canary / standby
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>En un único nodo no cabe todo cómodamente; dos nodos con dos H100 SXM cada uno bastarían para el setup conservador, y el resto del cluster (CPU-bound: gateway, vector store, observabilidad, control plane) corre en nodos sin GPU.&lt;/p>
&lt;p>La regla operativa: &lt;strong>la inferencia se concentra&lt;/strong>, el resto del stack se distribuye. Concentrar la inferencia maximiza el aprovechamiento de NVLink (tensor parallel cross-GPU sin pasar por PCIe); distribuir el resto evita que un evento en el nodo GPU se lleve por delante el control plane.&lt;/p>
&lt;p>Una configuración aún más conservadora —para PYMEs con un solo nodo 4×H100 SXM como punto de partida— sirve LLM general (TP=4) y embeddings/reranker en cohabitación con MIG (Multi-Instance GPU para particionar una H100 en slices aisladas hardware). El LLM de código se difiere a una segunda fase. Es viable y consciente del coste; lo que no es viable es prescindir de las capas 5, 6 y 7.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-posts">Lo que no hemos cubierto (próximos posts)&lt;/h2>
&lt;p>Este post se centra en el &lt;strong>diseño estático&lt;/strong> del stack. Quedan piezas que merecen su propio artículo:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>El plano multi-site activo/standby&lt;/strong>: Cilium Cluster Mesh, replicación Qdrant cross-cluster, RTO/RPO realistas, cuándo activo-activo paga y cuándo no.&lt;/li>
&lt;li>&lt;strong>El plano de fine-tuning continuo&lt;/strong>: cómo el pipeline LoRA cierra el bucle desde feedback de producción a adapter promovido, en el espíritu del &lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">post de retrain&lt;/a>.&lt;/li>
&lt;li>&lt;strong>El plano de safety/guardrails&lt;/strong>: dónde encaja Llama Guard, Presidio para PII y XGrammar para structured output garantizado, conectado a la capa 1 del gateway.&lt;/li>
&lt;li>&lt;strong>El plano de coste&lt;/strong>: instrumentación &lt;code>gen_ai.usage.*&lt;/code> a nivel tenant y modelo, dashboards de tokens/euro, decisiones de elasticidad GPU vía KEDA.&lt;/li>
&lt;li>&lt;strong>El plano de cumplimiento&lt;/strong>: cómo el stack se mapea a ENS Alto, NIS2 e ISO/IEC 42001 sin convertir el deployment en un ejercicio de compliance que paraliza la entrega.&lt;/li>
&lt;/ul>
&lt;p>Cada uno cae en una serie distinta del blog y se cubrirá con la misma disciplina: pieza concreta, decisión justificada, error típico que se ve en la práctica.&lt;/p>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">Pipeline LLMOps de seis etapas&lt;/a> — el marco general en el que este stack opera.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/catalogo-herramientas-oss-llmops/">El catálogo OSS para LLMOps en seis etapas&lt;/a> — ficha por ficha de cada herramienta del stack.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/anatomia-request-llm-mayo-2026/">Anatomía de una petición LLM en producción&lt;/a> — la misma arquitectura vista desde la perspectiva de una request individual.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/oss-vs-hyperscalers-llmops/">OSS vs hyperscalers en LLMOps&lt;/a> — comparación con las stacks de AWS/Azure/GCP.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a> — la columna vertebral de la capa 5.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a> — la pieza que da personalización per-tenant sin replicar el base.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/rag-corpus-curation-fundamentos/">RAG corpus curation&lt;/a> y &lt;a href="https://blog.lo0.es/posts/rag-reranker-hybrid-retrieval-fundamentos/">Reranker y hybrid retrieval&lt;/a> — todo lo que entra en la capa 3-4 para que el RAG no degrade.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization para inferencia LLM&lt;/a> — el porqué del FP8 W8A8 del dimensionado.&lt;/li>
&lt;/ul>
&lt;h3 id="documentación-oficial-relevante">Documentación oficial relevante&lt;/h3>
&lt;ul>
&lt;li>vLLM Production Stack — &lt;a href="https://docs.vllm.ai/">docs.vllm.ai&lt;/a>&lt;/li>
&lt;li>SGLang RadixAttention — &lt;a href="https://github.com/sgl-project/sglang">github.com/sgl-project/sglang&lt;/a>&lt;/li>
&lt;li>Envoy AI Gateway — &lt;a href="https://aigateway.envoyproxy.io/">aigateway.envoyproxy.io&lt;/a>&lt;/li>
&lt;li>Langfuse self-hosted — &lt;a href="https://langfuse.com/docs/self-hosting">langfuse.com/docs/self-hosting&lt;/a>&lt;/li>
&lt;li>OpenTelemetry Semantic Conventions for GenAI — &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">opentelemetry.io/docs/specs/semconv/gen-ai&lt;/a>&lt;/li>
&lt;li>Hubble flow observability — &lt;a href="https://docs.cilium.io/en/stable/observability/hubble/">docs.cilium.io/en/stable/observability/hubble&lt;/a>&lt;/li>
&lt;li>Otterize intent-based access — &lt;a href="https://docs.otterize.com/">docs.otterize.com&lt;/a>&lt;/li>
&lt;li>Flux GitOps toolkit — &lt;a href="https://fluxcd.io/">fluxcd.io&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>