Production AI Infrastructure

Del repositorio al clúster: un pipeline de despliegue end-to-end para SaaS con IA

Una guía práctica de despliegue a nivel de servicio para equipos de SaaS con IA: diseño de imágenes Docker, builds con GitHub Actions, empaquetado con Helm, transición a ArgoCD, External Secrets, IRSA y fortalecimiento de releases.

14 min de lectura

Resumen

  • Trata el repositorio del servicio como un producto: debe ser responsable de su Dockerfile, su workflow de CI, su chart de Helm y su contrato de ejecución.
  • Construye imágenes inmutables en GitHub Actions, publícalas en GHCR con GITHUB_TOKEN y usa la caché de Buildx con type=gha para agilizar los builds.
  • Si usas ArgoCD, mantén Git como fuente de verdad. Publicar una imagen en el registro no es, por sí solo, una estrategia de despliegue GitOps, a menos que agregues automatización de imágenes que escriba los cambios de vuelta en Git.
  • Usa Helm para empaquetar el servicio, External Secrets Operator para crear los secretos e IRSA para otorgar permisos de AWS sin claves de larga duración en los pods.
  • Configura startupProbe y readinessProbe de forma deliberada para servicios de IA con arranque lento que cargan modelos, preparan conexiones o ejecutan migraciones.
  • Firma los artefactos de release y publica su procedencia para que tu pipeline no solo sea rápido, sino también auditable.

Las guías modernas de despliegue suelen quedarse cortas. Explican Kubernetes, Docker o GitHub Actions, pero no la transición entre ellos. En la práctica, esa transición es donde aparecen la mayoría de las dificultades en producción.

Este artículo se centra en la capa del repositorio del servicio dentro del despliegue de un backend SaaS con IA: la parte que comienza con el código de la aplicación y termina con un pod saludable que recibe tráfico. Complementa el enfoque centrado en infraestructura de Production GitOps: Terraform, Helm, and ArgoCD for Node.js APIs and Agno Agents, al centrarse en un solo servicio desplegable.

El ejemplo está inspirado en una plataforma ficticia de SaaS con IA, pero las recomendaciones operativas que siguen se contrastaron con la documentación oficial el March 24, 2026.

El servicio que vamos a desplegar

Supongamos que tenemos un servicio backend dentro de una plataforma más amplia:

pulse-api/
├── app/
│   ├── main.py
│   ├── agents/
│   ├── api/
│   ├── db/
│   └── config.py
├── charts/pulse-api/
├── .github/workflows/
├── Dockerfile
├── compose.yaml
├── pyproject.toml
└── scripts/entrypoint.sh

Este servicio es responsable de cuatro cosas:

  1. El código de la aplicación.
  2. La definición de la imagen del contenedor.
  3. El contrato de los manifiestos de despliegue.
  4. La automatización que convierte un commit en un artefacto listo para publicar.

Ese límite de responsabilidad importa. Cuando el mismo repositorio define la imagen de ejecución, los endpoints de salud y el esquema de valores de Helm, reduces la cantidad de lugares donde el despliegue puede desviarse silenciosamente de lo esperado.

1. Construye un contenedor para lograr repetibilidad, no solo para que funcione en local

Para un backend SaaS con IA, el contenedor tiene que hacer más que «ejecutar Python». Necesita una resolución predecible de dependencias, un manejo seguro del sistema de archivos y un proceso de arranque que refleje las condiciones de producción.

Un buen Dockerfile de servicio suele optimizar tres cosas:

  • Versiones estables de las herramientas.
  • Capas de build eficientes.
  • Ejecución sin privilegios de root.
FROM python:3.12-slim

COPY --from=ghcr.io/astral-sh/uv:0.6.6 /uv /uvx /bin/

WORKDIR /app
ENV PYTHONPATH=/app

RUN apt-get update && apt-get install -y --no-install-recommends \
    git curl libcairo2 libpango-1.0-0 && \
    rm -rf /var/lib/apt/lists/*

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

COPY . .

RUN groupadd -r pulse && useradd -r -g pulse -d /app pulse && \
    chown -R pulse:pulse /app
USER pulse

EXPOSE 8000

ENTRYPOINT ["bash", "/app/scripts/entrypoint.sh"]
CMD ["uv", "run", "python", "app/main.py"]

Las ideas principales son sencillas:

  • Copia los manifiestos de dependencias antes que el código de la aplicación para que las instalaciones de dependencias permanezcan más tiempo en caché.
  • Fija las versiones de las herramientas de build en lugar de depender de latest.
  • Elimina los privilegios de root en la imagen para que Kubernetes pueda aplicar runAsNonRoot.

En los servicios de IA, también me gusta mantener el entrypoint pequeño y explícito. Si necesitas crear un archivo de clave privada, convertir una variable de entorno en un archivo o crear un directorio que solo se use durante la ejecución, hazlo en un único lugar auditable:

#!/bin/bash
set -e

if [ -n "$SNOWFLAKE_PRIVATE_KEY" ]; then
  mkdir -p "$(dirname "$SNOWFLAKE_PRIVATE_KEY_PATH")"
  printf '%s\n' "$SNOWFLAKE_PRIVATE_KEY" > "$SNOWFLAKE_PRIVATE_KEY_PATH"
  chmod 600 "$SNOWFLAKE_PRIVATE_KEY_PATH"
  unset SNOWFLAKE_PRIVATE_KEY
fi

exec "$@"

Ese tipo de script es aburrido, y aburrido es exactamente lo que quieres en el código de arranque de producción.

2. CI debe producir artefactos inmutables

Un repositorio de servicio suele necesitar tres flujos de automatización:

  • pull_request: comprobar que la rama compila y, opcionalmente, publicar una imagen de vista previa.
  • main: publicar la imagen de integración que se usa en desarrollo o staging.
  • tag: publicar una imagen de release y los metadatos asociados.

La documentación de GitHub muestra el patrón estándar para GHCR: iniciar sesión con docker/login-action, usar ${{ secrets.GITHUB_TOKEN }} y otorgar packages: write al job. Es más claro que introducir un token de acceso personal para publicar desde el mismo repositorio.

name: Build Main

on:
  push:
    branches: [main]

permissions:
  contents: read
  packages: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/acme/pulse-api:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Vale la pena conservar dos detalles del borrador original:

  • La caché de Buildx con type=gha es una opción predeterminada práctica para los runners alojados por GitHub.
  • Los builds para una sola plataforma son suficientes cuando tu clúster solo ejecuta linux/amd64.

Pero hay una mejora que haría de inmediato:

No permitas que etiquetas mutables de larga duración como development o latest se conviertan en la identidad principal de tu despliegue. Publícalas si ayudan a las personas, pero haz que tu sistema de despliegue converja en una etiqueta o un digest inmutable.

¿Por qué? Porque las referencias inmutables a imágenes facilitan mucho los rollbacks, la depuración, el seguimiento de la procedencia y la revisión de incidentes. La documentación del registro de contenedores de GitHub también explica explícitamente cómo descargar una imagen por digest cuando necesitas una imagen exacta.

3. Helm empaqueta el contrato de ejecución

El trabajo de Helm no consiste en «crear plantillas de Kubernetes porque YAML es molesto». Su verdadero trabajo es definir el contrato entre el servicio y cada entorno.

Estructura típica de un chart:

charts/pulse-api/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── serviceaccount.yaml
    ├── externalsecret.yaml
    ├── pdb.yaml
    └── hpa.yaml

La plantilla de Deployment debe dejar claras algunas cosas:

  • Qué imagen se ejecuta.
  • Qué variables de entorno que no son secretas están configuradas en Git.
  • Qué secretos se inyectan durante la ejecución.
  • Qué probes determinan el arranque y la disponibilidad para recibir tráfico.
  • Qué configuraciones de seguridad debe cumplir el pod.
containers:
  - name: pulse-api
    image: {{ include "pulse-api.image" . }}
    env:
      - name: PORT
        value: {{ .Values.service.port | quote }}
    envFrom:
      - secretRef:
          name: {{ include "pulse-api.fullname" . }}
    startupProbe:
      httpGet:
        path: /health
        port: http
      failureThreshold: 30
      periodSeconds: 5
    readinessProbe:
      httpGet:
        path: /health
        port: http
      periodSeconds: 10
    securityContext:
      runAsNonRoot: true
      allowPrivilegeEscalation: false
      capabilities:
        drop: [ALL]

Helm también facilita la publicación de charts como artefactos OCI. La documentación oficial de Helm admite helm push hacia una referencia de registro oci://, con el nombre y la etiqueta del chart inferidos a partir de los metadatos del chart empaquetado.

Si a tu equipo le gusta un modelo de release en el que la imagen de la aplicación y el chart avanzan juntos, los charts almacenados en registros OCI encajan bien.

4. La transición a GitOps: ArgoCD debe seguir a Git

Esta es la corrección de auditoría más importante que hice al adaptar el borrador original.

Es común describir un flujo como este:

CI builds image -> image lands in registry -> ArgoCD notices -> cluster updates

Ese no es el modelo básico de GitOps.

La documentación de ArgoCD es clara: la sincronización automática ocurre cuando ArgoCD detecta una diferencia entre los manifiestos deseados en Git y el estado real del clúster. El patrón previsto es que CI cambie Git y que ArgoCD reconcilie el estado a partir de ahí.

Por eso, el flujo más claro es:

commit code
  -> CI builds image
  -> CI writes immutable image tag or digest into Git-tracked manifests
  -> ArgoCD detects Git change
  -> ArgoCD syncs cluster

Si quieres promover imágenes a partir de cambios en el registro, agrega una capa dedicada como Argo CD Image Updater y configúrala para escribir los cambios de vuelta en Git. Su documentación admite explícitamente el método de escritura git para aplicaciones de Helm y Kustomize.

Esa distinción importa porque conserva el principal beneficio de GitOps: cualquiera puede inspeccionar Git y saber exactamente qué se supone que debe ejecutar el clúster.

5. Secretos: descubrimiento dinámico y acceso con privilegios mínimos

Para un backend SaaS moderno con IA, los secretos suelen incluir:

  • credenciales de base de datos
  • claves de API de proveedores
  • secretos de clientes OAuth
  • tokens de observabilidad
  • claves privadas

El patrón que me gusta usar aquí es:

source of truth -> cloud secret manager -> External Secrets Operator -> Kubernetes Secret -> pod env

External Secrets Operator admite spec.dataFrom.find para obtener varios secretos en un solo Secret de Kubernetes, y su función de reescritura puede combinar contenidos JSON con la estrategia Extract. Esto hace que el descubrimiento basado en etiquetas sea realmente útil para agrupar secretos por servicio.

Ejemplo:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
spec:
  refreshInterval: 10m
  secretStoreRef:
    name: pulse-api
    kind: SecretStore
  target:
    name: pulse-api
  dataFrom:
    - find:
        tags:
          service-pulse-api: enabled
      rewrite:
        - merge:
            strategy: Extract
            into: ""

Por qué este patrón escala:

  • La plantilla de despliegue no necesita una línea nueva por cada secreto.
  • La rotación de secretos ocurre fuera de la imagen de la aplicación.
  • El operador puede actualizar el Secret de Kubernetes periódicamente.

Pero no te quedes solo con la inyección de secretos. Tu pod también necesita permisos de AWS para acceder a los recursos que utiliza. En EKS, IRSA sigue siendo una excelente opción predeterminada para esto. AWS documenta el modelo con claridad: asocia un rol de IAM con una cuenta de servicio de Kubernetes para que los pods puedan obtener credenciales de AWS con permisos delimitados, sin incluir credenciales de larga duración en el contenedor.

Eso te da una postura de seguridad mucho mejor que distribuir AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY por todo tu entorno.

6. El arranque, la salud y las migraciones necesitan reglas diferentes

Los servicios de IA suelen tardar en arrancar por motivos legítimos:

  • inicialización del pool de conexiones a la base de datos
  • calentamiento de caché
  • inicialización de clientes de modelos o embeddings
  • comprobaciones de integración
  • migraciones de esquema

Kubernetes distingue estas funciones por una razón:

  • startupProbe protege un arranque inicial lento.
  • readinessProbe determina si el pod puede recibir tráfico.
  • livenessProbe detecta un proceso bloqueado más adelante en el ciclo de vida del contenedor.

La documentación de Kubernetes recomienda explícitamente configurar startupProbe con un valor de failureThreshold * periodSeconds lo bastante largo para cubrir el peor tiempo de arranque posible. También aclara que, si falla una probe de readiness, el pod deja de recibir tráfico del Service sin que se termine su ejecución.

Para backends de IA, eso suele significar:

  • ventanas de arranque amplias
  • comprobaciones de readiness estrictas
  • probes de liveness conservadoras

¿Qué pasa con ejecutar migraciones durante el arranque?

Aquí es donde la ingeniería práctica y la pureza arquitectónica suelen entrar en conflicto.

Ejecutar migraciones al arrancar la aplicación resulta atractivo porque reduce la cantidad de componentes operativos. Publicas la imagen, arrancas el contenedor, dejas que actualice el esquema y empiezas a atender tráfico.

Eso puede ser perfectamente razonable para un equipo pequeño y un modelo de despliegue con pocos cambios simultáneos.

Pero tiene sus costos:

  • varias réplicas pueden competir por ejecutar las migraciones
  • las migraciones largas prolongan el tiempo de arranque
  • la seguridad del rollout queda estrechamente ligada al comportamiento de los cambios de esquema

Mi regla práctica es:

  • Etapa temprana: las migraciones durante el arranque pueden ser aceptables si su alcance es pequeño y se entiende bien.
  • Etapa posterior: pasa a un job de migración dedicado o aplica un bloqueo consultivo en la base de datos para que un solo actor se encargue de ejecutar los cambios de esquema.

El borrador original iba bien encaminado al describir esto como una concesión a favor de la simplicidad. Yo solo evitaría presentarlo como la opción predeterminada universal.

7. Fortalecimiento del release: firma y procedencia

Un pipeline de despliegue maduro debe responder dos preguntas:

  1. ¿Qué artefacto desplegamos?
  2. ¿Podemos demostrar cómo se construyó?

Para la primera, firma los artefactos de release. Cosign de Sigstore admite la firma de contenedores sin claves mediante credenciales efímeras respaldadas por OIDC, incluidas las identidades de GitHub.

Para la segunda, publica la procedencia. El proyecto SLSA documenta builders y generadores de GitHub Actions para producir procedencia firmada en workflows de contenedores, y GitHub también documenta las atestaciones de artefactos para acreditar la procedencia del build.

Eso significa que un flujo de release apto para producción puede verse así:

git tag v1.5.0
  -> build image
  -> push image
  -> package chart
  -> sign image
  -> publish provenance / attestation
  -> update deployment manifests
  -> ArgoCD sync

No necesitas todo eso desde el primer día. Pero si esperas tener clientes empresariales, operar en entornos regulados o incluso responder un futuro cuestionario de seguridad, es mucho más fácil incorporar la procedencia desde temprano que hacerlo retroactivamente.

8. Un flujo práctico end-to-end

Esta es la versión del pipeline que realmente recomendaría para la mayoría de los equipos:

Developer merges PR
  -> GitHub Actions builds image
  -> Push image to GHCR with immutable tag
  -> CI updates Helm values or Argo CD Image Updater writes back to Git
  -> ArgoCD detects Git change
  -> External Secrets Operator refreshes referenced secrets as needed
  -> New pod starts
  -> startupProbe gives it time to initialize
  -> readinessProbe flips healthy
  -> Service sends traffic

Eso te da:

  • imágenes reproducibles
  • intención de despliegue visible en Git
  • separación clara entre configuración y secretos
  • acceso más seguro a la nube mediante la identidad de la cuenta de servicio
  • menos misterios del tipo «¿por qué se reinició este pod durante el rollout?»

9. Qué conservaría y qué cambiaría

El borrador original tenía buenas intuiciones. Sin duda conservaría estas ideas:

  • un repositorio por servicio responsable de su aplicación, imagen, chart y workflows
  • contenedores sin privilegios de root por defecto
  • versiones fijas de las herramientas
  • descubrimiento de secretos basado en etiquetas
  • un workflow de release dedicado para los builds promovidos

Pero después de contrastarlo con el proyecto en su conjunto y la documentación actual, cambiaría estos puntos:

  • Presentar ArgoCD como un sistema basado en Git, no en el registro de imágenes por defecto.
  • Preferir etiquetas de imagen inmutables o digests en lugar de depender de etiquetas flotantes como identidad del despliegue.
  • Tratar las migraciones durante el arranque como una concesión consciente por conveniencia, no como una arquitectura permanente.
  • Presentar Cosign y la procedencia como medidas para fortalecer los releases a nivel de artefacto, no solo como «pasos adicionales para después».

Eso convierte la guía de una buena historia ficticia en un patrón de producción más sólido.

Fuentes

Verificado el March 24, 2026 con la documentación primaria:

Este artículo utiliza una empresa y nombres de repositorios ficticios para dar un contexto concreto al despliegue. Los ejemplos específicos de arquitectura son ilustrativos, mientras que las recomendaciones sobre plataformas y workflows se basan en la documentación enlazada.