ArgoCD Kubernetes GitOps

ArgoCD i HPA: ciągły OutOfSync loop przez spec.replicas

Naprawa nieskończonej pętli OutOfSync w ArgoCD spowodowanej konfliktem między HPA a deklaratywnym zarządzaniem spec.replicas.

Jerzy Kopaczewski ·
ArgoCD nieustannie pokazuje OutOfSync dla Deploymentu lub HPA, mimo że nic się nie zmieniło w Git. Po każdym sync status natychmiast wraca do OutOfSync. Ten runbook pokazuje jak rozwiązać konflikt między HPA a ArgoCD.

Ten runbook jest częścią serii o blokadach synchronizacji ArgoCD. Zobacz też: etcd request too large i deadlock operacji. Jeśli interesuje Cię szerszy kontekst GitOps i ArgoCD, przeczytaj nasz post o GitOps i ArgoCD.

Objaw

ArgoCD pokazuje status OutOfSync dla zasobu HorizontalPodAutoscaler lub Deployment. Diff wskazuje na zmianę spec.replicas, która nie pochodzi z Git:

# ArgoCD shows OutOfSync for HPA resources continuously
# Diff shows spec.replicas changing between desired state and live state
argocd app diff <APP_NAME> --local ./chart

# Output shows:
# HorizontalPodAutoscaler my-app-hpa:
#   spec.replicas: 3 -> 7 (live != desired)

Synchronizacja jest technicznie możliwa, ale po każdym sync HPA natychmiast zmienia repliki z powrotem, powodując nieskończoną pętlę OutOfSync → Sync → OutOfSync. W środowiskach z włączonym selfHeal: true ArgoCD próbuje naprawiać ten „drift” w kółko, generując zbędne operacje i obciążając controller.

Przyczyna

HPA (HorizontalPodAutoscaler) automatycznie skaluje liczbę replik na podstawie metryk (CPU, memory, custom metrics). Kiedy HPA zmienia spec.replicas w Deployment, ta wartość odbiega od stanu zapisanego w Git. ArgoCD traktuje to jako drift, rozbieżność między desired state a live state.

To klasyczny konflikt własności pola (field ownership) między dwoma controllerami: ArgoCD chce utrzymać stan z Git, a HPA chce dostosować repliki do obciążenia. Problem dotyczy też GitHub ARC (Actions Runner Controller), który dynamicznie skaluje runner pods. Identyczny mechanizm konfliktu.

Rozwiązanie

Użyj ignoreDifferences z jqPathExpressions, aby ArgoCD ignorował pola zarządzane przez HPA. Dodatkowo włącz RespectIgnoreDifferences sync option, żeby ArgoCD nie nadpisywał tych pól podczas synchronizacji:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/org/app.git
    targetRevision: main
    path: k8s/
  destination:
    server: https://kubernetes.default.svc
    namespace: my-app
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jqPathExpressions:
        - .spec.replicas
    - group: autoscaling
      kind: HorizontalPodAutoscaler
      jqPathExpressions:
        - .spec.metrics[].resource.target.averageUtilization
        - .status
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - RespectIgnoreDifferences=true

Uwaga dotycząca wersji: jqPathExpressions wymaga ArgoCD ≥ 2.1. Opcja RespectIgnoreDifferences=true wymaga ArgoCD ≥ 2.5. Jeśli korzystasz ze starszej wersji, użyj jsonPointers zamiast jqPathExpressions (np. /spec/replicas).

# Apply the updated Application manifest
kubectl apply -f application-with-ignore.yaml

# Verify sync status returns to Synced
argocd app get my-app --output json | jq '.status.sync.status'
# Expected: "Synced"

Po zastosowaniu tej konfiguracji ArgoCD nie będzie traktował zmian w spec.replicas jako driftu. HPA może swobodnie skalować Deployment, a Application pozostanie w stanie Synced. To rozwiązanie jest preferowane nad alternatywą (usunięcie replicas z manifestów w Git), ponieważ zachowuje jawną deklarację wartości domyślnej w repozytorium.

Identyczny wzorzec stosuje się do innych controllerów dynamicznie modyfikujących zasoby: Istio injection dodający sidecary, cert-manager aktualizujący status certyfikatów czy VPA (Vertical Pod Autoscaler) zmieniający requests i limits.

Walidacja

# 1. Verify sync status is Synced (not OutOfSync)
argocd app get <APP_NAME> --output json | jq '.status.sync.status'
# Expected: "Synced"

# 2. Wait for HPA to scale (2-3 minutes) and check again
sleep 180
argocd app get <APP_NAME> --output json | jq '.status.sync.status'
# Expected: still "Synced" despite HPA changing replicas

# 3. Verify health status
argocd app get <APP_NAME> --output json | jq '.status.health.status'
# Expected: "Healthy"

# 4. Check that HPA is actually scaling
kubectl get hpa -n <NAMESPACE>
# Expected: TARGETS show current utilization, REPLICAS may differ from Git

Jeśli po kilku cyklach HPA status pozostaje Synced, problem jest rozwiązany. Kluczowe jest odczekanie przynajmniej jednego pełnego cyklu autoskalowania (2-3 minuty), aby potwierdzić, że pętla OutOfSync nie wraca.

Z perspektywy kosztów operacyjnych, pętla HPA OutOfSync generuje ciągłe próby rekoncyliacji, które obciążają zasoby controllera ArgoCD i zalewają kanały alertowe false positive. Zespoły uczą się ignorować ten szum, co oznacza, że prawdziwy błąd synchronizacji pozostaje niezauważony do momentu, aż problem na produkcji ujawni się godziny później.

 

Jerzy Kopaczewski

Pipeline CI/CD blokuje Twój zespół?

Umów bezpłatną 30-minutową rozmowę. Przejrzymy konfigurację ArgoCD i wskażemy, co naprawić od razu.