Módulo 5: Gitops With Argocd

8. Proyecto: un cambio en Git, reflejado solo

Descripción

Este proyecto cierra el módulo con la prueba que todas las lecciones anteriores prepararon: un cambio real, subido a Git con git push, reflejado en andes-cargo-cluster sin que nadie ejecute kubectl apply para nada que no sea observar. No es un cambio de juguete —vas a subir las réplicas de andes-cargo-status-api de 3 a 5, un cambio de negocio real, del tipo que un equipo de verdad haría un martes cualquiera—, y vas a medir, con timestamps reales, cuánto tarda en converger. Como bono, este proyecto documenta un hallazgo real que apareció sin planearlo mientras se escribía esta lección: el HorizontalPodAutoscaler del Módulo 3 y el replicas que declara Git terminan compitiendo por el mismo campo — y la corrección de producción real que resuelve esa tensión, verificada con evidencia literal.

Conexión con el módulo

Este proyecto usa, sin ningún cambio, las siete lecciones anteriores: Gitea (lección 3) como repositorio, ArgoCD (lección 4) como operador, el Application ya sincronizado (lecciones 5-6), y la distinción precisa entre escalado y RollingUpdate (lección 7) que hace posible describir, con exactitud, qué mecanismo estás viendo aquí.


Paso 1 — Confirma el estado de partida

kubectl get pods -n andes-cargo -l app=andes-cargo-status-api
kubectl get deployment andes-cargo-status-api -n andes-cargo -o jsonpath='{.spec.replicas} desired, {.status.readyReplicas} ready{"\n"}'

Qué esperar (literal, ejecutado — nombres de Pod con sufijo hash son tu valor variable; el conteo de tres réplicas es literal en este punto del módulo, heredado de la lección 6):

NAME                                      READY   STATUS    RESTARTS   AGE
andes-cargo-status-api-548966dd97-bpqjq   1/1     Running   0          76m
andes-cargo-status-api-548966dd97-ls2bc   1/1     Running   0          76m
andes-cargo-status-api-548966dd97-z7bpd   1/1     Running   0          41s

3 desired, 3 ready
grep -n "replicas:" deployment.yaml
10:  replicas: 3

Tres réplicas, en el clúster y en el repositorio — el punto de partida exacto de este proyecto.


Paso 2 — Cambia deployment.yaml, localmente, y confirma el diff

sed -i '' 's/replicas: 3/replicas: 5/' deployment.yaml
git diff

Qué esperar (literal, ejecutado):

diff --git a/deployment.yaml b/deployment.yaml
index 423cef4..9086693 100644
--- a/deployment.yaml
+++ b/deployment.yaml
@@ -7,7 +7,7 @@ metadata:
   labels:
     app: andes-cargo-status-api
 spec:
-  replicas: 3
+  replicas: 5
   strategy:
     type: RollingUpdate
     rollingUpdate:

Una línea. Nada más cambió — ni la imagen, ni el strategy, ni ningún otro campo del Pod template. Como confirmó la lección 7, esto es, con precisión, un evento de escalado — el ReplicaSet existente va a crecer, sin que se cree ninguno nuevo.


Paso 3 — git commit, git push — y nada más

git add deployment.yaml
git commit -m "Scale andes-cargo-status-api from 3 to 5 replicas"
git push origin main

Qué esperar (literal, ejecutado — el hash del commit es tu valor variable):

To http://localhost:3000/andes-cargo/andes-cargo-k8s.git
   820515f..cd0b5ca  main -> main

Esto es todo lo que vas a ejecutar contra el clúster. A partir de aquí, ningún comando de los pasos siguientes cambia nada — todos observan.


Paso 4 — Observa la convergencia, con timestamps reales

for i in $(seq 1 18); do
  ts=$(date +%H:%M:%S)
  sync=$(kubectl get application andes-cargo-status-api -n argocd -o jsonpath='{.status.sync.status}')
  revision=$(kubectl get application andes-cargo-status-api -n argocd -o jsonpath='{.status.sync.revision}' | cut -c1-7)
  replicas=$(kubectl get deployment andes-cargo-status-api -n andes-cargo -o jsonpath='{.spec.replicas}')
  echo "$ts sync=$sync revision=$revision spec.replicas=$replicas"
  if [ "$replicas" = "5" ]; then break; fi
  sleep 10
done

Qué esperar (literal, ejecutado — los timestamps exactos son tu valor variable; el patrón —varios ciclos sin cambio, después convergencia— es lo que confirma el mecanismo de la lección 2):

15:54:13 sync=Synced revision=820515f spec.replicas=3
15:54:23 sync=Synced revision=820515f spec.replicas=3
15:54:33 sync=Synced revision=820515f spec.replicas=3
15:54:43 sync=Synced revision=820515f spec.replicas=3
15:54:53 sync=Synced revision=820515f spec.replicas=3
15:55:03 sync=Synced revision=820515f spec.replicas=3
15:55:13 sync=Synced revision=820515f spec.replicas=3
15:55:24 sync=Synced revision=820515f spec.replicas=3
15:55:34 sync=Synced revision=820515f spec.replicas=3
15:55:44 sync=Synced revision=820515f spec.replicas=3
15:55:54 sync=Synced revision=cd0b5ca spec.replicas=5

Aproximadamente cien segundos entre el git push del Paso 3 y la convergencia detectada — dentro del rango que predijo la lección 2 (el intervalo de sondeo de ArgoCD, por defecto, de unos pocos minutos). Ni un solo kubectl apply, kubectl scale ni kubectl edit corrió en ningún momento de esta espera — el número pasó de 3 a 5 porque ArgoCD lo empujó, no porque nadie lo forzara.


Paso 5 — Confirma los Pods, en dos momentos

Unos segundos después de que revision cambiara a cd0b5ca:

kubectl get pods -n andes-cargo -l app=andes-cargo-status-api -o wide

Qué esperar (literal, ejecutado — todavía en transición, un Pod terminando):

NAME                                      READY   STATUS        RESTARTS   AGE   IP            NODE
andes-cargo-status-api-548966dd97-8w8tx   1/1     Running       0          15s   10.244.1.17   andes-cargo-cluster-worker
andes-cargo-status-api-548966dd97-9nh96   1/1     Running       0          15s   10.244.1.18   andes-cargo-cluster-worker
andes-cargo-status-api-548966dd97-bjcqx   1/1     Terminating   0          91s   10.244.1.16   andes-cargo-cluster-worker
andes-cargo-status-api-548966dd97-bpqjq   1/1     Running       0          79m   10.244.1.2    andes-cargo-cluster-worker
andes-cargo-status-api-548966dd97-hqlbt   1/1     Running       0          15s   10.244.2.10   andes-cargo-cluster-worker2
andes-cargo-status-api-548966dd97-ls2bc   1/1     Running       0          79m   10.244.2.2    andes-cargo-cluster-worker2

Y, unos segundos después, ya estable:

kubectl get all -n andes-cargo

Qué esperar (literal, ejecutado — nombres de Pod son tu valor variable; el patrón de cinco réplicas es literal):

NAME                                          READY   STATUS    RESTARTS   AGE
pod/andes-cargo-status-api-548966dd97-br69f   1/1     Running   0          42s
pod/andes-cargo-status-api-548966dd97-g4xhv   1/1     Running   0          42s
pod/andes-cargo-status-api-548966dd97-hqlbt   1/1     Running   0          70s
pod/andes-cargo-status-api-548966dd97-ls2bc   1/1     Running   0          79m
pod/andes-cargo-status-api-548966dd97-wtv4s   1/1     Running   0          42s

NAME                         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/status-api-service   ClusterIP   10.96.239.125   <none>        80/TCP    79m

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

NAME                                                DESIRED   CURRENT   READY   AGE
replicaset.apps/andes-cargo-status-api-548966dd97   5         5         5       79m

NAME                                                             REFERENCE                           TARGETS       MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/andes-cargo-status-api-hpa   Deployment/andes-cargo-status-api   cpu: 1%/50%   2         6         5          79m

5/5, un solo ReplicaSet (548966dd97, el mismo hash de siempre — confirmando, otra vez, que esto fue escalado, no un RollingUpdate de versión, exactamente como predijo la lección 7). Confirma también desde el ángulo de ArgoCD:

argocd app get andes-cargo-status-api

Qué esperar (literal, ejecutado):

Sync Status:        Synced to main (cd0b5ca)
Health Status:      Healthy

GROUP              KIND                     NAMESPACE    NAME                            STATUS  HEALTH   HOOK  MESSAGE
                   Namespace                andes-cargo  andes-cargo                     Synced  Running        namespace/andes-cargo unchanged
networking.k8s.io  NetworkPolicy            andes-cargo  default-deny-ingress            Synced                 networkpolicy.networking.k8s.io/default-deny-ingress unchanged
networking.k8s.io  NetworkPolicy            andes-cargo  allow-from-ingress-nginx        Synced                 networkpolicy.networking.k8s.io/allow-from-ingress-nginx unchanged
                   Secret                   andes-cargo  andes-cargo-status-api-secrets  Synced                 secret/andes-cargo-status-api-secrets configured
                   ConfigMap                andes-cargo  andes-cargo-status-api-config   Synced                 configmap/andes-cargo-status-api-config unchanged
                   Service                  andes-cargo  status-api-service              Synced  Healthy        service/status-api-service unchanged
apps               Deployment               andes-cargo  andes-cargo-status-api          Synced  Healthy        deployment.apps/andes-cargo-status-api configured
autoscaling        HorizontalPodAutoscaler  andes-cargo  andes-cargo-status-api-hpa      Synced  Healthy        horizontalpodautoscaler.autoscaling/andes-cargo-status-api-hpa unchanged
networking.k8s.io  Ingress                  andes-cargo  status-api-ingress              Synced  Healthy        ingress.networking.k8s.io/status-api-ingress unchanged
argoproj.io        Application              argocd       andes-cargo-status-api          Synced                 application.argoproj.io/andes-cargo-status-api configured

Sync to cd0b5ca — exactamente el commit del Paso 3. Synced, Healthy, cinco réplicas, cero kubectl apply. Esta es la prueba central de todo el módulo.

                    LO QUE ACABA DE PASAR

  Tú                    Gitea                  ArgoCD              andes-cargo-cluster
   │                       │                       │                        │
   │──git push (cd0b5ca)──▶│                       │                        │
   │                       │◀── poll (~100s) ──────│                        │
   │                       │────commit cd0b5ca────▶│                        │
   │                       │                       │──apply replicas: 5────▶│
   │                       │                       │◀───5/5 Ready───────────│
   │                       │                       │                        │
   │  Nunca corriste: kubectl apply, kubectl scale, kubectl edit             │

Paso 6 — El hallazgo real: el HPA y Git compiten por el mismo campo

Todo lo anterior es la prueba que este proyecto prometía. Lo que sigue es un descubrimiento real, no planeado, que apareció mientras se escribía esta lección — y que vale la pena documentar completo, porque es exactamente el tipo de problema que un equipo real encuentra la primera semana de usar GitOps junto a autoscaling.

Unos minutos después de la convergencia del Paso 5, sin que nadie tocara nada:

kubectl get deployment andes-cargo-status-api -n andes-cargo -o jsonpath='{.spec.replicas}{"\n"}'
kubectl get hpa -n andes-cargo
argocd app get andes-cargo-status-api --refresh | head -12

Qué esperar (literal, ejecutado — sorpresa: el número volvió a bajar, y ArgoCD lo marca):

2

NAME                         REFERENCE                           TARGETS       MINPODS   MAXPODS   REPLICAS   AGE
andes-cargo-status-api-hpa   Deployment/andes-cargo-status-api   cpu: 1%/50%   2         6         2          94m

Sync Status:        OutOfSync from main (cd0b5ca)
Health Status:      Healthy

OutOfSync. El HorizontalPodAutoscaler del Módulo 3 (andes-cargo-status-api-hpa, con minReplicas: 2) hizo exactamente lo que fue diseñado para hacer: con la CPU real al 1%, muy por debajo del objetivo del 50%, redujo las réplicas hacia el mínimo permitido — escribiendo directamente spec.replicas: 2 en el Deployment, sin pasar por Git. deployment.yaml, en Gitea, sigue diciendo replicas: 5 — Git y el clúster real, ahora, dicen cosas distintas, y ArgoCD lo detecta con precisión (esa es, exactamente, su función: comparar).

Con selfHeal: true activo desde la lección 5, la pregunta obvia es: ¿por qué ArgoCD no revierte esto a 5, como sí hizo con el kubectl scale manual de esa misma lección? La respuesta está en los propios registros del controlador:

kubectl logs -n argocd argocd-application-controller-0 --tail=200 | grep -i "skipping auto-sync"

Qué esperar (literal, ejecutado):

{"msg":"Skipping auto-sync: already attempted sync to [cd0b5cadbb43cfdf8c6aaa9c46a7fcd0dd8a63eb] with timeout 0s (retrying in 3m41.370372326s)", ...}

ArgoCD intentó una corrección automática apenas detectó la diferencia — pero, después de ese primer intento, aplica un período de enfriamiento antes de volver a intentarlo contra la misma revisión. Sin este mecanismo, selfHeal y el HPA entrarían en una pelea infinita: ArgoCD fuerza 5, el HPA lo baja de nuevo a 2 en su próximo ciclo (cada pocos segundos), ArgoCD lo vuelve a forzar, indefinidamente — el tipo exacto de oscilación que ningún sistema de producción debería tolerar. El enfriamiento evita esa pelea, a costa de dejar el Application en OutOfSync de forma indefinida mientras el HPA siga activo y la CPU se mantenga baja.

              LA PELEA QUE selfHeal NO LIBRA INDEFINIDAMENTE

  Git: replicas = 5          HPA controller: cpu 1%/50%,
  (sin cambios)                baja a replicas = 2
        │                              │
        └──── ArgoCD compara, detecta diferencia ────┘
                        │
                        ▼
        Primer intento: fuerza replicas = 5 ✓
                        │
                        ▼
        HPA vuelve a bajarlo a 2, segundos después
                        │
                        ▼
        ArgoCD: "ya intenté sync contra esta revisión,
                 reintento en ~3-4 minutos" — NO pelea sin fin

Paso 7 — La corrección real: decirle a ArgoCD que ese campo no es suyo

La solución de producción real, documentada por el propio proyecto, no es desactivar selfHeal ni bajar la frecuencia de sondeo — es decirle a ArgoCD, explícitamente, que ese campo específico no es de su competencia. ignoreDifferences hace exactamente eso:

# application.yaml (fragmento agregado)
spec:
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=false
      - RespectIgnoreDifferences=true
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas

ignoreDifferences excluye /spec/replicas del cálculo de Sync Status — ArgoCD deja de considerar ese campo al decidir si el Deployment está sincronizado o no. RespectIgnoreDifferences=true, dentro de syncOptions, es el complemento necesario: sin esta bandera, una sincronización real seguiría aplicando el valor completo de Git (incluido replicas: 5) cada vez que corriera, sobrescribiendo lo que el HPA hubiera decidido — la bandera le dice a la operación de sincronización misma que respete el valor que ya está en el clúster para ese campo, no solo que lo ignore al comparar.

git add application.yaml
git commit -m "Ignore spec.replicas on the Deployment: the HorizontalPodAutoscaler owns it, not Git"
git push origin main
argocd app sync andes-cargo-status-api

Qué esperar (literal, ejecutado — el hash es tu valor variable):

To http://localhost:3000/andes-cargo/andes-cargo-k8s.git
   cd0b5ca..db09bbe  main -> main

Sync Status:        Synced to main (db09bbe)
Health Status:      Healthy
...
apps  Deployment  andes-cargo  andes-cargo-status-api  Synced  Healthy  deployment.apps/andes-cargo-status-api unchanged

unchanged — a pesar de que el Deployment real seguía teniendo 2 réplicas (el valor que dejó el HPA) y Git sigue declarando 5. ArgoCD ya no considera eso una diferencia. Confírmalo con la misma prueba de continuidad que hizo el Paso 6, esta vez durante dos minutos completos:

for i in $(seq 1 10); do
  ts=$(date +%H:%M:%S)
  sync=$(kubectl get application andes-cargo-status-api -n argocd -o jsonpath='{.status.sync.status}')
  replicas=$(kubectl get deployment andes-cargo-status-api -n andes-cargo -o jsonpath='{.spec.replicas}')
  echo "$ts sync=$sync spec.replicas=$replicas"
  sleep 12
done

Qué esperar (literal, ejecutado):

16:15:27 sync=Synced spec.replicas=2
16:15:39 sync=Synced spec.replicas=2
16:15:51 sync=Synced spec.replicas=2
16:16:03 sync=Synced spec.replicas=2
16:16:15 sync=Synced spec.replicas=2
16:16:27 sync=Synced spec.replicas=2
16:16:39 sync=Synced spec.replicas=2
16:16:51 sync=Synced spec.replicas=2
16:17:03 sync=Synced spec.replicas=2
16:17:15 sync=Synced spec.replicas=2

Synced, de forma estable, con el HPA libre de mover replicas a donde la CPU real lo pida —2 en este momento—, sin que ArgoCD lo marque nunca más como una diferencia. Este es el estado correcto de producción: Git sigue siendo la fuente de verdad para qué corre (la imagen, la configuración, las probes), y el HPA es la fuente de verdad para cuántas réplicas, en tiempo real — dos autoridades distintas, cada una dueña de su propio campo, sin pelear entre sí.


Checklist final del módulo

  • Gitea corriendo dentro del clúster, con el repositorio andes-cargo/andes-cargo-k8s como única fuente de verdad de nueve manifiestos (lección 3).
  • ArgoCD v3.5.1 corriendo dentro del clúster, sincronizando ese repositorio de forma automática (lección 4).
  • Un cambio real (replicas: 3 → 5) subido con git push y reflejado en el clúster sin un solo kubectl apply, medido con timestamps reales (~100 segundos de convergencia) — la prueba central de este módulo (Pasos 1-5 de este proyecto).
  • selfHeal: true corrigiendo un cambio manual en segundos (lección 5) y respetando, con ignoreDifferences, el campo que le pertenece a otro controlador (Paso 6-7 de este proyecto) — las dos caras de la misma propiedad de GitOps, aplicadas con criterio.

Errores comunes

Concluir que el hallazgo del Paso 6 significa que "GitOps y autoscaling son incompatibles" (de generalización apresurada, el error más importante de esta lección). Qué pasa: alguien, al ver OutOfSync sin razón obvia, concluye que ArgoCD y HorizontalPodAutoscaler no deberían usarse juntos. Cómo detectarlo: si tu conclusión es "hay que elegir entre GitOps o autoscaling". Cómo corregirlo: el Paso 7 demuestra la solución estándar y documentada oficialmente (ignoreDifferences + RespectIgnoreDifferences=true) — es un patrón conocido, no un defecto de ninguna de las dos herramientas. Cualquier equipo que use ArgoCD junto a un HorizontalPodAutoscaler en producción real configura esto desde el principio.

Aplicar ignoreDifferences sin RespectIgnoreDifferences=true y esperar el mismo resultado (de YAML incompleto). Qué pasa: alguien agrega solo el bloque ignoreDifferences, sin la opción de sincronización complementaria. Cómo detectarlo: si una sincronización manual (argocd app sync) sigue sobrescribiendo el valor que el HPA había puesto. Cómo corregirlo: ignoreDifferences por sí solo afecta únicamente el cálculo de diferencias (qué cuenta como OutOfSync); sin RespectIgnoreDifferences=true, una sincronización real todavía aplica el manifiesto completo de Git, incluido el campo "ignorado". Las dos piezas trabajan juntas, no una en lugar de la otra.

No entender por qué selfHeal no peleó infinitamente antes de la corrección (de mecanismo, ya explicado con el log literal del Paso 6). Qué pasa: alguien espera ver a ArgoCD forzando replicas: 5 cada pocos segundos, en una pelea visible contra el HPA. Cómo detectarlo: si, al revisar los eventos del namespace durante el período OutOfSync, esperabas ver docenas de eventos ScalingReplicaSet alternando 2 y 5. Cómo corregirlo: el mensaje Skipping auto-sync: already attempted sync ... retrying in Nm del propio controlador confirma que ArgoCD intenta la corrección una vez, y después espera un período de enfriamiento antes de reintentar contra la misma revisión — un mecanismo de protección exactamente contra el escenario de este proyecto, documentado en el propio comportamiento del controlador, no en la documentación de más alto nivel.


Ejercicios

Ejercicio 1 — Reconstruye la secuencia completa de memoria. Sin volver a esta lección, enumera, en orden, los cinco eventos que ocurrieron entre el git push del Paso 3 y el estado final Synced/Healthy con ignoreDifferences activo.

Ver solución

(1) git push sube replicas: 5 a Gitea. (2) ArgoCD sondea el repositorio (~100 segundos después) y detecta el cambio. (3) ArgoCD aplica replicas: 5 al Deployment, sin ningún kubectl apply manual — convergencia confirmada. (4) Minutos después, el HorizontalPodAutoscaler, con CPU real baja, reduce replicas a 2 directamente en el clúster, sin pasar por Git — ArgoCD lo detecta como OutOfSync, pero no pelea indefinidamente (enfriamiento del controlador). (5) Se agrega ignoreDifferences + RespectIgnoreDifferences=true al Application, vía otro git push — el Sync Status vuelve a Synced de forma estable, con el HPA libre de mover replicas sin que ArgoCD lo marque nunca más.

Ejercicio 2 — Explica ignoreDifferences sin usar la palabra "ignorar". En una frase, sin usar la palabra "ignorar" ni "ignora", explica qué hace ignoreDifferences: [{group: apps, kind: Deployment, jsonPointers: [/spec/replicas]}].

Ver solución

Una explicación razonable: "Le dice a ArgoCD que el campo replicas de este Deployment en particular pertenece a otro controlador (el HorizontalPodAutoscaler), así que no debe contarlo como una diferencia entre Git y el clúster real, ni sobrescribirlo durante una sincronización."

Ejercicio 3 — Diseña la prueba de que la corrección funciona, sin copiar el Paso 7. Sin mirar el Paso 7 de nuevo, describe un experimento —con comandos concretos— que demuestre que ignoreDifferences está funcionando correctamente para este Application.

Ver solución

Una respuesta razonable: forzar al HPA a cambiar replicas de forma visible (por ejemplo, generando carga real con kubectl run load-generator, como en el Módulo 3, para que suba por encima de 5, o esperando a que la CPU baje y el HPA lo reduzca de forma natural, como hizo este proyecto) y, mientras eso ocurre, correr kubectl get application andes-cargo-status-api -n argocd -o jsonpath='{.status.sync.status}' en un bucle durante varios minutos. Si el resultado se mantiene en Synced durante todo el experimento, sin importar qué valor tenga replicas en cada momento, la corrección funciona — exactamente la prueba de dos minutos que hizo el Paso 7 de esta lección.


Resumen y siguiente paso

Este proyecto demostró, con evidencia literal y timestamps reales, la promesa central de todo el módulo: un cambio de negocio real (replicas: 3 → 5), subido con git push, reflejado en andes-cargo-cluster en aproximadamente cien segundos, sin un solo kubectl apply. También documentó, completo, un hallazgo real no planeado —la tensión entre selfHeal de ArgoCD y el HorizontalPodAutoscaler del Módulo 3 por el mismo campo replicas— y su corrección de producción real (ignoreDifferences + RespectIgnoreDifferences=true), verificada con una prueba de estabilidad de dos minutos. andes-cargo-status-api es, desde hoy, un servicio cuyo qué corre vive en Git (imagen, configuración, probes, NetworkPolicy) y cuyo cuánto corre vive en tiempo real, en el HorizontalPodAutoscaler — dos fuentes de verdad, cada una dueña de su propio campo, coexistiendo sin pelear.

Antes de avanzar deberías poder: reproducir, en tu propio clúster, el git push sin kubectl apply de este proyecto; explicar por qué selfHeal no peleó infinitamente contra el HPA; y escribir de memoria el bloque ignoreDifferences que resuelve esa tensión.

Siguiente módulo: seguridad de runtime, admission control y escaneo de imagen. El Módulo 6 toma el mismo repositorio andes-cargo-k8s que ArgoCD sincroniza y agrega la capa que decide, antes de que cualquier objeto llegue a etcd, si tiene permitido existir — OPA Gatekeeper y Kyverno, dos motores de políticas distintos, ambos corridos de verdad contra andes-cargo-cluster.

Recursos

  1. Argo CD — Diffing, Application-Level Configuration — documentación oficial de ignoreDifferences, fuente exacta de la corrección de este proyecto.
  2. Argo CD — Sync Options, Respect Ignore Difference on Sync — documentación de RespectIgnoreDifferences=true.
  3. Kubernetes — Horizontal Pod Autoscaling — referencia del controlador que compite por spec.replicas en este proyecto.
  4. kubernetes-and-eks-in-production-guide (NIEVA), Módulo 3, lección 7-8 — el origen del HorizontalPodAutoscaler que este proyecto retoma con un hallazgo nuevo.