GitOps del stack de inferencia con Flux: operar el asistente como código
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 ingesta documental para RAG que llena el vector store que aquí desplegamos, el servicio de embeddings y rerankers con TEI que es uno de los servicios que GitOps gestiona, y el hardening de secretos del stack soberano, 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.
TL;DR
Un asistente LLM en producción es un sistema distribuido, no un binario. Cuenta, como mínimo, con un motor de inferencia (vLLM o similar), un gateway L7 que enruta por modelo y aplica rate-limit, un front de chat, un vector store para RAG, un servicio de embeddings/reranker, y una pila de observabilidad. 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 —kubectl apply por aquí, helm upgrade por allá— produce un cluster cuyo estado real nadie sabe reconstruir: no hay registro de qué se aplicó, ni cuándo, ni por qué. GitOps resuelve esto con cuatro principios (OpenGitOps): el estado deseado es declarativo, está versionado e inmutable en git, se aplica automáticamente mediante un agente (nadie hace SSH para desplegar) y se reconcilia continuamente comparando el estado real contra el declarado. Flux 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 Kustomization o el HelmRelease, con interval (cada cuánto reconcilia), prune (borra lo que ya no está en git), dependsOn (ordena el arranque) y health checks. El interval fija el MTTR del drift: con interval=1m, 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 git revert. GitOps no es gratis: la curva de aprendizaje es real y debuggear por qué una Kustomization no reconcilia es una habilidad nueva.
La analogía: el plano maestro y el capataz que no negocia
Imagina una obra grande con un plano maestro firmado y archivado, y un capataz 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 única fuente de verdad: para cambiar la obra, no se toca la obra, se cambia el plano —y el capataz se encarga del resto en su siguiente ronda.
Esa es exactamente la mecánica de GitOps. Git es el plano maestro: 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). Flux es el capataz: cada interval recorre el cluster, compara contra git y converge. Y aquí está la lección que separa GitOps de “tener los YAML en un repo”: el capataz deshace los cambios manuales. Si un operador entra con kubectl edit 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 proyección reproducible de git. Borra el cluster entero, apunta un Flux nuevo al mismo repo, y la obra se reconstruye idéntica.
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 secretos —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 pieza de hardening desarrolla a fondo.
El problema: un asistente son seis servicios, no uno
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:
| Servicio | Función | Namespace típico | Estado |
|---|---|---|---|
| Motor de inferencia (vLLM) | Sirve los tokens del LLM general y el de código | llm-serving | Stateless (modelo desde object store) |
| Embeddings + reranker (TEI) | Vectoriza queries y reordena candidatos para RAG | llm-serving | Stateless |
| Vector store (Qdrant) | Almacena y busca los embeddings de documentos | data | Stateful (PVC) |
| Gateway L7 (Envoy AI Gateway / LiteLLM) | Routing por modelo, rate-limit, auth | gateway | Semi-stateless |
| Front de chat | UI del asistente | apps | Stateless |
| Observabilidad (Langfuse, OTel Collector, dashboards) | Trazas, métricas y logs LLM-aware | observability | Stateful (Postgres de Langfuse) |
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 lleno, y todo emite trazas a observabilidad. Desplegar esto a mano implica recordar el orden, los valores de cada helm install, los ConfigMap, los Secret, los nodeSelector 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.
Los seis controladores de Flux
Flux no es un binario monolítico, sino un conjunto de controladores que cooperan —el GitOps Toolkit. Una instalación por defecto trae cuatro; los dos de image automation se añaden con --components-extra (instalación de Flux):
- source-controller: clona y mantiene actualizadas las fuentes —repos git (
GitRepository), repos Helm (HelmRepository), buckets, artefactos OCI—. Es quien “tiene el plano en la mano”: expone el contenido del repo como un artefacto interno que los demás consumen. - kustomize-controller: toma un artefacto de fuente, renderiza Kustomize (bases + overlays) y aplica el resultado al cluster. Es el responsable de
prune,dependsOny los health checks de lasKustomization. - helm-controller: reconcilia objetos
HelmRelease—instala y actualiza charts de Helm de forma declarativa, sin que nadie ejecutehelmdesde una terminal (helm-controller). - notification-controller: el puente con el exterior en ambos sentidos. Recibe webhooks (para reconciliar al instante en cada push, en vez de esperar al
interval) y emite eventos/alertas a Slack, a un chat interno o a un sistema de incidencias. - image-reflector-controller: escanea registries de contenedores y guarda los tags encontrados en una base de datos interna. Es los “ojos” de la image automation.
- image-automation-controller: 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.
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.
El diagrama tiene tres recorridos. El gris es el flujo principal: git → source-controller → kustomize/helm-controller → cluster. El rojo discontinuo es el bucle de reconciliación que se ejecuta cada interval: compara estado real contra deseado, corrige el drift y poda lo que sobra. El morado es la image automation: el reflector ve un tag nuevo en el registry, la policy decide si cumple, y el automation-controller escribe de vuelta a git. Ese último recorrido es el que cierra el círculo y convierte git en un sistema que se actualiza solo.
Estructura de repo: clusters, infrastructure, apps
La convención más extendida separa tres niveles de responsabilidad. No es la única, pero envejece bien:
gitops-repo/
├── clusters/
│ └── prod/
│ ├── infrastructure.yaml # Kustomization → ./infrastructure
│ └── apps.yaml # Kustomization → ./apps (dependsOn infrastructure)
├── infrastructure/
│ ├── controllers/ # ingress, cert-manager, GPU operator, KEDA...
│ └── configs/ # ClusterIssuer, RuntimeClass, StorageClass...
└── apps/
├── base/ # manifiestos comunes a todos los entornos
│ ├── llm-engine/ # HelmRelease vLLM
│ ├── gateway/ # HelmRelease del gateway L7
│ ├── chat-front/ # Deployment + Service + Ingress
│ ├── vector-store/ # HelmRelease Qdrant (+ PVC)
│ └── observability/ # HelmRelease Langfuse + OTel
├── staging/ # overlay: réplicas bajas, modelo pequeño
└── prod/ # overlay: réplicas altas, modelo grande, MIG
La pieza clave es la separación base / overlay de Kustomize. La base/ describe el servicio una sola vez; cada overlay (staging/, prod/) aplica un patch con las diferencias del entorno: número de réplicas, tamaño del modelo, perfil MIG, nodeSelector, --gpu-memory-utilization. Esto evita duplicar manifiestos completos por entorno. La carpeta clusters/ es el punto de entrada que Flux reconcilia primero: contiene las Kustomization raíz que apuntan a infrastructure/ y apps/, con un dependsOn que garantiza que la infraestructura (CRDs, operadores, storage classes) esté lista antes que las aplicaciones.
Cuántos manifiestos: el cálculo que justifica los overlays
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:
$$ \text{archivos}_{\text{naïve}} = N \times M \times k $$
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$:
$$ 6 \times 3 \times 5 = 90 \text{ archivos completos, cada uno mantenido por separado.} $$
Con la estructura base/overlay, la base se escribe una vez y cada overlay solo contiene el patch de las diferencias (típicamente 1 fichero corto por servicio por entorno):
$$ \text{archivos}{\text{overlay}} = \underbrace{N \times k}{\text{base}} + \underbrace{N \times M}_{\text{patches}} = 6 \times 5 + 6 \times 3 = 30 + 18 = 48 $$
No es solo que sean menos archivos (48 frente a 90): es que el grueso del cambio se hace en un solo sitio. Cambiar el límite de memoria del motor para todos los entornos es una edición en base/, 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.
El bucle de reconciliación: desired vs actual
El corazón de Flux es un bucle de control que no tiene fin: cada interval, lee el estado deseado (git), lee el estado real (cluster), calcula la diferencia y la aplica. Es el mismo principio de un termostato: lee la temperatura objetivo (git), lee la temperatura real (cluster) y enciende o apaga la caldera hasta que coinciden. No “despliega una vez”; converge para siempre.
Un manifiesto Kustomization de las aplicaciones, con las piezas que importan (referencia Kustomization):
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 1m # reconcilia cada minuto: fija el MTTR del drift
path: ./apps/prod # overlay de producción
prune: true # borra del cluster lo que se quita de git
sourceRef:
kind: GitRepository
name: gitops-repo
dependsOn:
- name: infrastructure # no aplica apps hasta que infra esté Ready
wait: true # espera a que los recursos estén healthy
timeout: 5m # falla la reconciliación si no converge en 5 min
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: gateway
namespace: gateway
Cuatro campos hacen el trabajo pesado:
intervaldefine cada cuánto se ejecuta la ronda del capataz. Es el determinante directo del MTTR del drift (siguiente sección).prune: trueactiva 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. Sinprune, un servicio retirado del repo sigue corriendo —el plano y la obra divergen en silencio.dependsOnordena el grafo: Flux no aplica los manifiestos de unaKustomizationhasta que todas las referenciadas tengan estadoReady: True. Es el mecanismo que garantiza datos → gateway → front.wait+healthChecks: conwait: true, Flux monitoriza todos los recursos aplicados y espera a que estén listos antes de marcar la reconciliación como exitosa;healthCheckspermite afinar exactamente qué recursos vigilar. Esto es lo que hace que undependsOnsignifique algo: la dependencia no está satisfecha hasta que está sana, no solo aplicada.
MTTR y detección de drift en función del interval
El interval 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:
$$ \mathbb{E}[t_{\text{detección}}] = \frac{T}{2} $$
A esto se le suma el tiempo de corrección $t_c$ (renderizar, hacer diff, aplicar), normalmente unos segundos. El MTTR del drift queda:
$$ \text{MTTR}_{\text{drift}} = \frac{T}{2} + t_c $$
Con interval=1m 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 interval=10m, 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 interval=10s 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 Kustomization reconciliando cada 10 s, el kube-apiserver y el image-reflector empiezan a notar la presión. La regla práctica: interval corto (1m) para las aplicaciones críticas cuyo drift duele, interval largo (10–30m) para infraestructura estable que casi nunca cambia, y webhooks del notification-controller para reconciliación instantánea en cada push —así el interval solo gobierna el drift, no la latencia de despliegue.
Helm como código: el HelmRelease del motor de inferencia
Para el motor, el gateway y el front, lo habitual es empaquetarlos como charts de Helm y declararlos con HelmRelease. El helm-controller los reconcilia sin que nadie ejecute helm jamás:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: llm-engine
namespace: llm-serving
spec:
interval: 10m
chart:
spec:
chart: vllm
sourceRef:
kind: HelmRepository
name: vllm-charts
values:
image:
tag: "0.8.4" # gestionado por image automation (ver abajo)
replicaCount: 2
nodeSelector:
nvidia.com/gpu.product: H100-SXM
resources:
limits:
nvidia.com/gpu: 1
extraArgs:
- "--gpu-memory-utilization=0.90"
- "--tensor-parallel-size=1"
Todo lo que en el mundo imperativo sería un flag de vllm serve —--gpu-memory-utilization, --tensor-parallel-size— 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 compartir una GPU es directa: el perfil MIG y el --gpu-memory-utilization son parámetros de despliegue, y aquí son código.
Image automation: subir el tag sin tocar nada a mano
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 tag en el HelmRelease. La image automation de Flux lo hace sola, con tres objetos (automate image updates):
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: vllm
namespace: flux-system
spec:
image: registry.example.local/inference/vllm
interval: 5m # cada 5 min escanea los tags del registry
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: vllm
namespace: flux-system
spec:
imageRepositoryRef:
name: vllm
policy:
semver:
range: ">=0.8.0 <0.9.0" # solo parches y minors dentro de 0.8.x–0.8.x
---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: vllm
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: gitops-repo
git:
commit:
author:
name: fluxbot
email: fluxbot@example.local
messageTemplate: "auto: bump vllm to {{ .NewTag }}"
El reparto de papeles: ImageRepository escanea todos los tags del repositorio de la imagen y los guarda en la base de datos interna del reflector. ImagePolicy lee esos tags y elige el “último” según la política —el campo policy es obligatorio y define cómo se selecciona (ImagePolicy). ImageUpdateAutomation toma el tag elegido, edita el manifiesto en el repo (donde haya un marcador # {"$imagepolicy": "flux-system:vllm"}) y hace commit (ImageUpdateAutomation).
La elección de política importa:
- semver: interpreta los tags como versiones semánticas y elige la más alta que cumpla el rango (
range: ">=0.8.0 <0.9.0"). Por defecto excluye prereleases (0.8.0-rc.1no 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. - regex / alphabetical / numerical: para esquemas de tags que no son semver puros —por ejemplo
main-<sha>-<timestamp>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.
Una pauta de seguridad: en lugar de que ImageUpdateAutomation haga push directo a main, se le configura para que escriba a una rama y abra un PR. 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 canary, blue-green y shadow.
Secretos en GitOps: el problema huevo-gallina
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 —en git solo va el secreto cifrado; el cluster tiene la llave para descifrarlo:
- SOPS + age/KMS: cifras los valores sensibles de un manifiesto con SOPS (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.
- sealed-secrets: un controlador en el cluster tiene una clave privada. Cifras el secreto contra su clave pública (con
kubeseal), elSealedSecretcifrado va a git, y el controlador lo descifra a unSecretnormal dentro del cluster. La clave privada nunca sale del cluster. - External Secrets + Vault: en git no va ni siquiera el secreto cifrado, solo una referencia (
ExternalSecret) que dice “el valor de esta clave está en Vault, en esta ruta”. 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.
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 pieza hermana de hardening entra en el detalle de cada opción, la rotación de claves y el modelo de amenaza.
Promoción y rollback: git revert es el botón de pánico
La consecuencia más elegante de tener el cluster como proyección de git es que el rollback es un git revert. 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 (≤ interval, 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.
Esto encaja con las estrategias de promoción del post de canary, blue-green y shadow: Flux gestiona qué está declarado, y la herramienta de rollout progresivo gestiona cómo se mueve el tráfico 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 main.
La costura: GitOps no es gratis
Sería deshonesto vender GitOps como magia. Tiene costes reales que cualquier equipo paga:
Curva de aprendizaje. El modelo declarativo es un cambio de mentalidad. El operador que lleva años arreglando incidentes con kubectl edit tiene que desaprender el reflejo: ahora el cluster le revierte los cambios y eso, al principio, parece que Flux “le pelea”. Entender que el cambio se hace en git, no en el cluster, lleva semanas de incomodidad.
Debuggear al reconciliador. Cuando una Kustomization 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 dependsOn? ¿El timeout saltó antes de que el modelo grande terminara de cargar? Diagnosticar esto requiere flux get, flux logs, leer los status.conditions de los objetos Flux y entender en qué eslabón se atascó. Es una habilidad nueva, distinta de debuggear Kubernetes “a secas”.
Timeouts y cargas lentas. El timeout de una Kustomization con wait: true 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 Kustomization con timeout: 2m, aunque todo esté bien. Calibrar timeouts por servicio es trabajo fino.
El drift que sí querías. A veces el operador necesita un cambio temporal urgente y Flux se lo revierte. La respuesta correcta —suspender la reconciliación de esa Kustomization con flux suspend, 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.
Ninguno de estos costes anula el beneficio. Pero un equipo que adopta GitOps esperando que “todo sea más fácil desde el día uno” se frustra. Es más fácil a partir del mes dos, cuando la auditabilidad, la reproducibilidad y el rollback trivial ya han pagado la curva.
Aplicado al cluster genérico 4×H100
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.
nodeSelectors y perfiles MIG como código. El overlay de prod 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 compartir una GPU, pero ahora versionado:
# apps/prod/llm-engine-patch.yaml
spec:
values:
nodeSelector:
nvidia.com/gpu.product: H100-SXM
extraArgs:
- "--gpu-memory-utilization=0.92"
- "--tensor-parallel-size=1"
---
# apps/prod/embeddings-patch.yaml — sobre un slice MIG
spec:
values:
nodeSelector:
nvidia.com/mig.config: "3g.40gb"
resources:
limits:
nvidia.com/mig-3g.40gb: 1
El perfil MIG (3g.40gb), la fracción de VRAM (0.92), 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 nvidia-smi mig a mano que nadie registra.
Orden de arranque con dependsOn. El grafo de dependencias del asistente se codifica con dependsOn encadenados. El orden correcto es datos → embeddings/motor → gateway → front:
# apps/prod/gateway.yaml
spec:
dependsOn:
- name: llm-engine # el gateway no sirve sin motor
- name: embeddings
# ...
---
# apps/prod/chat-front.yaml
spec:
dependsOn:
- name: gateway # el front no sirve sin gateway
Con wait: true en cada Kustomization, Flux no marca llm-engine como Ready hasta que el pod del motor pasa su health check —lo que en el caso de vLLM significa modelo cargado y endpoint respondiendo, 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 dependsOn + wait, el orden lo garantiza el reconciliador, no la memoria del operador.
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.
Cierre
GitOps no es una herramienta, es una inversión de la dirección del control: 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.
Ver también
- Hardening y secretos del stack LLM soberano — 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.
- Siete fases de despliegue de una plataforma LLM on-premise — GitOps es la fase F3 de ese recorrido; aquí lo desplegamos, allí se sitúa en la secuencia completa.
- Cinco niveles de madurez de la plataforma LLM on-premise — pasar de
kubectl applya git como única autoridad es el salto de nivel que este post operacionaliza. - Autoscaling LLM en Kubernetes con KEDA — el autoscaler convive con GitOps: KEDA ajusta réplicas por métrica mientras Flux mantiene el resto del estado; cómo no se pelean.
- Canary, blue-green y shadow para modelos LLM — la promoción progresiva que image automation dispara y que el git revert respalda como rollback de último recurso.
- Kubelet resource managers en RKE2 y NUMA — los
nodeSelectory la topología de los pods GPU que aquí declaramos como código tienen su contrapartida en la política del kubelet. - Compartir una GPU: time-slicing, MPS y MIG — los perfiles MIG y el
--gpu-memory-utilizationque en este post son datos de un overlay; allí, la mecánica de por qué.
Referencias
- OpenGitOps — principios de GitOps — opengitops.dev
- Flux installation — fluxcd.io/flux/installation
- Flux Kustomization — fluxcd.io/flux/components/kustomize/kustomizations
- helm-controller — github.com/fluxcd/helm-controller
- Flux Image Policies — fluxcd.io/flux/components/image/imagepolicies
- Flux Image Update Automations — fluxcd.io/flux/components/image/imageupdateautomations
- Automate image updates to Git — fluxcd.io/flux/guides/image-update
- SOPS — github.com/getsops/sops