<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Flux on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/flux/</link><description>Recent content in Flux on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 11 Jun 2026 10:20:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/tags/flux/index.xml" rel="self" type="application/rss+xml"/><item><title>GitOps del stack de inferencia con Flux: operar el asistente como código</title><link>https://blog.lo0.es/posts/gitops-stack-inferencia-llm-flux/</link><pubDate>Thu, 11 Jun 2026 10:20:00 +0000</pubDate><guid>https://blog.lo0.es/posts/gitops-stack-inferencia-llm-flux/</guid><description>&lt;blockquote>
&lt;p>Este post forma parte de la serie operativa sobre cómo exprimir un cluster LLM on-premise genérico de 4×H100 SXM 80 GB. Las piezas hermanas: la &lt;a href="https://blog.lo0.es/posts/ingesta-documental-rag-pdf-a-chunk-indexado/">ingesta documental para RAG&lt;/a> que llena el vector store que aquí desplegamos, el &lt;a href="https://blog.lo0.es/posts/servir-embeddings-rerankers-tei-produccion/">servicio de embeddings y rerankers con TEI&lt;/a> que es uno de los servicios que GitOps gestiona, y el &lt;a href="https://blog.lo0.es/posts/hardening-secretos-stack-llm-soberano/">hardening de secretos del stack soberano&lt;/a>, que profundiza en el problema de los secretos que aquí solo enunciamos. El asistente completo extremo a extremo (LibreChat + LiteLLM + RAG) que orquesta todo esto tendrá su propio post.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Un asistente LLM en producción es un sistema distribuido, no un binario. Cuenta, como mínimo, con un &lt;strong>motor de inferencia&lt;/strong> (vLLM o similar), un &lt;strong>gateway L7&lt;/strong> que enruta por modelo y aplica rate-limit, un &lt;strong>front de chat&lt;/strong>, un &lt;strong>vector store&lt;/strong> para RAG, un servicio de &lt;strong>embeddings/reranker&lt;/strong>, y una pila de &lt;strong>observabilidad&lt;/strong>. Seis servicios largos, repartidos en tres o cuatro namespaces, con dependencias de arranque (el front no sirve de nada si el gateway no está, el gateway no sirve de nada si el motor no ha cargado el modelo, el RAG no responde si el vector store está vacío). Operarlo a mano —&lt;code>kubectl apply&lt;/code> por aquí, &lt;code>helm upgrade&lt;/code> por allá— produce un cluster cuyo estado real nadie sabe reconstruir: no hay registro de qué se aplicó, ni cuándo, ni por qué. &lt;strong>GitOps&lt;/strong> resuelve esto con cuatro principios (&lt;a href="https://opengitops.dev/">OpenGitOps&lt;/a>): el estado deseado es &lt;strong>declarativo&lt;/strong>, está &lt;strong>versionado e inmutable&lt;/strong> en git, se &lt;strong>aplica automáticamente&lt;/strong> mediante un agente (nadie hace SSH para desplegar) y se &lt;strong>reconcilia continuamente&lt;/strong> comparando el estado real contra el declarado. &lt;strong>Flux&lt;/strong> es ese agente: seis controladores (source, kustomize, helm, notification, image-reflector, image-automation) que clonan el repo, renderizan Kustomize/Helm, aplican al cluster, corrigen drift y, opcionalmente, escriben de vuelta a git para subir el tag de una imagen nueva. La unidad de trabajo es la &lt;code>Kustomization&lt;/code> o el &lt;code>HelmRelease&lt;/code>, con &lt;code>interval&lt;/code> (cada cuánto reconcilia), &lt;code>prune&lt;/code> (borra lo que ya no está en git), &lt;code>dependsOn&lt;/code> (ordena el arranque) y health checks. El &lt;code>interval&lt;/code> fija el MTTR del drift: con &lt;code>interval=1m&lt;/code>, una edición manual al cluster se revierte en ≤1 min de media. Los secretos no caben en claro en git —problema huevo-gallina— y se resuelve con SOPS/age, sealed-secrets o External Secrets + Vault. El rollback es un &lt;code>git revert&lt;/code>. GitOps no es gratis: la curva de aprendizaje es real y debuggear por qué una &lt;code>Kustomization&lt;/code> no reconcilia es una habilidad nueva.&lt;/p>
&lt;h2 id="la-analogía-el-plano-maestro-y-el-capataz-que-no-negocia">La analogía: el plano maestro y el capataz que no negocia&lt;/h2>
&lt;p>Imagina una obra grande con un &lt;strong>plano maestro&lt;/strong> firmado y archivado, y un &lt;strong>capataz&lt;/strong> que tiene una sola orden: que la obra sea idéntica al plano, a todas horas. El capataz no improvisa. Cada cierto tiempo recorre la obra con el plano en la mano y compara: si una pared está donde dice el plano, la deja; si alguien ha movido un tabique de noche sin actualizar el plano, lo devuelve a su sitio; si el plano dice que hay una columna y no existe, la levanta; si una columna existe pero ya no aparece en el plano, la derriba. El plano es la &lt;strong>única fuente de verdad&lt;/strong>: para cambiar la obra, no se toca la obra, se cambia el plano —y el capataz se encarga del resto en su siguiente ronda.&lt;/p>
&lt;p>Esa es exactamente la mecánica de GitOps. &lt;strong>Git es el plano maestro&lt;/strong>: declarativo (describe el estado final, no los pasos), versionado (cada cambio queda firmado en el historial, con autor y motivo), inmutable (un commit no se reescribe). &lt;strong>Flux es el capataz&lt;/strong>: cada &lt;code>interval&lt;/code> recorre el cluster, compara contra git y converge. Y aquí está la lección que separa GitOps de &amp;ldquo;tener los YAML en un repo&amp;rdquo;: el capataz &lt;strong>deshace los cambios manuales&lt;/strong>. Si un operador entra con &lt;code>kubectl edit&lt;/code> y sube las réplicas del motor de inferencia de 2 a 4 a las tres de la madrugada para apagar un incendio, en la siguiente ronda Flux lo devuelve a 2 —porque el plano dice 2. Esto enfurece a quien viene del mundo imperativo y es precisamente el punto: si quieres 4 réplicas de forma permanente, actualizas el plano. El cluster deja de ser un sistema con memoria propia y opaca, y pasa a ser una &lt;strong>proyección reproducible de git&lt;/strong>. Borra el cluster entero, apunta un Flux nuevo al mismo repo, y la obra se reconstruye idéntica.&lt;/p>
&lt;p>La analogía también marca la frontera: el plano describe el edificio, no quién tiene las llaves del almacén de material. Los &lt;strong>secretos&lt;/strong> —contraseñas, tokens, claves— no pueden ir en el plano público. Ese es el problema huevo-gallina que tratamos más abajo y que la &lt;a href="https://blog.lo0.es/posts/hardening-secretos-stack-llm-soberano/">pieza de hardening&lt;/a> desarrolla a fondo.&lt;/p>
&lt;h2 id="el-problema-un-asistente-son-seis-servicios-no-uno">El problema: un asistente son seis servicios, no uno&lt;/h2>
&lt;p>Antes de Flux, conviene fijar el tamaño del problema. Un asistente LLM soberano mínimamente serio, desplegado sobre el cluster genérico de referencia, tiene esta topología de servicios:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Servicio&lt;/th>
&lt;th>Función&lt;/th>
&lt;th>Namespace típico&lt;/th>
&lt;th>Estado&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Motor de inferencia (vLLM)&lt;/td>
&lt;td>Sirve los tokens del LLM general y el de código&lt;/td>
&lt;td>&lt;code>llm-serving&lt;/code>&lt;/td>
&lt;td>Stateless (modelo desde object store)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Embeddings + reranker (TEI)&lt;/td>
&lt;td>Vectoriza queries y reordena candidatos para RAG&lt;/td>
&lt;td>&lt;code>llm-serving&lt;/code>&lt;/td>
&lt;td>Stateless&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vector store (Qdrant)&lt;/td>
&lt;td>Almacena y busca los embeddings de documentos&lt;/td>
&lt;td>&lt;code>data&lt;/code>&lt;/td>
&lt;td>&lt;strong>Stateful&lt;/strong> (PVC)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gateway L7 (Envoy AI Gateway / LiteLLM)&lt;/td>
&lt;td>Routing por modelo, rate-limit, auth&lt;/td>
&lt;td>&lt;code>gateway&lt;/code>&lt;/td>
&lt;td>Semi-stateless&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Front de chat&lt;/td>
&lt;td>UI del asistente&lt;/td>
&lt;td>&lt;code>apps&lt;/code>&lt;/td>
&lt;td>Stateless&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Observabilidad (Langfuse, OTel Collector, dashboards)&lt;/td>
&lt;td>Trazas, métricas y logs LLM-aware&lt;/td>
&lt;td>&lt;code>observability&lt;/code>&lt;/td>
&lt;td>Stateful (Postgres de Langfuse)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Seis servicios, cuatro o cinco namespaces, dos componentes con estado persistente, y un grafo de dependencias real: el front llama al gateway, el gateway al motor y al servicio de embeddings, el RAG depende del vector store &lt;strong>lleno&lt;/strong>, y todo emite trazas a observabilidad. Desplegar esto a mano implica recordar el orden, los valores de cada &lt;code>helm install&lt;/code>, los &lt;code>ConfigMap&lt;/code>, los &lt;code>Secret&lt;/code>, los &lt;code>nodeSelector&lt;/code> para clavar los pods GPU en los nodos correctos. Hazlo dos veces (un entorno de staging y uno de producción) y los dos divergen en cuestión de días. Eso es exactamente lo que GitOps elimina.&lt;/p>
&lt;h2 id="los-seis-controladores-de-flux">Los seis controladores de Flux&lt;/h2>
&lt;p>Flux no es un binario monolítico, sino un conjunto de controladores que cooperan —el &lt;em>GitOps Toolkit&lt;/em>. Una instalación por defecto trae cuatro; los dos de image automation se añaden con &lt;code>--components-extra&lt;/code> (&lt;a href="https://fluxcd.io/flux/installation/">instalación de Flux&lt;/a>):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>source-controller&lt;/strong>: clona y mantiene actualizadas las fuentes —repos git (&lt;code>GitRepository&lt;/code>), repos Helm (&lt;code>HelmRepository&lt;/code>), buckets, artefactos OCI—. Es quien &amp;ldquo;tiene el plano en la mano&amp;rdquo;: expone el contenido del repo como un artefacto interno que los demás consumen.&lt;/li>
&lt;li>&lt;strong>kustomize-controller&lt;/strong>: toma un artefacto de fuente, renderiza Kustomize (bases + overlays) y aplica el resultado al cluster. Es el responsable de &lt;code>prune&lt;/code>, &lt;code>dependsOn&lt;/code> y los health checks de las &lt;code>Kustomization&lt;/code>.&lt;/li>
&lt;li>&lt;strong>helm-controller&lt;/strong>: reconcilia objetos &lt;code>HelmRelease&lt;/code> —instala y actualiza charts de Helm de forma declarativa, sin que nadie ejecute &lt;code>helm&lt;/code> desde una terminal (&lt;a href="https://github.com/fluxcd/helm-controller">helm-controller&lt;/a>).&lt;/li>
&lt;li>&lt;strong>notification-controller&lt;/strong>: el puente con el exterior en ambos sentidos. Recibe webhooks (para reconciliar al instante en cada push, en vez de esperar al &lt;code>interval&lt;/code>) y emite eventos/alertas a Slack, a un chat interno o a un sistema de incidencias.&lt;/li>
&lt;li>&lt;strong>image-reflector-controller&lt;/strong>: escanea registries de contenedores y guarda los tags encontrados en una base de datos interna. Es los &amp;ldquo;ojos&amp;rdquo; de la image automation.&lt;/li>
&lt;li>&lt;strong>image-automation-controller&lt;/strong>: usa lo que ven los ojos para escribir de vuelta en git —hace commit del nuevo tag de imagen en los manifiestos cuando aparece una versión que cumple la política.&lt;/li>
&lt;/ul>
&lt;p>Un cluster sin image automation no necesita los dos últimos. Pero para un stack de inferencia donde el servidor de modelos se actualiza con cierta frecuencia, son los que automatizan la promoción de versiones sin tocar nada a mano.&lt;/p>
&lt;div class="diagram" style="max-width:860px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 860 420" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="bucle GitOps: git, source-controller, kustomize y helm controllers, cluster, drift y image automation que escribe de vuelta a git">
&lt;defs>
&lt;marker id="ga" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>&lt;/marker>
&lt;marker id="gb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#8b5cf6"/>&lt;/marker>
&lt;marker id="gr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#ef4444"/>&lt;/marker>
&lt;/defs>
&lt;text x="430" y="26" text-anchor="middle" font-size="14" font-weight="700" fill="currentColor">El bucle de reconciliación de Flux&lt;/text>
&lt;rect x="40" y="60" width="150" height="80" rx="8" fill="#3b82f6" fill-opacity="0.12" stroke="#3b82f6" stroke-width="1.6"/>
&lt;text x="115" y="92" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">git&lt;/text>
&lt;text x="115" y="112" text-anchor="middle" font-size="10" fill="currentColor">plano maestro&lt;/text>
&lt;text x="115" y="127" text-anchor="middle" font-size="10" fill="currentColor">estado deseado&lt;/text>
&lt;rect x="270" y="50" width="180" height="46" rx="6" fill="#22c55e" fill-opacity="0.12" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="360" y="70" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">source-controller&lt;/text>
&lt;text x="360" y="86" text-anchor="middle" font-size="9.5" fill="currentColor">clona el repo · artefacto&lt;/text>
&lt;rect x="270" y="106" width="180" height="46" rx="6" fill="#22c55e" fill-opacity="0.12" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="360" y="126" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">kustomize-controller&lt;/text>
&lt;text x="360" y="142" text-anchor="middle" font-size="9.5" fill="currentColor">renderiza overlays · aplica&lt;/text>
&lt;rect x="270" y="162" width="180" height="46" rx="6" fill="#22c55e" fill-opacity="0.12" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="360" y="182" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">helm-controller&lt;/text>
&lt;text x="360" y="198" text-anchor="middle" font-size="9.5" fill="currentColor">reconcilia HelmRelease&lt;/text>
&lt;rect x="560" y="90" width="170" height="120" rx="8" fill="#f59e0b" fill-opacity="0.12" stroke="#f59e0b" stroke-width="1.6"/>
&lt;text x="645" y="118" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">cluster&lt;/text>
&lt;text x="645" y="138" text-anchor="middle" font-size="10" fill="currentColor">estado real&lt;/text>
&lt;text x="645" y="158" text-anchor="middle" font-size="9.5" fill="currentColor">motor · gateway&lt;/text>
&lt;text x="645" y="173" text-anchor="middle" font-size="9.5" fill="currentColor">front · vector store&lt;/text>
&lt;text x="645" y="188" text-anchor="middle" font-size="9.5" fill="currentColor">observabilidad&lt;/text>
&lt;path d="M190,90 L268,75" fill="none" stroke="#64748b" stroke-width="1.6" marker-end="url(#ga)"/>
&lt;path d="M450,129 L558,140" fill="none" stroke="#64748b" stroke-width="1.6" marker-end="url(#ga)"/>
&lt;path d="M450,185 L558,165" fill="none" stroke="#64748b" stroke-width="1.6" marker-end="url(#ga)"/>
&lt;path d="M645,210 C645,250 360,250 360,213" fill="none" stroke="#ef4444" stroke-width="1.8" stroke-dasharray="5 3" marker-end="url(#gr)"/>
&lt;text x="430" y="268" text-anchor="middle" font-size="10" fill="#ef4444">reconcile cada interval: compara real vs deseado, corrige drift, prune&lt;/text>
&lt;rect x="270" y="312" width="180" height="58" rx="6" fill="#8b5cf6" fill-opacity="0.12" stroke="#8b5cf6" stroke-width="1.6"/>
&lt;text x="360" y="334" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">image automation&lt;/text>
&lt;text x="360" y="350" text-anchor="middle" font-size="9.5" fill="currentColor">reflector escanea registry&lt;/text>
&lt;text x="360" y="364" text-anchor="middle" font-size="9.5" fill="currentColor">policy elige tag · commit&lt;/text>
&lt;path d="M560,200 C500,300 460,320 452,335" fill="none" stroke="#8b5cf6" stroke-width="1.6" marker-end="url(#gb)"/>
&lt;text x="540" y="300" text-anchor="middle" font-size="9.5" fill="#8b5cf6">registry: tag nuevo&lt;/text>
&lt;path d="M268,338 C160,330 115,250 115,142" fill="none" stroke="#8b5cf6" stroke-width="1.8" marker-end="url(#gb)"/>
&lt;text x="120" y="250" text-anchor="middle" font-size="9.5" fill="#8b5cf6">escribe de vuelta&lt;/text>
&lt;text x="120" y="263" text-anchor="middle" font-size="9.5" fill="#8b5cf6">a git (commit)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>El diagrama tiene tres recorridos. El &lt;strong>gris&lt;/strong> es el flujo principal: git → source-controller → kustomize/helm-controller → cluster. El &lt;strong>rojo discontinuo&lt;/strong> es el bucle de reconciliación que se ejecuta cada &lt;code>interval&lt;/code>: compara estado real contra deseado, corrige el drift y poda lo que sobra. El &lt;strong>morado&lt;/strong> es la image automation: el reflector ve un tag nuevo en el registry, la policy decide si cumple, y el automation-controller &lt;strong>escribe de vuelta a git&lt;/strong>. Ese último recorrido es el que cierra el círculo y convierte git en un sistema que se actualiza solo.&lt;/p>
&lt;h2 id="estructura-de-repo-clusters-infrastructure-apps">Estructura de repo: clusters, infrastructure, apps&lt;/h2>
&lt;p>La convención más extendida separa tres niveles de responsabilidad. No es la única, pero envejece bien:&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">gitops-repo/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── clusters/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── prod/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── infrastructure.yaml # Kustomization → ./infrastructure
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── apps.yaml # Kustomization → ./apps (dependsOn infrastructure)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── infrastructure/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── controllers/ # ingress, cert-manager, GPU operator, KEDA...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── configs/ # ClusterIssuer, RuntimeClass, StorageClass...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── apps/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── base/ # manifiestos comunes a todos los entornos
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── llm-engine/ # HelmRelease vLLM
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── gateway/ # HelmRelease del gateway L7
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── chat-front/ # Deployment + Service + Ingress
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── vector-store/ # HelmRelease Qdrant (+ PVC)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── observability/ # HelmRelease Langfuse + OTel
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── staging/ # overlay: réplicas bajas, modelo pequeño
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── prod/ # overlay: réplicas altas, modelo grande, MIG
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La pieza clave es la separación &lt;strong>base / overlay&lt;/strong> de Kustomize. La &lt;code>base/&lt;/code> describe el servicio una sola vez; cada overlay (&lt;code>staging/&lt;/code>, &lt;code>prod/&lt;/code>) aplica un &lt;code>patch&lt;/code> con las diferencias del entorno: número de réplicas, tamaño del modelo, perfil MIG, &lt;code>nodeSelector&lt;/code>, &lt;code>--gpu-memory-utilization&lt;/code>. Esto evita duplicar manifiestos completos por entorno. La carpeta &lt;code>clusters/&lt;/code> es el punto de entrada que Flux reconcilia primero: contiene las &lt;code>Kustomization&lt;/code> raíz que apuntan a &lt;code>infrastructure/&lt;/code> y &lt;code>apps/&lt;/code>, con un &lt;code>dependsOn&lt;/code> que garantiza que la infraestructura (CRDs, operadores, storage classes) esté lista &lt;strong>antes&lt;/strong> que las aplicaciones.&lt;/p>
&lt;h3 id="cuántos-manifiestos-el-cálculo-que-justifica-los-overlays">Cuántos manifiestos: el cálculo que justifica los overlays&lt;/h3>
&lt;p>Aquí es donde los números hacen el argumento. Sin overlays, cada servicio requiere un juego completo de manifiestos por entorno. Con $N$ servicios y $M$ entornos, el coste en archivos a mantener es:&lt;/p>
&lt;p>$$
\text{archivos}_{\text{naïve}} = N \times M \times k
$$&lt;/p>
&lt;p>donde $k$ es el número medio de manifiestos por servicio (Deployment/HelmRelease + Service + ConfigMap + PVC + Ingress ≈ 5). Para nuestro asistente, $N=6$ servicios y $M=3$ entornos (dev, staging, prod), con $k=5$:&lt;/p>
&lt;p>$$
6 \times 3 \times 5 = 90 \text{ archivos completos, cada uno mantenido por separado.}
$$&lt;/p>
&lt;p>Con la estructura base/overlay, la base se escribe una vez y cada overlay solo contiene el &lt;code>patch&lt;/code> de las diferencias (típicamente 1 fichero corto por servicio por entorno):&lt;/p>
&lt;p>$$
\text{archivos}&lt;em>{\text{overlay}} = \underbrace{N \times k}&lt;/em>{\text{base}} + \underbrace{N \times M}_{\text{patches}} = 6 \times 5 + 6 \times 3 = 30 + 18 = 48
$$&lt;/p>
&lt;p>No es solo que sean menos archivos (48 frente a 90): es que &lt;strong>el grueso del cambio se hace en un solo sitio&lt;/strong>. Cambiar el límite de memoria del motor para todos los entornos es una edición en &lt;code>base/&lt;/code>, no tres ediciones sincronizadas. El factor de ahorro crece con $M$: para 5 entornos, la versión naïve son 150 archivos y la de overlays son 60. La duplicación es el enemigo de la auditabilidad, y los overlays la atacan en la raíz.&lt;/p>
&lt;h2 id="el-bucle-de-reconciliación-desired-vs-actual">El bucle de reconciliación: desired vs actual&lt;/h2>
&lt;p>El corazón de Flux es un bucle de control que no tiene fin: cada &lt;code>interval&lt;/code>, lee el estado deseado (git), lee el estado real (cluster), calcula la diferencia y la aplica. Es el mismo principio de un &lt;strong>termostato&lt;/strong>: lee la temperatura objetivo (git), lee la temperatura real (cluster) y enciende o apaga la caldera hasta que coinciden. No &amp;ldquo;despliega una vez&amp;rdquo;; converge para siempre.&lt;/p>
&lt;p>Un manifiesto &lt;code>Kustomization&lt;/code> de las aplicaciones, con las piezas que importan (&lt;a href="https://fluxcd.io/flux/components/kustomize/kustomizations/">referencia Kustomization&lt;/a>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kustomize.toolkit.fluxcd.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Kustomization&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">flux-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval: 1m # reconcilia cada minuto&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fija el MTTR del drift&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./apps/prod &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># overlay de producción&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prune&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># borra del cluster lo que se quita de git&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sourceRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GitRepository&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gitops-repo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dependsOn&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">infrastructure &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># no aplica apps hasta que infra esté Ready&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">wait&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># espera a que los recursos estén healthy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># falla la reconciliación si no converge en 5 min&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">healthChecks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Cuatro campos hacen el trabajo pesado:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>interval&lt;/code>&lt;/strong> define cada cuánto se ejecuta la ronda del capataz. Es el determinante directo del MTTR del drift (siguiente sección).&lt;/li>
&lt;li>&lt;strong>&lt;code>prune: true&lt;/code>&lt;/strong> activa la recolección de basura: los objetos que se aplicaron antes pero ya no están en la revisión actual de git se borran del cluster automáticamente. Sin &lt;code>prune&lt;/code>, un servicio retirado del repo sigue corriendo —el plano y la obra divergen en silencio.&lt;/li>
&lt;li>&lt;strong>&lt;code>dependsOn&lt;/code>&lt;/strong> ordena el grafo: Flux no aplica los manifiestos de una &lt;code>Kustomization&lt;/code> hasta que todas las referenciadas tengan estado &lt;code>Ready: True&lt;/code>. Es el mecanismo que garantiza datos → gateway → front.&lt;/li>
&lt;li>&lt;strong>&lt;code>wait&lt;/code> + &lt;code>healthChecks&lt;/code>&lt;/strong>: con &lt;code>wait: true&lt;/code>, Flux monitoriza todos los recursos aplicados y espera a que estén listos antes de marcar la reconciliación como exitosa; &lt;code>healthChecks&lt;/code> permite afinar exactamente qué recursos vigilar. Esto es lo que hace que un &lt;code>dependsOn&lt;/code> signifique algo: la dependencia no está satisfecha hasta que está &lt;strong>sana&lt;/strong>, no solo aplicada.&lt;/li>
&lt;/ul>
&lt;h3 id="mttr-y-detección-de-drift-en-función-del-interval">MTTR y detección de drift en función del interval&lt;/h3>
&lt;p>El &lt;code>interval&lt;/code> es el único parámetro que el operador elige para gobernar la velocidad del bucle, y se traduce directamente en métricas operativas. Si una desviación (drift) ocurre en un instante aleatorio dentro del periodo de reconciliación $T$, el tiempo de espera hasta que Flux la detecta se distribuye uniformemente en $[0, T]$, con media:&lt;/p>
&lt;p>$$
\mathbb{E}[t_{\text{detección}}] = \frac{T}{2}
$$&lt;/p>
&lt;p>A esto se le suma el tiempo de corrección $t_c$ (renderizar, hacer diff, aplicar), normalmente unos segundos. El MTTR del drift queda:&lt;/p>
&lt;p>$$
\text{MTTR}_{\text{drift}} = \frac{T}{2} + t_c
$$&lt;/p>
&lt;p>Con &lt;code>interval=1m&lt;/code> y $t_c \approx 10\text{s}$, el drift se corrige en $\frac{60}{2} + 10 = 40$ s de media, con un peor caso de $60 + 10 = 70$ s. Con &lt;code>interval=10m&lt;/code>, la media sube a $5\text{min};10\text{s}$ y el peor caso a más de 10 min: una ventana en la que un cambio manual sigue activo. La tentación es poner &lt;code>interval=10s&lt;/code> y olvidarse, pero hay un coste: cada reconciliación consume CPU y, sobre todo, hace peticiones a la API del cluster y al registry. Con decenas de &lt;code>Kustomization&lt;/code> reconciliando cada 10 s, el &lt;code>kube-apiserver&lt;/code> y el image-reflector empiezan a notar la presión. La regla práctica: &lt;code>interval&lt;/code> corto (1m) para las aplicaciones críticas cuyo drift duele, &lt;code>interval&lt;/code> largo (10–30m) para infraestructura estable que casi nunca cambia, y &lt;strong>webhooks&lt;/strong> del notification-controller para reconciliación instantánea en cada push —así el &lt;code>interval&lt;/code> solo gobierna el drift, no la latencia de despliegue.&lt;/p>
&lt;h2 id="helm-como-código-el-helmrelease-del-motor-de-inferencia">Helm como código: el HelmRelease del motor de inferencia&lt;/h2>
&lt;p>Para el motor, el gateway y el front, lo habitual es empaquetarlos como charts de Helm y declararlos con &lt;code>HelmRelease&lt;/code>. El helm-controller los reconcilia sin que nadie ejecute &lt;code>helm&lt;/code> jamás:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">helm.toolkit.fluxcd.io/v2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HelmRelease&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-engine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-serving&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">10m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">chart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">chart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sourceRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HelmRepository&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-charts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.8.4&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># gestionado por image automation (ver abajo)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicaCount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/gpu.product&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">H100-SXM&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extraArgs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;--gpu-memory-utilization=0.90&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;--tensor-parallel-size=1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Todo lo que en el mundo imperativo sería un flag de &lt;code>vllm serve&lt;/code> —&lt;code>--gpu-memory-utilization&lt;/code>, &lt;code>--tensor-parallel-size&lt;/code>— vive ahora como dato versionado. Cambiar la fracción de VRAM que el motor reserva es un commit, revisable en un PR, con historial. La relación con &lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">compartir una GPU&lt;/a> es directa: el perfil MIG y el &lt;code>--gpu-memory-utilization&lt;/code> son parámetros de despliegue, y aquí son código.&lt;/p>
&lt;h2 id="image-automation-subir-el-tag-sin-tocar-nada-a-mano">Image automation: subir el tag sin tocar nada a mano&lt;/h2>
&lt;p>El motor de inferencia se actualiza con cierta frecuencia (parches de vLLM, fixes de seguridad). Sin automation, cada versión nueva exige que alguien edite el &lt;code>tag&lt;/code> en el &lt;code>HelmRelease&lt;/code>. La image automation de Flux lo hace sola, con tres objetos (&lt;a href="https://fluxcd.io/flux/guides/image-update/">automate image updates&lt;/a>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">image.toolkit.fluxcd.io/v1beta2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ImageRepository&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">flux-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry.example.local/inference/vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># cada 5 min escanea los tags del registry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">image.toolkit.fluxcd.io/v1beta2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ImagePolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">flux-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imageRepositoryRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">semver&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">range&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;&amp;gt;=0.8.0 &amp;lt;0.9.0&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># solo parches y minors dentro de 0.8.x–0.8.x&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">image.toolkit.fluxcd.io/v1beta1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ImageUpdateAutomation&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">flux-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sourceRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GitRepository&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gitops-repo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">git&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">author&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fluxbot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">email&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fluxbot@example.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">messageTemplate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;auto: bump vllm to {{ .NewTag }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El reparto de papeles: &lt;strong>&lt;code>ImageRepository&lt;/code>&lt;/strong> escanea todos los tags del repositorio de la imagen y los guarda en la base de datos interna del reflector. &lt;strong>&lt;code>ImagePolicy&lt;/code>&lt;/strong> lee esos tags y elige el &amp;ldquo;último&amp;rdquo; según la política —el campo &lt;code>policy&lt;/code> es obligatorio y define cómo se selecciona (&lt;a href="https://fluxcd.io/flux/components/image/imagepolicies/">ImagePolicy&lt;/a>). &lt;strong>&lt;code>ImageUpdateAutomation&lt;/code>&lt;/strong> toma el tag elegido, edita el manifiesto en el repo (donde haya un marcador &lt;code># {&amp;quot;$imagepolicy&amp;quot;: &amp;quot;flux-system:vllm&amp;quot;}&lt;/code>) y hace commit (&lt;a href="https://fluxcd.io/flux/components/image/imageupdateautomations/">ImageUpdateAutomation&lt;/a>).&lt;/p>
&lt;p>La elección de política importa:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>semver&lt;/strong>: interpreta los tags como versiones semánticas y elige la más alta que cumpla el rango (&lt;code>range: &amp;quot;&amp;gt;=0.8.0 &amp;lt;0.9.0&amp;quot;&lt;/code>). Por defecto excluye prereleases (&lt;code>0.8.0-rc.1&lt;/code> no entra) salvo que se pidan explícitamente. Es la opción correcta para producción: te quedas dentro de un rango de versiones probado y no saltas a un major nuevo sin querer.&lt;/li>
&lt;li>&lt;strong>regex / alphabetical / numerical&lt;/strong>: para esquemas de tags que no son semver puros —por ejemplo &lt;code>main-&amp;lt;sha&amp;gt;-&amp;lt;timestamp&amp;gt;&lt;/code> de un pipeline interno—. Más flexible, pero te obliga a confiar en que el orden de tags refleja el orden de versiones, lo cual es frágil.&lt;/li>
&lt;/ul>
&lt;p>Una pauta de seguridad: en lugar de que &lt;code>ImageUpdateAutomation&lt;/code> haga &lt;code>push&lt;/code> directo a &lt;code>main&lt;/code>, se le configura para que escriba a una &lt;strong>rama&lt;/strong> y abra un &lt;strong>PR&lt;/strong>. Así el bump de versión pasa por revisión humana o por un pipeline de validación antes de llegar al cluster. El automation propone; el humano (o un gate automático) dispone. Y para producción, el bump de versión no es el despliegue: es el disparador del rollout progresivo descrito en &lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">canary, blue-green y shadow&lt;/a>.&lt;/p>
&lt;h2 id="secretos-en-gitops-el-problema-huevo-gallina">Secretos en GitOps: el problema huevo-gallina&lt;/h2>
&lt;p>GitOps exige que todo el estado deseado esté en git. Pero los secretos —la contraseña de Postgres de Langfuse, el token del registry, las API keys del gateway— no pueden ir en claro en un repo, ni siquiera privado: el historial de git es inmutable, y un secreto commiteado una vez queda ahí para siempre. Esta es la tensión fundamental: GitOps quiere todo en git; la seguridad prohíbe los secretos en git. Hay tres familias de solución, todas con la misma idea de fondo —&lt;strong>en git solo va el secreto cifrado; el cluster tiene la llave para descifrarlo&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>SOPS + age/KMS&lt;/strong>: cifras los valores sensibles de un manifiesto con &lt;a href="https://github.com/getsops/sops">SOPS&lt;/a> (deja las claves legibles, cifra solo los valores). El YAML cifrado va a git; el kustomize-controller lleva una clave age o accede a un KMS para descifrarlo en el momento de aplicar. Simple, sin componentes extra en el cluster.&lt;/li>
&lt;li>&lt;strong>sealed-secrets&lt;/strong>: un controlador en el cluster tiene una clave privada. Cifras el secreto contra su clave pública (con &lt;code>kubeseal&lt;/code>), el &lt;code>SealedSecret&lt;/code> cifrado va a git, y el controlador lo descifra a un &lt;code>Secret&lt;/code> normal dentro del cluster. La clave privada nunca sale del cluster.&lt;/li>
&lt;li>&lt;strong>External Secrets + Vault&lt;/strong>: en git no va ni siquiera el secreto cifrado, solo una &lt;strong>referencia&lt;/strong> (&lt;code>ExternalSecret&lt;/code>) que dice &amp;ldquo;el valor de esta clave está en Vault, en esta ruta&amp;rdquo;. El operador External Secrets lo resuelve en tiempo de aplicación. Es el patrón más limpio para muchos secretos y rotación frecuente, a costa de operar un Vault.&lt;/li>
&lt;/ul>
&lt;p>La elección depende del volumen de secretos y de si ya tienes un gestor central. Para un stack pequeño, SOPS+age es suficiente y sin dependencias. La &lt;a href="https://blog.lo0.es/posts/hardening-secretos-stack-llm-soberano/">pieza hermana de hardening&lt;/a> entra en el detalle de cada opción, la rotación de claves y el modelo de amenaza.&lt;/p>
&lt;h2 id="promoción-y-rollback-git-revert-es-el-botón-de-pánico">Promoción y rollback: git revert es el botón de pánico&lt;/h2>
&lt;p>La consecuencia más elegante de tener el cluster como proyección de git es que &lt;strong>el rollback es un &lt;code>git revert&lt;/code>&lt;/strong>. Si un despliegue rompe producción —el motor nuevo da peor latencia, el gateway empieza a devolver 5xx—, no hay que recordar qué versión había antes ni reconstruir el estado a mano. Se revierte el commit que introdujo el cambio, y en la siguiente reconciliación (≤ &lt;code>interval&lt;/code>, o instantáneo con webhook) Flux devuelve el cluster al estado anterior. El historial de git es, literalmente, el historial de despliegues: cada commit es un punto de restauración con autor, fecha y motivo.&lt;/p>
&lt;p>Esto encaja con las estrategias de promoción del post de &lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">canary, blue-green y shadow&lt;/a>: Flux gestiona &lt;strong>qué está declarado&lt;/strong>, y la herramienta de rollout progresivo gestiona &lt;strong>cómo se mueve el tráfico&lt;/strong> entre la versión vieja y la nueva. El git revert es el rollback de último recurso (vuelve todo a un estado conocido); el canary es el mecanismo que evita necesitarlo. Para servicios donde el cambio de versión es delicado, lo correcto es que image automation haga el bump en una rama, el canary valide los gates de regresión, y solo entonces se promocione el commit a &lt;code>main&lt;/code>.&lt;/p>
&lt;h2 id="la-costura-gitops-no-es-gratis">La costura: GitOps no es gratis&lt;/h2>
&lt;p>Sería deshonesto vender GitOps como magia. Tiene costes reales que cualquier equipo paga:&lt;/p>
&lt;p>&lt;strong>Curva de aprendizaje.&lt;/strong> El modelo declarativo es un cambio de mentalidad. El operador que lleva años arreglando incidentes con &lt;code>kubectl edit&lt;/code> tiene que desaprender el reflejo: ahora el cluster le revierte los cambios y eso, al principio, parece que Flux &amp;ldquo;le pelea&amp;rdquo;. Entender que el cambio se hace en git, no en el cluster, lleva semanas de incomodidad.&lt;/p>
&lt;p>&lt;strong>Debuggear al reconciliador.&lt;/strong> Cuando una &lt;code>Kustomization&lt;/code> no aplica, el error no está en el pod —está en la cadena de reconciliación. ¿El source-controller clonó la revisión correcta? ¿El kustomize-controller renderizó bien el overlay? ¿Falló un health check del &lt;code>dependsOn&lt;/code>? ¿El timeout saltó antes de que el modelo grande terminara de cargar? Diagnosticar esto requiere &lt;code>flux get&lt;/code>, &lt;code>flux logs&lt;/code>, leer los &lt;code>status.conditions&lt;/code> de los objetos Flux y entender en qué eslabón se atascó. Es una habilidad nueva, distinta de debuggear Kubernetes &amp;ldquo;a secas&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Timeouts y cargas lentas.&lt;/strong> El &lt;code>timeout&lt;/code> de una &lt;code>Kustomization&lt;/code> con &lt;code>wait: true&lt;/code> tiene que ser mayor que el tiempo de arranque del servicio más lento. Un motor de inferencia que tarda 4 minutos en cargar un modelo grande desde el object store hará fallar una &lt;code>Kustomization&lt;/code> con &lt;code>timeout: 2m&lt;/code>, aunque todo esté bien. Calibrar timeouts por servicio es trabajo fino.&lt;/p>
&lt;p>&lt;strong>El drift que sí querías.&lt;/strong> A veces el operador &lt;em>necesita&lt;/em> un cambio temporal urgente y Flux se lo revierte. La respuesta correcta —suspender la reconciliación de esa &lt;code>Kustomization&lt;/code> con &lt;code>flux suspend&lt;/code>, hacer el cambio, y luego reflejarlo en git y reanudar— es disciplina que hay que construir. Sin esa disciplina, la gente acaba peleándose con el capataz a las 3 de la madrugada.&lt;/p>
&lt;p>Ninguno de estos costes anula el beneficio. Pero un equipo que adopta GitOps esperando que &amp;ldquo;todo sea más fácil desde el día uno&amp;rdquo; se frustra. Es más fácil &lt;strong>a partir del mes dos&lt;/strong>, cuando la auditabilidad, la reproducibilidad y el rollback trivial ya han pagado la curva.&lt;/p>
&lt;h2 id="aplicado-al-cluster-genérico-4h100">Aplicado al cluster genérico 4×H100&lt;/h2>
&lt;p>Sobre el cluster de referencia (4×H100 SXM 80 GB por nodo GPU, NVLink, más nodos CPU y de control), el stack del asistente se gestiona enteramente por GitOps. Las decisiones de hardware se vuelven datos en los overlays.&lt;/p>
&lt;p>&lt;strong>nodeSelectors y perfiles MIG como código.&lt;/strong> El overlay de &lt;code>prod&lt;/code> clava cada servicio en el nodo correcto y declara el perfil de GPU. El motor general usa una GPU entera; los embeddings y el LLM pequeño caben en slices MIG —exactamente el reparto del post de &lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">compartir una GPU&lt;/a>, pero ahora versionado:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># apps/prod/llm-engine-patch.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/gpu.product&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">H100-SXM&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extraArgs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;--gpu-memory-utilization=0.92&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;--tensor-parallel-size=1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># apps/prod/embeddings-patch.yaml — sobre un slice MIG&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/mig.config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;3g.40gb&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/mig-3g.40gb&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El perfil MIG (&lt;code>3g.40gb&lt;/code>), la fracción de VRAM (&lt;code>0.92&lt;/code>), el tensor-parallel: todo es texto en un PR. Cambiar el reparto de GPU entre servicios es un commit revisable, no una sesión de &lt;code>nvidia-smi mig&lt;/code> a mano que nadie registra.&lt;/p>
&lt;p>&lt;strong>Orden de arranque con dependsOn.&lt;/strong> El grafo de dependencias del asistente se codifica con &lt;code>dependsOn&lt;/code> encadenados. El orden correcto es &lt;strong>datos → embeddings/motor → gateway → front&lt;/strong>:&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 180" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="orden de arranque con dependsOn: datos, motor y embeddings, gateway, front">
&lt;defs>
&lt;marker id="da" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>&lt;/marker>
&lt;/defs>
&lt;text x="410" y="26" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">Orden de arranque del asistente vía dependsOn&lt;/text>
&lt;rect x="20" y="60" width="150" height="70" rx="8" fill="#8b5cf6" fill-opacity="0.12" stroke="#8b5cf6" stroke-width="1.6"/>
&lt;text x="95" y="88" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">datos&lt;/text>
&lt;text x="95" y="106" text-anchor="middle" font-size="9.5" fill="currentColor">vector store&lt;/text>
&lt;text x="95" y="120" text-anchor="middle" font-size="9.5" fill="currentColor">Postgres Langfuse&lt;/text>
&lt;rect x="220" y="60" width="160" height="70" rx="8" fill="#f59e0b" fill-opacity="0.12" stroke="#f59e0b" stroke-width="1.6"/>
&lt;text x="300" y="84" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">motor + embeddings&lt;/text>
&lt;text x="300" y="102" text-anchor="middle" font-size="9.5" fill="currentColor">vLLM (GPU entera)&lt;/text>
&lt;text x="300" y="116" text-anchor="middle" font-size="9.5" fill="currentColor">TEI (slice MIG)&lt;/text>
&lt;rect x="430" y="60" width="150" height="70" rx="8" fill="#3b82f6" fill-opacity="0.12" stroke="#3b82f6" stroke-width="1.6"/>
&lt;text x="505" y="88" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">gateway L7&lt;/text>
&lt;text x="505" y="106" text-anchor="middle" font-size="9.5" fill="currentColor">routing por modelo&lt;/text>
&lt;text x="505" y="120" text-anchor="middle" font-size="9.5" fill="currentColor">rate-limit · auth&lt;/text>
&lt;rect x="630" y="60" width="150" height="70" rx="8" fill="#22c55e" fill-opacity="0.12" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="705" y="88" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">front de chat&lt;/text>
&lt;text x="705" y="106" text-anchor="middle" font-size="9.5" fill="currentColor">UI del asistente&lt;/text>
&lt;text x="705" y="120" text-anchor="middle" font-size="9.5" fill="currentColor">expuesto al cliente&lt;/text>
&lt;path d="M170,95 L218,95" fill="none" stroke="#64748b" stroke-width="1.8" marker-end="url(#da)"/>
&lt;path d="M380,95 L428,95" fill="none" stroke="#64748b" stroke-width="1.8" marker-end="url(#da)"/>
&lt;path d="M580,95 L628,95" fill="none" stroke="#64748b" stroke-width="1.8" marker-end="url(#da)"/>
&lt;text x="194" y="150" text-anchor="middle" font-size="9" fill="currentColor">dependsOn&lt;/text>
&lt;text x="404" y="150" text-anchor="middle" font-size="9" fill="currentColor">dependsOn&lt;/text>
&lt;text x="604" y="150" text-anchor="middle" font-size="9" fill="currentColor">dependsOn&lt;/text>
&lt;/svg>
&lt;/div>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># apps/prod/gateway.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dependsOn&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-engine &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># el gateway no sirve sin motor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">embeddings&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># apps/prod/chat-front.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dependsOn&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># el front no sirve sin gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Con &lt;code>wait: true&lt;/code> en cada &lt;code>Kustomization&lt;/code>, Flux no marca &lt;code>llm-engine&lt;/code> como &lt;code>Ready&lt;/code> hasta que el pod del motor pasa su health check —lo que en el caso de vLLM significa &lt;strong>modelo cargado y endpoint respondiendo&lt;/strong>, no solo pod arrancado. Solo entonces empieza a aplicar el gateway. Esto evita la cascada de fallos clásica del despliegue manual: levantar el front primero, ver 502 porque el gateway no está, levantar el gateway, ver 503 porque el motor todavía carga el modelo. Con &lt;code>dependsOn&lt;/code> + &lt;code>wait&lt;/code>, el orden lo garantiza el reconciliador, no la memoria del operador.&lt;/p>
&lt;p>El resultado: borrar el namespace entero del asistente y dejar que Flux lo reconstruya desde git produce exactamente el mismo stack, en el mismo orden, con la misma configuración de GPU. El cluster 4×H100 deja de tener una configuración que solo conoce quien la montó, y pasa a ser una proyección reproducible de un repo que cualquiera puede auditar.&lt;/p>
&lt;h2 id="cierre">Cierre&lt;/h2>
&lt;p>GitOps no es una herramienta, es una &lt;strong>inversión de la dirección del control&lt;/strong>: en vez de empujar cambios al cluster, declaras el estado en git y dejas que un agente tire del cluster hacia él. Para un asistente LLM —seis servicios, varios namespaces, dependencias de arranque, configuración de GPU delicada— esa inversión convierte un sistema frágil y opaco en uno reproducible y auditable. Flux es el capataz que mantiene la obra idéntica al plano y deshace cualquier cambio que no pase antes por el plano. El precio es una curva de aprendizaje real y una habilidad nueva de debugging. El premio es que el cluster deja de tener secretos sobre sí mismo: todo lo que es, está escrito, firmado y reproducible en git.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/hardening-secretos-stack-llm-soberano/">Hardening y secretos del stack LLM soberano&lt;/a> — la pieza hermana: el detalle de SOPS, sealed-secrets y External Secrets, rotación de claves y modelo de amenaza del problema huevo-gallina que aquí solo enunciamos.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/siete-fases-despliegue-plataforma-llm-on-premise/">Siete fases de despliegue de una plataforma LLM on-premise&lt;/a> — GitOps es la fase F3 de ese recorrido; aquí lo desplegamos, allí se sitúa en la secuencia completa.&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 LLM on-premise&lt;/a> — pasar de &lt;code>kubectl apply&lt;/code> a git como única autoridad es el salto de nivel que este post operacionaliza.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling LLM en Kubernetes con KEDA&lt;/a> — el autoscaler convive con GitOps: KEDA ajusta réplicas por métrica mientras Flux mantiene el resto del estado; cómo no se pelean.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">Canary, blue-green y shadow para modelos LLM&lt;/a> — la promoción progresiva que image automation dispara y que el git revert respalda como rollback de último recurso.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">Kubelet resource managers en RKE2 y NUMA&lt;/a> — los &lt;code>nodeSelector&lt;/code> y la topología de los pods GPU que aquí declaramos como código tienen su contrapartida en la política del kubelet.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/compartir-gpu-time-slicing-mps-mig/">Compartir una GPU: time-slicing, MPS y MIG&lt;/a> — los perfiles MIG y el &lt;code>--gpu-memory-utilization&lt;/code> que en este post son datos de un overlay; allí, la mecánica de por qué.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>OpenGitOps — principios de GitOps — &lt;a href="https://opengitops.dev/">opengitops.dev&lt;/a>&lt;/li>
&lt;li>Flux installation — &lt;a href="https://fluxcd.io/flux/installation/">fluxcd.io/flux/installation&lt;/a>&lt;/li>
&lt;li>Flux Kustomization — &lt;a href="https://fluxcd.io/flux/components/kustomize/kustomizations/">fluxcd.io/flux/components/kustomize/kustomizations&lt;/a>&lt;/li>
&lt;li>helm-controller — &lt;a href="https://github.com/fluxcd/helm-controller">github.com/fluxcd/helm-controller&lt;/a>&lt;/li>
&lt;li>Flux Image Policies — &lt;a href="https://fluxcd.io/flux/components/image/imagepolicies/">fluxcd.io/flux/components/image/imagepolicies&lt;/a>&lt;/li>
&lt;li>Flux Image Update Automations — &lt;a href="https://fluxcd.io/flux/components/image/imageupdateautomations/">fluxcd.io/flux/components/image/imageupdateautomations&lt;/a>&lt;/li>
&lt;li>Automate image updates to Git — &lt;a href="https://fluxcd.io/flux/guides/image-update/">fluxcd.io/flux/guides/image-update&lt;/a>&lt;/li>
&lt;li>SOPS — &lt;a href="https://github.com/getsops/sops">github.com/getsops/sops&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>