Módulo 5: Gitops With Argocd

5. Anatomía de una `Application` de ArgoCD

Descripción

Tienes las dos piezas —Gitea (lección 3) y ArgoCD (lección 4)— corriendo, pero todavía sin conocerse: ArgoCD no sabe que andes-cargo-k8s existe, y Gitea no tiene ni idea de que algo llamado ArgoCD está corriendo en el mismo clúster. El único objeto que falta es un Application — el recurso central de ArgoCD, el mismo que cicd-and-gitops-on-aws-guide M7.3 te mostró verificado contra documentación oficial pero nunca aplicado. Esta lección diseca ese objeto campo por campo, con el YAML real que vas a aplicar en la lección 6 — y cierra con evidencia literal, ya ejecutada en este mismo laboratorio, de lo que pasa cuando selfHeal está activo.

Conexión con el módulo

Esta lección es la bisagra entre instalar las piezas (lecciones 3-4) y conectarlas (lección 6). Sin entender qué significa cada campo de Application, aplicar el manifiesto de la lección 6 sería copiar YAML sin criterio — con esta lección, cada línea de ese manifiesto tiene una razón que puedes explicar sin volver a leerla.


El recurso completo, el mismo que vas a aplicar en la lección 6

# application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: andes-cargo-status-api
  namespace: argocd
spec:
  project: default
  source:
    repoURL: http://gitea-http.gitea.svc.cluster.local:3000/andes-cargo/andes-cargo-k8s.git
    targetRevision: main
    path: .
  destination:
    server: https://kubernetes.default.svc
    namespace: andes-cargo
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=false

Este no es un ejemplo genérico — es, literalmente, el archivo que vas a aplicar en la lección 6, con el repoURL exacto del repositorio que creaste en la lección 3. Vale la pena leerlo con el mismo criterio con el que lees un resource de Terraform o un Deployment: cada bloque de nivel superior responde una pregunta distinta.


metadata: dónde vive el propio objeto Application

metadata:
  name: andes-cargo-status-api
  namespace: argocd

Aquí está el primer detalle que sorprende a quien ve esto por primera vez: el Application vive en el namespace argocd, no en andes-cargo. Un Application es un objeto de control —le pertenece a ArgoCD, es la instrucción que ArgoCD sigue—, distinto de los recursos que ese Application administra (el Deployment, el Service, etc., que sí van a vivir en andes-cargo, según declara destination.namespace más abajo). Es la misma separación que ya conoces entre HorizontalPodAutoscaler (vive en el namespace de la carga de trabajo que escala) y un ClusterRole (no vive en ningún namespace, porque es un recurso de alcance de clúster) — cada objeto de Kubernetes vive donde tiene sentido para quién lo administra, no necesariamente junto a lo que describe.


spec.source: de dónde viene el estado deseado

source:
  repoURL: http://gitea-http.gitea.svc.cluster.local:3000/andes-cargo/andes-cargo-k8s.git
  targetRevision: main
  path: .
  • repoURL — el nombre DNS interno de Gitea (lección 3), no localhost. Es la corrección exacta que la lección 3 ya adelantó en sus "Errores comunes": ArgoCD corre dentro del clúster, así que necesita la dirección que resuelve desde ahí, no la que resuelve desde tu máquina.
  • targetRevision — la rama, tag o commit que ArgoCD sigue. main significa "lo último que exista en esa rama, siempre" — cada vez que ArgoCD sondea el repositorio (lección 2), vuelve a preguntar "¿qué hay en main ahora mismo?", no "¿qué había en main cuando apliqué este Application?".
  • path — la carpeta, dentro del repositorio, que contiene los manifiestos. . significa "la raíz misma" — los nueve archivos YAML que subiste en la lección 3 están todos ahí, sin subcarpetas. Un repositorio más grande, con varias aplicaciones, normalmente usaría subcarpetas distintas (apps/andes-cargo-status-api/, apps/otro-servicio/) y un Application por cada una, apuntando a su propio path.

spec.destination: a dónde se aplica

destination:
  server: https://kubernetes.default.svc
  namespace: andes-cargo
  • server — la API de Kubernetes contra la que ArgoCD va a aplicar los manifiestos. https://kubernetes.default.svc es una dirección especial: significa "el mismo clúster donde ArgoCD está corriendo", resuelta por el DNS interno de Kubernetes sin que tengas que escribir ninguna IP ni ningún kubeconfig adicional. ArgoCD también puede administrar clústeres externos al suyo propio (un patrón real llamado hub-and-spoke, donde un solo ArgoCD central administra varios clústeres) — fuera del alcance de esta guía, pero vale la pena saber que el campo existe para eso.
  • namespaceandes-cargo, el mismo namespace que declaraste en namespace.yaml desde el Módulo 1. Cualquier manifiesto del repositorio que no declare su propio metadata.namespace explícito hereda este valor; los que sí lo declaran explícitamente (los nueve de este repositorio lo hacen, todos con namespace: andes-cargo) lo usan tal cual, sin depender de este campo.

spec.syncPolicy: sincronización manual frente a automática

Esta es la parte que más cambia el comportamiento diario de ArgoCD, y la que esta lección explica con más detalle porque no es obvia a simple vista.

syncPolicy:
  automated:
    prune: true
    selfHeal: true
  syncOptions:
    - CreateNamespace=false

Si el bloque automated no existiera (syncPolicy: {}, o directamente sin syncPolicy), el Application seguiría existiendo y comparando —el bucle de la lección 2 sigue corriendo siempre—, pero ArgoCD solo reportaría la diferencia (Sync Status: OutOfSync) sin aplicar nada por su cuenta. Alguien tendría que ejecutar argocd app sync andes-cargo-status-api (o hacer clic en "Sync" desde la UI) cada vez que quisiera que el cambio se aplicara — sincronización manual, útil cuando un equipo quiere una aprobación humana explícita antes de cada cambio, incluso viniendo de Git.

Con automated presente, como en este application.yaml, ArgoCD aplica el cambio en cuanto lo detecta, sin esperar ningún comando — sincronización automática, el modelo que el resto de este módulo usa.

Dentro de automated, dos campos booleanos que hacen cosas distintas:

  • prune: true — si un manifiesto desaparece del repositorio (alguien borra hpa.yaml y hace git push), ArgoCD borra el recurso correspondiente del clúster también. Sin prune: true, ArgoCD solo aplica lo que sigue existiendo en Git, pero nunca borra lo que ya no está — un recurso "huérfano" se quedaría corriendo indefinidamente.
  • selfHeal: true — si alguien cambia algo directamente en el clúster (kubectl scale, kubectl edit, cualquier cosa que no pase por Git), ArgoCD revierte ese cambio para que vuelva a coincidir con lo que Git declara. Es la propiedad más agresiva de las dos, y la que confirma con evidencia real la sección siguiente.

syncOptions: [CreateNamespace=false] es una decisión explícita de este laboratorio: el namespace andes-cargo ya existe (namespace.yaml, aplicado desde el Módulo 1) y sigue viviendo dentro del propio repositorio que ArgoCD sincroniza — no hace falta que ArgoCD lo cree por su cuenta antes de sincronizar el resto.


Evidencia real: selfHeal en acción, en este mismo laboratorio

Todo lo que sigue corrió de verdad, contra el Application andes-cargo-status-api ya sincronizado (el mismo YAML de arriba, aplicado como parte de la construcción de este módulo) — la lección 6 te lleva paso a paso hasta este mismo punto. Antes de la prueba, el Deployment tenía 5 réplicas, según la última convergencia registrada. Ahora, alguien —simulando a un ingeniero apurado, sin pasar por Git— ejecuta esto directamente contra el clúster:

kubectl scale deployment andes-cargo-status-api -n andes-cargo --replicas=2
kubectl get deployment andes-cargo-status-api -n andes-cargo

Qué esperar (literal, ejecutado — el cambio manual sí se aplica, por un instante):

deployment.apps/andes-cargo-status-api scaled

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
andes-cargo-status-api   2/5     5            2           79m

Kubernetes obedeció el comando manual sin protestar —kubectl scale es una operación válida contra cualquier Deployment, ArgoCD no tiene forma de "bloquearla" en el momento en que ocurre—. Lo que pasa después es la parte que demuestra selfHeal:

# esperando, sin ejecutar nada más...
kubectl get deployment andes-cargo-status-api -n andes-cargo

Qué esperar (literal, ejecutado — unos ocho segundos después, sin que nadie corrigiera nada a mano):

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
andes-cargo-status-api   5/5     5            5           79m

De vuelta a 5 réplicas, solo. El registro de eventos del propio namespace confirma la secuencia exacta:

kubectl get events -n andes-cargo --sort-by=.lastTimestamp
...   Normal    ScalingReplicaSet   deployment/andes-cargo-status-api   Scaled down replica set andes-cargo-status-api-548966dd97 from 5 to 2
...   Normal    ScalingReplicaSet   deployment/andes-cargo-status-api   Scaled up replica set andes-cargo-status-api-548966dd97 from 2 to 5

Dos eventos, en ese orden: primero el comando manual (Scaled down ... from 5 to 2), después la corrección de ArgoCD (Scaled up ... from 2 to 5) — sin ningún kubectl apply de por medio en la corrección. argocd app history también lo confirma, sin registrar ningún despliegue nuevo asociado a este evento —porque, desde el punto de vista de Git, nada cambió; el Deployment simplemente volvió a coincidir con lo que ya declaraba.

                    LO QUE selfHeal: true ACABA DE HACER

  Git dice: replicas = 5          Alguien corre:              ArgoCD detecta
  (fuente de verdad, sin          kubectl scale --replicas=2  la diferencia y
   cambios)                       (el clúster ahora dice 2)   corrige, solo
        │                                │                          │
        └────────────── comparación continua (lección 2) ───────────┘
                                          │
                                          ▼
                              El clúster vuelve a decir 5,
                              sin que nadie corriera
                              kubectl apply ni git push

Errores comunes

Asumir que syncPolicy.automated significa "ArgoCD nunca me deja tocar el clúster a mano" (de expectativa, corregido por la propia evidencia de esta lección). Qué pasa: alguien piensa que kubectl scale directamente contra un recurso administrado por ArgoCD va a fallar o va a estar bloqueado. Cómo detectarlo: la evidencia de esta lección lo contradice — el comando manual sí se aplicó, durante unos ocho segundos. Cómo corregirlo: selfHeal no impide el cambio manual en el momento en que ocurre — Kubernetes sigue aceptando cualquier comando válido contra cualquier recurso. Lo que hace es revertirlo en la próxima comparación, que en este laboratorio tardó unos ocho segundos. Un cambio manual muy breve (más rápido que el ciclo de comparación de ArgoCD) técnicamente sí llegaría a tener efecto por un instante — pero no dura.

Confundir prune: true con "borra todo lo que no reconozca" (de alcance, importa para namespaces compartidos). Qué pasa: alguien teme que prune: true borre recursos de otros equipos que compartan el mismo namespace. Cómo detectarlo: si no tienes claro qué recursos, exactamente, entran en el alcance de "lo que Git declara" para este Application. Cómo corregirlo: prune solo actúa sobre recursos que ArgoCD ya administra —los que fueron creados como parte de una sincronización anterior de este mismo Application, identificados con una etiqueta interna que ArgoCD agrega—. Un recurso que otro equipo creó a mano, sin pasar nunca por este Application, nunca entra en el alcance de prune, sin importar en qué namespace viva.

Pensar que Application vive en el mismo namespace que administra, por analogía con otros recursos (conceptual, ya explicado arriba pero fácil de olvidar). Qué pasa: alguien busca el Application con kubectl get application -n andes-cargo y no encuentra nada. Cómo detectarlo: el comando devuelve una lista vacía o un error. Cómo corregirlo: Application vive en argocd (el namespace de ArgoCD), no en el namespace que declara como destinationkubectl get application -n argocd es el comando correcto.


Ejercicios

Ejercicio 1 — Reescribe syncPolicy para sincronización manual, sin ayuda. A partir del YAML completo de esta lección, escribe la versión de syncPolicy que dejaría a ArgoCD reportando diferencias sin aplicarlas automáticamente.

Ver solución
syncPolicy:
  syncOptions:
    - CreateNamespace=false

Basta con quitar el bloque automated completo (prune/selfHeal) — sin él, ArgoCD sigue comparando (el bucle nunca se detiene) pero deja el Sync Status en OutOfSync cuando encuentra una diferencia, en vez de corregirla sola. Alguien tendría que correr argocd app sync andes-cargo-status-api explícitamente para aplicar el cambio.

Ejercicio 2 — Explica por qué Application vive en argocd, con tus propias palabras. Sin copiar el texto de esta lección, explica a un colega por qué el Application que administra recursos de andes-cargo no vive, él mismo, en el namespace andes-cargo.

Ver solución

Una explicación razonable: "El namespace de un objeto de Kubernetes normalmente refleja quién lo administra, no necesariamente qué describe. El Application es un objeto de control que le pertenece a ArgoCD — vive junto al resto de los objetos de ArgoCD (argocd), aunque lo que describe (un Deployment, un Service) viva en un namespace completamente distinto (andes-cargo). Es parecido a cómo un control remoto no vive dentro del televisor que controla."

Ejercicio 3 — Predice el resultado si prune fuera false en el escenario del Módulo 4, lección 8. Recuerda el checklist del Módulo 4: nueve manifiestos, incluidas dos NetworkPolicy. Si prune: false (en vez de true) y alguien borrara networkpolicy-default-deny.yaml del repositorio, con git push, ¿qué esperarías ver en el clúster?

Ver solución

La NetworkPolicy default-deny-ingress seguiría existiendo en el clúster, sin cambios — con prune: false, ArgoCD nunca borra un recurso que ya no aparece en Git, solo aplica lo que sí sigue estando ahí. El Sync Status probablemente mostraría una diferencia parcial (el archivo desapareció de Git, pero el recurso sigue en el clúster), sin que ArgoCD la resuelva por su cuenta — exactamente el motivo por el que este laboratorio usa prune: true: un repositorio Git que de verdad sea la única fuente de verdad necesita que borrar un archivo tenga el mismo efecto que borrar el recurso.


Resumen y siguiente paso

Esta lección diseccionó el Application completo que vas a aplicar en la lección 6: source (de dónde viene el estado deseado — el repositorio de Gitea, rama main, raíz del repositorio), destination (a dónde se aplica — el propio clúster, namespace andes-cargo), y syncPolicy (cómo se sincroniza — automática, con prune y selfHeal activos). Confirmaste, con evidencia real y no prometida, que selfHeal: true corrige un cambio manual en segundos, sin que nadie ejecute ningún comando de corrección — la propiedad de GitOps que drift.yml de cicd-and-gitops-on-aws-guide nunca implementó de forma activa.

Antes de avanzar deberías poder: explicar cada campo de nivel superior de un Application sin ayuda; distinguir sincronización manual de automática con un ejemplo de YAML; y predecir qué hace prune: true frente a prune: false ante un archivo borrado del repositorio.

Siguiente lección: manos a la obra, sincronizando andes-cargo-status-api desde Git. Ahí aplicas, tú mismo, exactamente este Application — y ves, paso a paso, la primera convergencia real del clúster completo.

Recursos

  1. Argo CD — Application Specification — referencia oficial completa de todos los campos de spec, incluidos los que esta lección no cubrió por estar fuera del alcance de este laboratorio.
  2. Argo CD — Sync Options — documentación de CreateNamespace y el resto de las opciones de syncOptions.
  3. Argo CD — Automated Sync Policy — documentación oficial de prune y selfHeal, fuente de la explicación de esta lección.
  4. cicd-and-gitops-on-aws-guide (NIEVA), Módulo 7, lección 3 — el YAML de Application original, mostrado sin ejecutar, que esta lección finalmente aplica de verdad.