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_TOKENy usa la caché de Buildx contype=ghapara 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
startupProbeyreadinessProbede 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:
- El código de la aplicación.
- La definición de la imagen del contenedor.
- El contrato de los manifiestos de despliegue.
- 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=ghaes 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:
startupProbeprotege un arranque inicial lento.readinessProbedetermina si el pod puede recibir tráfico.livenessProbedetecta 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:
- ¿Qué artefacto desplegamos?
- ¿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:
- Documentación de GitHub: Publicación de imágenes Docker con GitHub Actions
- Documentación de GitHub: Uso del registro de contenedores
- Documentación de Docker: Backend de caché de GitHub Actions
- Documentación de Helm: Uso de registros basados en OCI
- Documentación de Argo CD: Política de sincronización automática
- Documentación de Argo CD Image Updater: Métodos de actualización
- External Secrets Operator: Obtención de información de varios secretos
- External Secrets Operator: Reescritura de claves de
dataFrom - API de External Secrets Operator:
ExternalSecret - Documentación de Kubernetes: Configuración de probes de liveness, readiness y startup
- Documentación de Amazon EKS: Roles de IAM para cuentas de servicio
- Documentación de Sigstore: Firma de contenedores con Cosign
- SLSA: Disponibilidad general de SLSA 3 Container Generator para GitHub Actions
- Documentación de GitHub: Atestaciones de artefactos y procedencia de builds
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.