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:

ServicioFunciónNamespace típicoEstado
Motor de inferencia (vLLM)Sirve los tokens del LLM general y el de códigollm-servingStateless (modelo desde object store)
Embeddings + reranker (TEI)Vectoriza queries y reordena candidatos para RAGllm-servingStateless
Vector store (Qdrant)Almacena y busca los embeddings de documentosdataStateful (PVC)
Gateway L7 (Envoy AI Gateway / LiteLLM)Routing por modelo, rate-limit, authgatewaySemi-stateless
Front de chatUI del asistenteappsStateless
Observabilidad (Langfuse, OTel Collector, dashboards)Trazas, métricas y logs LLM-awareobservabilityStateful (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, dependsOn y los health checks de las Kustomization.
  • helm-controller: reconcilia objetos HelmRelease —instala y actualiza charts de Helm de forma declarativa, sin que nadie ejecute helm desde 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 bucle de reconciliación de Fluxgitplano maestroestado deseadosource-controllerclona el repo · artefactokustomize-controllerrenderiza overlays · aplicahelm-controllerreconcilia HelmReleaseclusterestado realmotor · gatewayfront · vector storeobservabilidadreconcile cada interval: compara real vs deseado, corrige drift, pruneimage automationreflector escanea registrypolicy elige tag · commitregistry: tag nuevoescribe de vueltaa git (commit)

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:

  • interval define cada cuánto se ejecuta la ronda del capataz. Es el determinante directo del MTTR del drift (siguiente sección).
  • prune: true 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 prune, un servicio retirado del repo sigue corriendo —el plano y la obra divergen en silencio.
  • dependsOn ordena el grafo: Flux no aplica los manifiestos de una Kustomization hasta que todas las referenciadas tengan estado Ready: True. Es el mecanismo que garantiza datos → gateway → front.
  • wait + healthChecks: con wait: true, Flux monitoriza todos los recursos aplicados y espera a que estén listos antes de marcar la reconciliación como exitosa; healthChecks permite afinar exactamente qué recursos vigilar. Esto es lo que hace que un dependsOn signifique 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.1 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.
  • 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), el SealedSecret cifrado va a git, y el controlador lo descifra a un Secret normal 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:

Orden de arranque del asistente vía dependsOndatosvector storePostgres Langfusemotor + embeddingsvLLM (GPU entera)TEI (slice MIG)gateway L7routing por modelorate-limit · authfront de chatUI del asistenteexpuesto al clientedependsOndependsOndependsOn
# 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

Referencias