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.
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:
jqPathExpressionswymaga ArgoCD ≥ 2.1. OpcjaRespectIgnoreDifferences=truewymaga ArgoCD ≥ 2.5. Jeśli korzystasz ze starszej wersji, użyjjsonPointerszamiastjqPathExpressions(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.
Alternatywa B) Usuń spec.replicas z manifestu Git:
Zamiast ignorować różnicę - usuń pole z manifestu, oddając HPA pełną kontrolę:
# Deployment BEZ spec.replicas (HPA zarządza wyłącznie)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
# replicas: NIE DEKLAROWANE - HPA ma pełną kontrolę
# HPA minReplicas definiuje minimum, nie potrzeba replicas tutaj
selector:
matchLabels:
app: my-app
template:
# ...
Przy Kustomize upewnij się, że overlay nie dodaje replicas z powrotem:
# kustomization.yaml - NIE używaj sekcji replicas:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
# ❌ USUŃ:
# replicas:
# - name: my-app
# count: 3
Alternatywa C) Server-Side Diff (ArgoCD 2.10+):
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
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
managedFieldsManagers:
- kube-controller-manager # HPA działa przez tego managera
jsonPointers:
- /spec/replicas
syncPolicy:
syncOptions:
- ServerSideApply=true
- RespectIgnoreDifferences=true
Alternatywa D) Globalna konfiguracja (dla wszystkich aplikacji):
# argocd-cm ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
resource.customizations.ignoreDifferences.apps_Deployment: |
jsonPointers:
- /spec/replicas
resource.customizations.ignoreDifferences.apps_StatefulSet: |
jsonPointers:
- /spec/replicas
resource.customizations.ignoreDifferences.argoproj.io_Rollout: |
jsonPointers:
- /spec/replicas
kubectl apply -f argocd-cm.yaml
kubectl rollout restart deployment argocd-repo-server -n argocd
kubectl rollout restart deployment argocd-application-controller -n argocd
KEDA ScaledObject - specjalny przypadek:
# Przy KEDA: ScaledObject tworzy HPA dynamicznie
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jqPathExpressions:
- .spec.replicas
- group: autoscaling
kind: HorizontalPodAutoscaler
jqPathExpressions:
- .status
- .spec.metrics
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.
Pipeline CI/CD blokuje Twój zespół?
Umów bezpłatną 30-minutową rozmowę. Przejrzymy konfigurację ArgoCD i wskażemy, co naprawić od razu.