StackPractices
advanced Por Mathias Paulenko

Guía Completa de GitOps en Producción

Implementá GitOps con ArgoCD o Flux en producción: reconciliación pull, multi-cluster, despliegue progresivo, secretos con SOPS e integración CI/CD.

Introducción

Alguien hace un hotfix a un Deployment de producción con kubectl edit a las 2 AM, el cambio no queda registrado en ningún lado, y el próximo deploy programado lo borra silenciosamente. GitOps existe para eliminar esa clase de problemas: Git se convierte en el único lugar donde vive el estado deseado, y un agente dentro del cluster lo descarga y aplica — nadie toca el cluster directamente.

Esta guía recorre un setup de GitOps en producción de punta a punta: instalación y configuración de ArgoCD o Flux, estructura del repositorio GitOps, gestión de secretos con SOPS, despliegue progresivo con rollouts canary, rollback automático y la integración con tu pipeline de CI. Todo lo que sigue es YAML funcional que podés adaptar — para profundizar en ArgoCD específicamente, mirá la Guía Completa de GitOps con ArgoCD.

Principios de GitOps

GitOps, según lo define el grupo de trabajo OpenGitOps, descansa sobre cuatro propiedades. Todo lo demás en esta guía se deriva de ellas:

Principios de GitOps:

1. Declarativo — El estado del sistema se describe declarativamente en Git
2. Versionado — Git guarda el historial completo del estado deseado
3. Pull — Los agentes descargan los cambios desde Git y los aplican (no push)
4. Reconciliación continua — Los agentes comparan constantemente el estado real vs el deseado

Beneficios:
  - Auditoría: cada cambio es un commit de Git con autor y timestamp
  - Rollback: volver a cualquier estado anterior con git revert
  - Detección de drift: los agentes detectan y corrigen cambios manuales
  - Seguridad: no hace falta acceso directo al cluster para desplegar
  - Colaboración: los PRs habilitan code review para cambios de infraestructura

El modelo pull es la parte que los equipos subestiman al principio. Un pipeline push necesita credenciales del cluster en CI — comprometer el CI significa comprometer el cluster. Con pull, las credenciales nunca salen del cluster; el agente solo necesita acceso de lectura a un repo Git.

Cuándo usar GitOps

GitOps vale la pena cuando:

  • Corrés múltiples clusters o entornos. Templar la misma app en us-east-1, eu-west-1 y staging con un solo ApplicationSet le gana a scriptear loops de kubectl.
  • La auditoría y el compliance importan. Cada cambio de producción se convierte en un PR revisado con autor y timestamp — ese es tu log de auditoría.
  • Necesitás rollbacks confiables. git revert más auto-sync es un mecanismo de rollback que no tenés que construir ni probar aparte.
  • Los cambios manuales siguen causando incidentes. selfHeal convierte “¿quién tocó esto?” en un no-evento: el agente lo revierte y lo reporta.
  • Los equipos despliegan de forma independiente. Cada equipo es dueño de sus manifiestos vía PRs sin tener credenciales del cluster.

GitOps no encaja en: clusters hobby de un solo desarrollador donde el overhead del agente supera al beneficio, y workflows muy imperativos (bases de datos con failover manual, jobs únicos) que no se mapean a un estado deseado declarativo. Forzarlos a entrar en GitOps crea más problemas de los que resuelve.

Cómo funciona GitOps

El pipeline se divide limpiamente en dos: CI es dueño del repo de aplicación y produce artefactos inmutables; el agente GitOps es dueño del estado del cluster y lee un repo de configuración separado.

flowchart diagram: Repo de app<br/>código fuente

El loop de reconciliación es el mecanismo clave: el agente descarga el estado deseado del repo GitOps en cada intervalo (30s–5m según configuración), lo compara con el estado vivo del cluster y aplica la diferencia. Un kubectl edit manual aparece como drift en la próxima reconciliación — con self-heal activado se revierte automáticamente; sin self-heal solo se reporta como OutOfSync.

Tener dos repos — uno para el código de la aplicación y otro para los manifiestos de despliegue — es deliberado. El código de la app cambia con cada feature; la config del cluster solo debería cambiar en releases intencionales. Además le permite a CI escribir en el repo de config sin que el agente del cluster necesite acceso de escritura a nada.

Setup de ArgoCD

ArgoCD es un controlador que corre dentro del cluster y expone una UI web, una API y un CLI. Es la mejor opción cuando varios equipos necesitan visibilidad del estado de sync, o cuando querés un solo plano de control gestionando muchos clusters.

Instalación de ArgoCD

# Instalar ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Obtener la contraseña de admin
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# Port forward para acceder a la UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Instalar el CLI de ArgoCD
brew install argocd  # macOS
# o
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd && mv argocd /usr/local/bin/

La URL del manifiesto stable siempre apunta a la última release — fijalo a un tag versionado (v3.x.y/manifests/install.yaml) en producción para que los upgrades sean deliberados y no un efecto colateral de una reinstalación.

Manifiesto Application

Un Application es la unidad de trabajo de ArgoCD: un path del repo, un destino, una política de sync.

# ArgoCD Application para el servicio API
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-service
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io  # Borrado en cascada
spec:
  project: production

  source:
    repoURL: https://github.com/myorg/k8s-manifests
    targetRevision: main
    path: production/api-service

  destination:
    server: https://kubernetes.default.svc
    namespace: production

  syncPolicy:
    automated:
      prune: true       # Borra recursos eliminados de Git
      selfHeal: true    # Revierte cambios manuales
      allowEmpty: false # Evita borrar todos los recursos
    syncOptions:
      - CreateNamespace=true
      - PruneLast=true
      - ApplyOutOfSyncOnly=true
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

  revisionHistoryLimit: 10  # Guarda las últimas 10 revisiones de sync

Tres campos hacen el trabajo pesado:

  • prune: true borra los recursos vivos que ya no existen en Git — sin esto, los manifiestos eliminados quedan para siempre.
  • selfHeal: true es la reconciliación de drift real; sin esto ArgoCD reporta OutOfSync pero deja el cambio manual en su lugar.
  • allowEmpty: false es tu cinturón de seguridad: si el path del repo queda vacío por accidente (un rename mal hecho, un commit vacío), ArgoCD se niega a vaciar el namespace en vez de podarlo todo.

revisionHistoryLimit: 10 importa para el rollback — delimita hasta dónde puede llegar argocd app rollback sin tocar Git.

AppProject para multi-equipo

AppProject es la frontera de RBAC: desde qué repos puede desplegar un equipo, a qué namespaces puede apuntar y qué objetos a nivel cluster puede tocar.

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: production
  namespace: argocd
spec:
  description: Aplicaciones del entorno de producción

  # Repositorios de origen permitidos
  sourceRepos:
    - https://github.com/myorg/k8s-manifests
    - https://github.com/myorg/api-config

  # Namespaces de destino permitidos
  destinations:
    - server: https://kubernetes.default.svc
      namespace: production
    - server: https://kubernetes.default.svc
      namespace: production-*

  # Recursos a nivel cluster permitidos
  clusterResourceWhitelist:
    - group: ''
      kind: Namespace
    - group: rbac.authorization.k8s.io
      kind: RoleBinding

  # Recursos denegados
  clusterResourceBlacklist:
    - group: ''
      kind: Node
    - group: ''
      kind: PersistentVolume

  # Roles para acceso por equipo
  roles:
    - name: platform-team
      description: Acceso del equipo de plataforma
      policies:
        - p, proj:production:platform-team, applications, get, production/*, allow
        - p, proj:production:platform-team, applications, sync, production/*, allow
      groups:
        - platform-team

La separación whitelist/blacklist es la parte importante: los equipos de plataforma pueden gestionar sus propios namespaces y RBAC, pero no pueden crear Nodes ni PersistentVolumes — el radio de impacto de un Application malo queda dentro del proyecto.

ApplicationSet para multi-cluster

ApplicationSet genera un Application por destino, así que “desplegar api-service en todos los clusters de prod” es un solo manifiesto en vez de N copias.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: api-all-clusters
  namespace: argocd
spec:
  generators:
    - list:
        elements:
          - cluster: us-east-1
            url: https://1.2.3.4
          - cluster: eu-west-1
            url: https://5.6.7.8
          - cluster: ap-southeast-1
            url: https://9.10.11.12
  template:
    metadata:
      name: 'api-{{cluster}}'
    spec:
      project: production
      source:
        repoURL: https://github.com/myorg/k8s-manifests
        targetRevision: main
        path: production/api-service
      destination:
        server: '{{url}}'
        namespace: production
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

El generador list es el más simple; los generadores cluster y git cubren casos dinámicos (un Application por secret de cluster registrado, o por directorio del repo). Para overrides de valores por cluster, combiná ApplicationSet con overlays de Kustomize en vez de bifurcar el árbol de manifiestos.

Setup de Flux

Flux es un conjunto de controladores componibles (source, kustomize, helm, notification) sin UI. Encaja en equipos que viven en la CLI, que quieren la menor huella posible, o que ya estandarizaron en Helm y Kustomize.

Instalación de Flux

# Instalar el CLI de Flux
brew install fluxcd/tap/flux  # macOS

# Bootstrap de Flux con GitHub
flux bootstrap github \
  --owner=myorg \
  --repository=fleet-infra \
  --branch=main \
  --path=clusters/production \
  --personal=false

# Verificar la instalación de Flux
flux check
flux get kustomizations
flux get helmreleases

flux bootstrap es en sí mismo GitOps: commitea los manifiestos de Flux en tu repo, así que los upgrades de Flux son PRs como todo lo demás. El comando es idempotente — corrélo de nuevo para actualizar Flux en el lugar.

Flux Kustomization

Un Kustomization ata un path del repo a un loop de reconciliación — el equivalente en Flux al Application de ArgoCD.

# clusters/production/api-service.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: api-service
  namespace: flux-system
spec:
  interval: 1m  # Reconcilia cada minuto
  path: ./production/api-service
  sourceRef:
    kind: GitRepository
    name: k8s-manifests
  prune: true  # Borra recursos eliminados de Git
  wait: true   # Espera a que los recursos estén listos
  timeout: 5m
  targetNamespace: production

  # Health checks
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: api
      namespace: production
    - apiVersion: v1
      kind: Service
      name: api-service
      namespace: production

  # Sustitución de variables post-build
  postBuild:
    substitute:
      cluster_name: production
      cluster_region: us-east-1

wait: true junto con healthChecks mejora el modo de fallo: un sync no cuenta como “aplicado” hasta que el Deployment reporta ready de verdad, así que una imagen rota aparece como un Kustomization fallido en vez de pods degradados en silencio. postBuild.substitute inyecta valores por cluster sin duplicar manifiestos — combinalo con placeholders ${cluster_name} en el YAML de origen.

Flux GitRepository

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: k8s-manifests
  namespace: flux-system
spec:
  interval: 30s
  url: https://github.com/myorg/k8s-manifests
  ref:
    branch: main
  secretRef:
    name: github-deploy-key  # Para repos privados

Flux HelmRelease

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: redis
  namespace: production
spec:
  interval: 5m
  chart:
    spec:
      chart: redis
      version: "18.x"
      sourceRef:
        kind: HelmRepository
        name: bitnami
        namespace: flux-system
  values:
    architecture: replication
    auth:
      enabled: true
      existingSecret: redis-secret
      existingSecretPasswordKey: redis-password
    replica:
      replicaCount: 3
      persistence:
        enabled: true
        size: 10Gi
    metrics:
      enabled: true
      serviceMonitor:
        enabled: true

Fijá el chart a un rango de versión mayor (18.x) en vez de un * a secas — seguís recibiendo parches a través del historial de Git, pero un bump de major con breaking changes no puede salir sin revisión.

ArgoCD o Flux

ArgoCDFlux
UIUI web completa + API + CLISolo CLI (las UIs son de terceros)
Multi-clusterIntegrado, un solo plano de controlInstalación por cluster (o vía Flux sharding)
Modelo de appsCRDs Application / ApplicationSetCRDs Kustomization / HelmRelease / GitRepository
Despliegue progresivoArgo RolloutsFlagger
HuellaMás pesado (API server, UI, repo server)Controladores mínimos
RBACA nivel proyecto, granularSolo RBAC de Kubernetes

Elegí ArgoCD cuando varios equipos necesitan visibilidad compartida y RBAC por app, o cuando gestionás muchos clusters desde un solo panel. Elegí Flux para equipos CLI-first, integración ajustada con Helm, o mínimo overhead del plano de control. Muchas organizaciones corren ambos — Flux para la infraestructura de plataforma, ArgoCD para los equipos de aplicación que quieren la UI.

Estructura del repositorio GitOps

Cómo estructures el repo de config determina cuánto dolerá el trabajo multi-entorno. Una estructura que escala:

k8s-manifests/
├── base/                    # Manifiestos compartidos (base de kustomize)
│   └── api-service/
│       ├── deployment.yaml
│       └── service.yaml
├── production/              # Overlays por entorno
│   ├── api-service/
│   │   └── kustomization.yaml   # tag de imagen, réplicas, host de ingress
│   └── secrets/
│       └── api-secrets.yaml     # cifrado con SOPS
├── staging/
│   └── api-service/
│       └── kustomization.yaml
└── clusters/                # Bootstrap de Flux / wiring por cluster
    └── production/
        └── api-service.yaml

Dos reglas que funcionan en la práctica:

  • Un repo GitOps por entorno (o cluster), no por app. Una única fuente de verdad solo funciona si “la verdad” está en un solo lugar; un repo por app la dispersa.
  • Overlays antes que branches. Una branch staging que mergea a main para promover suena atractiva y produce drift constante entre branches. Un directorio por entorno con overlays de Kustomize mantiene todos los entornos visibles a la vez.

Gestión de secretos con SOPS

Los secretos en plaintext en Git son un no rotundo — el historial de Git es para siempre. Las opciones compatibles con GitOps:

  • SOPS + age/KMS: cifra los archivos de secretos del lado del cliente, commitealos, y dejá que el agente los descifre dentro del cluster. Funciona con Flux nativamente y con ArgoCD vía plugins.
  • External Secrets Operator: mantené los secretos en AWS Secrets Manager, Vault o Key Vault; ESO los sincroniza a Secrets de Kubernetes. La mejor opción cuando ya existe un gestor de secretos.
  • Sealed Secrets: cifrado contra una clave pública específica del cluster; el archivo sellado es seguro de commitear. Simple, pero la rotación es incómoda.

SOPS es el punto de partida más liviano:

# Instalar SOPS y age
brew install sops age

# Generar el par de claves age
age-keygen -o sops-key.txt
# Clave pública: age1xxxxx...
# Clave privada: AGE-SECRET-KEY-1xxxxx...

# Cifrar un archivo de secretos
sops --encrypt --age age1xxxxx... --in-place secret.yaml

# Descifrar un archivo de secretos
sops --decrypt secret.yaml

# Editar un archivo cifrado en el lugar
sops secret.yaml

# Rotar claves
sops updatekeys secret.yaml
# Secreto cifrado (SOPS)
apiVersion: v1
kind: Secret
metadata:
  name: api-secrets
  namespace: production
type: Opaque
stringData:
  DB_PASSWORD: ENC[AES256_GCM,data:abc123,iv:xxx,tag:yyy,type:str]
  API_KEY: ENC[AES256_GCM,data:def456,iv:xxx,tag:yyy,type:str]
sops:
  age:
    - recipient: age1xxxxx...
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        ...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2026-07-04T12:00:00Z"
  mac: ENC[AES256_GCM,data:mac,iv:xxx,tag:yyy,type:str]
# Kustomization de Flux con descifrado SOPS
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: api-secrets
  namespace: flux-system
spec:
  interval: 1m
  path: ./production/secrets
  sourceRef:
    kind: GitRepository
    name: k8s-manifests
  prune: true
  decryption:
    provider: sops
    secretRef:
      name: sops-age-key  # Secret que contiene la clave privada

La clave privada de age vive como un Secret del cluster (sops-age-key) — nunca en Git. Rotarla implica sops updatekeys en cada archivo cifrado más un Secret nuevo en el cluster; scripteá eso antes de necesitarlo a las 3 AM. Si necesitás almacenamiento centralizado de secretos más allá de Kubernetes, el External Configuration Store Pattern cubre ese lado.

Despliegue progresivo

El despliegue progresivo acota el radio de impacto de una release mala: enrutá una fracción del tráfico a la versión nueva, mirá la tasa de errores y la latencia, y promové o revertí automáticamente según las métricas.

flowchart diagram: Deploy v2

Argo Rollouts

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: api
  namespace: production
spec:
  replicas: 10
  strategy:
    canary:
      canaryService: api-canary
      stableService: api-stable
      trafficRouting:
        nginx:
          stableIngress: api-stable-ingress
      steps:
        - setWeight: 10
        - pause: { duration: 2m }
        - setWeight: 25
        - pause: { duration: 2m }
        - setWeight: 50
        - pause: { duration: 5m }
        - setWeight: 75
        - pause: { duration: 5m }
      analysis:
        templates:
          - templateName: success-rate
        startingStep: 2  # Empieza el análisis después del 25% canary
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          # CI fija el tag inmutable en cada release — nunca "latest"
          image: ghcr.io/myorg/api:v1.4.2
          ports:
            - containerPort: 8000
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 5
            periodSeconds: 10
# Template de análisis para canary
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
  namespace: production
spec:
  metrics:
    - name: success-rate
      interval: 1m
      successCondition: result[0] >= 0.99
      failureLimit: 3  # Aborta después de 3 fallos
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{status!~"5.."}[2m]))
            /
            sum(rate(http_requests_total[2m]))

El AnalysisTemplate es lo que convierte esto en una quality gate en vez de un load balancer sofisticado: se consulta Prometheus cada minuto, y tres checks fallidos abortan el rollout. Ajustá successCondition a una señal en la que realmente alertás — un umbral de éxito del 99% con ventana de 1m detecta roturas obvias rápido; las regresiones sutiles necesitan pasos más largos y una métrica de latencia p95 al lado.

Flux Flagger

apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: api
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  service:
    port: 80
    targetPort: 8000
    gateways:
      - public-gateway.istio-system.svc.cluster.local
    hosts:
      - api.stackpractices.com
  analysis:
    interval: 1m
    threshold: 5
    maxWeight: 50
    stepWeight: 10
    metrics:
      - name: request-success-rate
        thresholdRange:
          min: 99
        interval: 1m
      - name: request-duration
        thresholdRange:
          max: 500
        interval: 30s
    webhooks:
      - name: load-test
        type: rollout
        url: http://flagger-loadtester.test/
        timeout: 5s
        metadata:
          cmd: "hey -z 1m -q 10 -c 2 http://api.production:80/"

Flagger mueve stepWeight (10%) en cada interval (1m) hasta maxWeight, verificando la tasa de éxito y la duración p99 en cada paso. El webhook load-test importa más de lo que parece: sin tráfico de base, un canary que recibe 2 requests por minuto produce ruido en las métricas, no señal.

Rollback automático

Con GitOps, el rollback es una operación de Git — ese es el punto. Hay dos caminos y conviene saber cuándo aplica cada uno:

  • git revert + push (preferido). El agente sincroniza el cluster al estado revertido. El rollback en sí queda como historial auditable y revisable — el mismo workflow que cualquier cambio.
  • Rollback puntual de ArgoCD (emergencia). argocd app rollback fija la app a una revisión anterior en el cluster sin commit de Git. Es más rápido, pero salta el rastro de auditoría — seguilo con un git revert para que Git y el cluster converjan de nuevo.
# Application de ArgoCD — ajustes de historial que habilitan el rollback
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-service
  namespace: argocd
spec:
  revisionHistoryLimit: 10  # argocd app rollback puede volver hasta estas revisiones
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
# Rollback en ArgoCD — usá el ID de historial, no un SHA de Git
argocd app history api-service            # encontrá el ID de revisión al que volver
argocd app rollback api-service <id>      # fija esa revisión

# Flux: rollback revirtiendo el commit de Git
git revert <commit-sha>
git push origin main
# Flux sincroniza automáticamente al estado revertido en la próxima reconciliación

Ojo con la trampa del rollback con selfHeal: si revertís fijando una revisión mientras selfHeal está activo, ArgoCD ve el estado fijado como drift respecto a Git y te pelea el cambio. Desactivá el auto-sync primero (argocd app set api-service --sync-policy none), o hacé el git revert y dejá que el sync normal se encargue.

Integración con CI/CD

El trabajo de CI en GitOps es construir el artefacto y actualizar el repo GitOps — nunca tocar el cluster.

# GitHub Actions: build y actualización del repo GitOps
name: Deploy
on:
  push:
    branches: [main]

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

      - name: Build y push de la imagen
        run: |
          docker build -t ghcr.io/myorg/api:${{ github.sha }} .
          echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
          docker push ghcr.io/myorg/api:${{ github.sha }}

      - name: Actualizar el repo GitOps
        run: |
          git clone https://github.com/myorg/k8s-manifests.git
          cd k8s-manifests

          # Actualizar el tag de imagen en el manifiesto
          sed -i "s|ghcr.io/myorg/api:.*|ghcr.io/myorg/api:${{ github.sha }}|" production/api-service/deployment.yaml

          git config user.name "CI Bot"
          git config user.email "ci@stackpractices.com"
          git add .
          git commit -m "Deploy api:${{ github.sha }}"
          git push

Notas para endurecer ese workflow en producción:

  • Tagueá las imágenes con ${{ github.sha }} (o semver), nunca latest. GitOps solo funciona si el estado deseado en Git es exacto — latest es estado mutable escondido fuera de Git.
  • Preferí kustomize edit set image api=ghcr.io/myorg/api:${{ github.sha }} sobre sed cuando tus manifiestos sean Kustomize; entiende la sintaxis y no puede corromper un manifiesto.
  • Con muchas apps, salteá el sed artesanal y usá ArgoCD Image Updater o los CRDs ImagePolicy/ImageRepository de Flux — el propio agente escribe los commits de tags en el repo.

Código complementario: manifiestos GitOps para producción: todos los YAML de esta guía — ArgoCD, Flux, SOPS, rollouts y el workflow de CI.

Buenas prácticas

  • Separá el repo de la app del repo de config. CI pushea tags de imagen al repo de config; el agente solo lo lee. Mezclarlos crea una tormenta de commits en el repo que leen tus auditores.
  • Tags de imagen inmutables en todos lados. Un tag v1.4.2 en Git es un hecho; latest es un rumor. El rollback, la detección de drift y la promoción entre entornos asumen un estado deseado exacto.
  • Gateá los syncs por health, no solo por apply. wait: true + healthChecks en Flux, sync waves + health assessment en ArgoCD — un sync que devuelve éxito mientras los pods crashean es peor que un fallo visible.
  • Usá sync waves para dependencias ordenadas. Anotá los recursos con argocd.argoproj.io/sync-wave (CRDs y RBAC primero, apps al final) para que un entorno nuevo converja en el orden correcto.
  • Limitá revisionHistoryLimit deliberadamente. 10 es un default sano — suficiente margen de rollback, huella chica en etcd.
  • Ensayá la rotación de claves. sops updatekeys, Secret nuevo en el cluster, re-cifrado — si nunca lo hiciste, la primera vez va a ser durante un incidente.

Errores comunes

  • selfHeal: true + allowEmpty sin definir. Un accidente con el path vacío y el agente poda el namespace. Siempre definí allowEmpty: false en sync policies automatizadas.
  • Secretos commiteados en plaintext “temporalmente”. Git recuerda. Si alguna vez tocó el repo sin cifrar, rotalo — la fuga ya pasó.
  • Intervalos de reconciliación demasiado agresivos. interval: 10s × 200 aplicaciones = carga constante sobre el API server y el proveedor de Git. Empezá en 1–5m y ajustá solo donde la latencia importe.
  • Podar CRDs o namespaces compartidos. prune: true en un Application dueño de objetos a nivel cluster puede borrar recursos de los que dependen otras apps. Mantené los objetos cluster-scoped en un Application separado, sin prune.
  • Dejar que CI haga kubectl apply “solo esta vez”. Cada apply directo es drift que el agente va a revertir o a ocultar. Sin excepciones — así se mantiene el rastro de auditoría significativo.
  • Promoción de entornos via merge de branches. Staging→prod mergeando branches produce dos historiales divergentes. Promové copiando el tag de imagen (o el cambio de overlay) al path de prod vía PR.

Solución de Problemas

Fallos de sync que se repiten en producción, y cómo leerlos:

Application clavada en OutOfSync después de correr sync. Corré argocd app diff api-service — el diff suele mostrar un campo inmutable (spec.selector de un Deployment no puede cambiar en el lugar) o un admission webhook rechazando el manifiesto. Se arregla borrando el objeto para que el agente lo recree, o sumando la sync option Replace=true para ese recurso.

El borrado en cascada se cuelga para siempre. El resources-finalizer bloquea el borrado del Application mientras un recurso gestionado está trabado (loop de finalizers, namespace inalcanzable). Como último recurso, quitá el finalizer: kubectl -n argocd patch app api-service --type=json -p='[{"op":"remove","path":"/metadata/finalizers"}]' — los recursos huérfanos te tocan a vos.

El Kustomization de Flux muestra Ready=False pero los pods están Running. wait: true más healthChecks verifica los objetos exactos listados — un Deployment renombrado falla el check aunque la carga esté sana. kubectl -n flux-system describe kustomization api-service muestra qué objeto hizo timeout.

Errores de descifrado SOPS después de rotar claves. O el Secret del cluster con la clave privada de age no se actualizó, o el secretRef del Kustomization sigue apuntando al nombre viejo. Compará kubectl -n flux-system get secret sops-age-key contra la línea recipient del archivo cifrado — tienen que coincidir.

ApplicationSet generó apps para los clusters equivocados. El generador list o cluster matcheó más de lo previsto. Inspeccioná los nombres generados con kubectl -n argocd get applications antes de activar prune — un generador mal configurado a escala no solo crea apps, las borra.

See Also

Lectura Adicional

Preguntas frecuentes

¿Cuál es la diferencia entre ArgoCD y Flux?

Ambos implementan GitOps para Kubernetes. ArgoCD trae una UI web, multi-cluster desde un solo plano de control y ApplicationSets para templating. Flux es más liviano, es puros CRDs y tiene integración nativa más ajustada con Helm y Kustomize. Elegí ArgoCD si los equipos necesitan una UI y multi-cluster centralizado. Elegí Flux para workflows CLI-driven y mínima huella.

¿Cómo funciona la reconciliación de drift?

El agente compara continuamente el estado vivo del cluster con el estado deseado en Git. Cuando detecta una diferencia — un kubectl edit manual, un recurso borrado — reaplica el estado de Git. Con selfHeal: true (ArgoCD) o prune: true + reconciliación automatizada (Flux), los cambios manuales se revierten solos; sin self-heal, el drift solo se reporta como OutOfSync.

¿Cómo manejo los secretos en GitOps?

Nunca commitees secretos en plaintext. Cifrá los archivos del lado del cliente con SOPS (age o AWS KMS) y commiteá el cifrado — el agente los descifra dentro del cluster con una clave privada guardada como Secret del cluster. O mantené los secretos fuera de Git con External Secrets Operator (lee de AWS Secrets Manager, Vault, Key Vault) o Sealed Secrets. Detalles completos en Gestión de secretos con SOPS.

¿Qué es el despliegue progresivo en GitOps?

Sacar una versión nueva a una fracción del tráfico — 10%, 25%, 50% — mientras checks de métricas automatizados corren en cada paso, en vez de actualizar todos los pods a la vez. Argo Rollouts y Flagger lo implementan: si el check de tasa de éxito o latencia falla, el tráfico vuelve a la versión estable. Es una quality gate, no solo traffic shaping.

¿Debería usar un repo Git o varios para GitOps?

Un repo de config por cluster o entorno, y repos separados para el código de aplicación. Un solo repo GitOps mantiene visible el estado deseado completo y hace los cambios atómicos; el código de la app vive en otro lado para que CI pueda actualizar tags de imagen sin que el agente necesite escritura sobre el código. Ver Estructura del repositorio GitOps.

¿Cómo hago rollback de un deployment en GitOps?

Preferí git revert + push — el agente sincroniza al estado revertido y el rollback queda como historial auditado. En emergencias, argocd app rollback <app> <id> fija una revisión anterior al instante (el ID sale de argocd app history), pero seguilo con un revert para que Git se ponga al día. Detalles y la trampa del self-heal en Rollback automático.

¿Cómo actualiza CI el repo GitOps?

CI construye y pushea la imagen, clona el repo de config, actualiza el tag de imagen (kustomize edit set image o un sed templado), commitea y pushea. El agente toma el cambio en la próxima reconciliación. ArgoCD Image Updater o el ImagePolicy de Flux automatizan el commit del tag por completo.

¿GitOps funciona para bases de datos y cargas stateful?

Parcialmente. Desplegar el StatefulSet y su config vía GitOps funciona bien; las migraciones de esquema y las decisiones de failover no se mapean bien a sync declarativo y suelen quedar como jobs de CI u operators. La regla general: si la operación es "converger a este estado", GitOps encaja; si es "correr este procedimiento", dejala fuera del loop de sync.