Argo CD'yi HA Kurup Bozarak Anlamak: redis-ha, App-of-Apps ve Failover Etmeyen Tek Pod

Argo CD'yi Helm ile HA kurup her şeyi tek root Application'a veriyoruz; sonra Redis'i, bir repo-server'ı ve bir node'u öldürüp neyin çalıştığını ölçüyoruz.

Yazan: Mirac Can Yılmaz 26 dk okuma
Argo CD'yi HA Kurup Bozarak Anlamak: redis-ha, App-of-Apps ve Failover Etmeyen Tek Pod - platform-mühendisliği yazısının kapak görseli

"HA Argo CD" genelde values dosyasında tek bir şey demek: birkaç bileşenin yanına replicas: 2. İlk seferinde ben de öyle yaptım. Yaşadığım ilk gerçek kesinti, toplu bir sync sırasında belleği biten bir repo-server'dı; üç sağlıklı API server replica'sı da olanı seyrediyordu. Sorun hiçbir zaman API server'da değildi.

Bu yazıda Argo CD'yi dört node'lu bir kind cluster'ında HA modda kuruyoruz, geri kalan her şeyi tek bir root Application'a (app-of-apps) veriyoruz, sonra bilerek bozuyoruz: Redis master'ı, bir repo-server'ı ve application controller'ın çalıştığı node'u. Her arızanın süresini ölçüyoruz. İkisi fark edilmeden geçiyor (6 saniye ve 1 saniye). Üçüncüsü, elle bir taint ekleyene kadar bütün reconciliation'ı yedi dakika durdurdu. Sonunda Argo CD'nin hangi parçalarının gerçekten failover ettiğini, hangisinin etmediğini ve bu konuda ne yapılacağını bileceksiniz.

Lab argocd-ha-app-of-apps klasöründe. Argo CD'nin deploy ettiği şeyler, bir şirkette olacağı gibi üç ayrı public repoda duruyor: platform-gitops (platform ekibinin), product-helloapi-gitops (bir ürün ekibinin deploy config'i) ve hellofiber (o ekibin Go servisi).

Neye ihtiyacınız var

  • 4 CPU / 8 GB Docker. Bu çalıştırmada: Apple Silicon üzerinde Colima 0.8, Docker 27. Her şey ayaktayken dört node yaklaşık 3 GB kullanıyor.
  • kind 0.33, kubectl, helm 3.15+. macOS'ta: brew install kind kubectl helm.
  • Bu çalıştırmadaki sürümler: argo/argo-cd chart 10.9.2 (Argo CD v3.5.3, Redis 8.6.4), kindest/node:v1.35.0.
  • Temel Argo CD: Application nedir, sync ve health ne demek. Bu, Production'da GitOps serisinin ilk yazısı; blogdaki önceki yazılardan hiçbiri gerekmiyor.

Neden bu şekilde

ha/install.yaml değil, Helm chart. Argo CD düz HA manifest'leri de yayınlıyor ve ben yıllarca onları kullandım. Chart'ın tek bir üstünlüğü var: values dosyası, sonrasında Argo CD'nin kendisiyle ilgili yönettiği şeyin ta kendisi oluyor. Düz manifest'lerde upstream dosyanın üstüne ConfigMap patch'lemeye başlıyorsunuz ve bu çok belirli bir şekilde bozuluyor (Ne ters gitti bölümüne bakın).

Tek dev Application değil, app-of-apps. Ekip başına tek Application, bozuk bir manifest o ekibin bütün servislerini bloklayana ve her sync dakikalar sürene kadar daha basit görünüyordu. Burada elle uygulanan tek şey root. Bir dizini gösteriyor ve o dizindeki her şey kendi başına bir Application: Argo CD'nin kendisi, AppProject'ler, ürün ve ortam başına birer Application üreten ApplicationSet'ler. Bozuk bir servis sadece kendi Application'ını bozuyor.

Tek repo değil, üç repo. Platform config'i, bir ürünün deploy config'i ve o ürünün kodu farklı hızlarda değişiyor ve farklı kişiler tarafından review ediliyor. Aşağıdaki ayrım bir şirkette kuracağım yapı; lab sadece private repolar yerine public GitHub repoları kullanıyor.

Repo Kim değiştiriyor İçinde ne var
platform-gitops platform ekibi Argo CD values, bootstrap/root.yaml, apps/, ApplicationSet'ler, namespace + quota
product-helloapi-gitops ürün ekibi Helm chart, overlays/{dev,prod}/values.yaml (image tag, replica, mesaj)
hellofiber ürün ekibi Go kodu; CI, amd64 ve arm64 için ghcr.io/miraccan00/hellofiber:sha-<7> yayınlıyor

Argo CD app-of-apps mimarisi: platform ve ürün ekibinin sahip olduğu üç Git reposu; tek root Application, AppProject'leri, kendini yöneten Argo CD Application'ını ve iki ApplicationSet'i oluşturuyor

Adım 1 — Üç worker ve tek bir helm install

Redis HA, üç pod'unu zorunlu anti-affinity ile üç ayrı node'a koyuyor. İki worker ile üçüncü Redis pod'u sonsuza kadar Pending kalır; o yüzden kind cluster'ında üç worker var:

# kind/cluster.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: argocd-ha
nodes:
  - role: control-plane
    image: kindest/node:v1.35.0
  - role: worker
    image: kindest/node:v1.35.0
  - role: worker
    image: kindest/node:v1.35.0
  - role: worker
    image: kindest/node:v1.35.0

Colima'da dört kind node'u varsayılan inotify limitlerini tüketiyor ve node container'ları açılışta ölüyor; scripts/colima-limits.sh limitleri yükseltiyor (Cluster API yazısındakiyle aynı script).

"HA"nın bileşen bileşen kararlaştırıldığı yer values dosyası. platform-gitops reposunda duruyor, çünkü Adım 2'de Argo CD onu tekrar okuyacak:

# platform-gitops/argocd/values-ha.yaml
redis-ha:
  enabled: true          # the chart's warning: needs 3 nodes (hard anti-affinity)

controller:
  replicas: 1            # no PDB: with one replica, minAvailable 1 would block every node drain

server:
  replicas: 2
  pdb:
    enabled: true
    minAvailable: 1

repoServer:
  replicas: 2
  pdb:
    enabled: true
    minAvailable: 1
  resources:
    requests:
      cpu: 100m
      memory: 256Mi
    limits:
      memory: 1Gi        # a mass sync renders many charts at once; this is the component that OOMs

applicationSet:
  replicas: 2
  pdb:
    enabled: true
    minAvailable: 1

dex:
  enabled: false         # SSO comes in the next post (OIDC); local admin for now

notifications:
  enabled: false

configs:
  params:
    server.insecure: "true"   # the lab reaches the UI through port-forward; TLS belongs on the gateway
    # Concurrent manifest generations per repo-server replica. 0 (default) = unlimited,
    # so a mass sync renders everything at once and memory spikes past the limit.
    reposerver.parallelism.limit: 5
  cm:
    # product-*-gitops overlays wrap the chart with kustomize `helmCharts:`.
    # Without this flag the repo-server refuses to render them:
    #   "must specify --enable-helm"
    kustomize.buildOptions: --enable-helm

Her satırın size kazandırdığı:

Bileşen Replica Onun için HA ne demek
Redis (redis-ha) 3 Redis + 3 Sentinel, 3 HAProxy Gerçek failover: Sentinel bir replica'yı master yapıyor, HAProxy trafiği yeni master'a yönlendiriyor
repo-server 2 Stateless, manifest render ediyor. İki replica yükü paylaşıyor; OOM'u durduran şey limit + paralellik sınırı
API server 2 Stateless UI/API. Olması iyi; kaybetmek sync'leri durdurmuyor
ApplicationSet controller 2 Leader election: biri çalışıyor, biri bekliyor
application controller 1 Active-active değil. Fazla replica = cluster'lar arasında sharding; tek hedef cluster varken ikinci replica sadece bekler

Akılda tutulacak satır sonuncusu. Adım 5'te geri geliyor.

make argocd
# helm install argocd argo/argo-cd --version 10.9.2 -n argocd --create-namespace \
#   -f https://raw.githubusercontent.com/miraccan00/platform-gitops/blog-04/argocd/values-ha.yaml --wait --timeout 10m

helm install --wait 343 saniye sürdü. Çoğu Redis: StatefulSet üç pod'unu sırayla başlatıyor ve her biri Sentinel'in kendisini görmesini bekliyor. On üç pod, üç worker'a dağılmış (sütunlar kısaltıldı):

NAME                                               READY   STATUS    NODE
argocd-redis-ha-haproxy-647d9bfdf8-qlqcs           1/1     Running   argocd-ha-worker
argocd-server-8646bcb568-lqw9s                     1/1     Running   argocd-ha-worker
argocd-repo-server-566fc4b49f-q4gk4                1/1     Running   argocd-ha-worker
argocd-redis-ha-server-1                           3/3     Running   argocd-ha-worker
argocd-repo-server-566fc4b49f-gw4t7                1/1     Running   argocd-ha-worker2
argocd-redis-ha-haproxy-647d9bfdf8-95wv6           1/1     Running   argocd-ha-worker2
argocd-redis-ha-server-2                           3/3     Running   argocd-ha-worker2
argocd-application-controller-0                    1/1     Running   argocd-ha-worker2
argocd-applicationset-controller-d864b8898-hvts8   1/1     Running   argocd-ha-worker2
argocd-redis-ha-haproxy-647d9bfdf8-s4xg9           1/1     Running   argocd-ha-worker3
argocd-redis-ha-server-0                           3/3     Running   argocd-ha-worker3
argocd-applicationset-controller-d864b8898-qkhnt   1/1     Running   argocd-ha-worker3
argocd-server-8646bcb568-cw8sr                     1/1     Running   argocd-ha-worker3

Bu yazıdaki son helm komutu bu.

UI'ı açın: bundan sonraki her adımı orada da izleyeceğiz

make ui
# http://localhost:8080  admin / <parola>

make ui önce admin parolasını yazıyor, sonra svc/argocd-server'a port-forward açıyor. Values'daki server.insecure: "true" yüzünden bu düz HTTP: tarayıcıya http://localhost:8080 yazın, kullanıcı adı admin. Port-forward tek bir argocd-server pod'una bağlanıyor ve Adım 5'te o pod ölebiliyor; make ui bu yüzden bir döngüde çalışıyor ve bağlantı koparsa kendini yeniden bağlıyor. make ui çalıştığı terminali meşgul ediyor: onu açık bırakın ve bundan sonraki bütün make komutlarını ikinci bir terminalde, aynı argocd-ha-app-of-apps/ klasöründe çalıştırın (Makefile KUBECONFIG'i ./.lab içinden kendisi ayarlıyor).

Yazıda geçen ekranların doğrudan adresleri, giriş yaptıktan sonra:

Ekran Adres
Bütün Application'lar http://localhost:8080/applications
root ağacı http://localhost:8080/applications/argocd/root
Argo CD'nin kendisi, Pods görünümü http://localhost:8080/applications/argocd/argocd?view=pods
Ürün, prod http://localhost:8080/applications/argocd/product-helloapi-prod
Ürün, dev http://localhost:8080/applications/argocd/product-helloapi-dev
ApplicationSet'ler http://localhost:8080/applicationsets

Adreslerdeki ilk argocd, Application objelerinin durduğu namespace.

Argo CD v3.5.3 giriş ekranı, http://localhost:8080 üzerinden port-forward ile

Bootstrap'tan önce Applications sayfası boş. Argo CD kurulu ve ayakta, ama henüz hiçbir şeyi yönetmiyor. Bu ekranı bir kez görmek önemli: birazdan tek bir kubectl apply bu sayfayı dolduracak.

Bootstrap'tan önce Argo CD Applications sayfası: "No applications available to you just yet"

Adım 2 — Tek bir kubectl apply, sonra Argo CD devralıyor

Elle uygulanan tek obje root Application:

# platform-gitops/bootstrap/root.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/miraccan00/platform-gitops.git
    targetRevision: blog-04
    path: apps
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      # prune: a file deleted from apps/ removes that Application from the cluster.
      # Without it a deleted app keeps running and root shows OutOfSync forever.
      prune: true
      selfHeal: true

apps/ içinde üç dosya var ve ürettikleri ağaç platformun tamamı:

root (Application, elle uygulanır)
├── platform, products (AppProject, wave -2)
├── argocd (Application → argo-cd chart 10.9.2 + argocd/values-ha.yaml, wave -1)
└── applicationsets (Application → applicationsets/)
    ├── namespaces (ApplicationSet) → ns-product-helloapi-dev, ns-product-helloapi-prod
    └── products   (ApplicationSet) → product-helloapi-dev, product-helloapi-prod

İlginç olan argocd: Argo CD kendi kurulumunu, helm install'ın kullandığı aynı chart sürümü ve aynı values dosyasıyla yönetiyor.

# platform-gitops/apps/argocd.yaml (spec)
spec:
  project: platform
  sources:
    - repoURL: https://argoproj.github.io/argo-helm
      chart: argo-cd
      targetRevision: 10.9.2
      helm:
        releaseName: argocd          # must match `helm install argocd ...`, or every object name changes
        valueFiles:
          - $values/argocd/values-ha.yaml
    - repoURL: https://github.com/miraccan00/platform-gitops.git
      targetRevision: blog-04
      ref: values
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      # prune false, on purpose: a bad commit that drops a ConfigMap or the redis
      # StatefulSet from the render would make Argo CD delete the parts it runs on,
      # and it could not sync itself back. selfHeal still reverts drift.
      prune: false
      selfHeal: true
    syncOptions:
      - ServerSideApply=true

İki source: chart Helm reposundan, values dosyası Git'ten ($values ikinci source'u gösteriyor). Bundan sonra Argo CD'yi yükseltmek, targetRevision'ı değiştiren tek satırlık bir PR.

make bootstrap
# kubectl apply -f https://raw.githubusercontent.com/miraccan00/platform-gitops/blog-04/bootstrap/root.yaml
NAME                       SYNC STATUS   HEALTH STATUS
applicationsets            Synced        Healthy
argocd                     Synced        Healthy
ns-product-helloapi-dev    Synced        Healthy
ns-product-helloapi-prod   Synced        Healthy
product-helloapi-dev       Synced        Healthy
product-helloapi-prod      Synced        Healthy
root                       Synced        Healthy
all 7 Synced/Healthy in 19s

kubectl apply'dan yedi yeşil Application'a on dokuz saniye.

UI'da: make bootstrap'ı UI açıkken çalıştırın. Bu ekran apply'dan beş saniye sonra: root Syncing, argocd Application'ı yeni oluşmuş, applicationsets henüz yok. argocd wave -1'de, applicationsets wave 0'da; root'un sync'i wave sırasını izliyor.

Apply'dan 5 saniye sonra: root Healthy/OutOfSync/Syncing, argocd Application'ı yeni oluşmuş

Yirmi saniye sonra sağ üstteki liste ikonuyla (2) liste görünümüne geçin: yedi Application yeşil. En alttaki root satırına (1) tıklayın.

Liste görünümünde yedi Application Synced/Healthy; root satırı ve liste görünümü ikonu işaretli

root'un ağacı apps/ dizininin kendisi: iki AppProject ve iki Application. Kartlardaki ↗ ikonu o Application'ın kendi sayfasını açıyor.

root Application'ının ağacı: applicationsets ve argocd Application'ları, platform ve products AppProject'leri

applicationsets kartındaki ↗ ile bir kat aşağı inin. Yukarıdaki metin ağacının ikinci yarısı bu ekran: iki ApplicationSet ve ürettikleri dört Application.

applicationsets Application'ının ağacı: namespaces ve products ApplicationSet'leri, her biri dev ve prod için birer Application üretiyor

argocd Application'ında DETAILS'e tıklayın. Summary "This is a multi-source app" diyor, annotation'larda sync-wave=-1 duruyor, IMAGES satırında da chart'ın getirdiği üç image var (argocd v3.5.3, redis 8.6.4, haproxy). SOURCES sekmesi chart'ı ve $values referansını ayrı ayrı gösteriyor.

argocd Application'ının Details paneli: multi-source app, platform projesi, ServerSideApply, Synced/Healthy, image listesi

Paneli kapatın ve sağ üstteki ikinci ikona, Pods görünümüne geçin; GROUP BY NODE kalsın. HA'nın gerçekte neye benzediğini gösteren ekran bu: on üç pod, üç worker'a dağılmış. Adım 5'te bu ekrana geri döneceğiz.

argocd Application'ının Pods görünümü, node'a göre gruplanmış: argocd-ha-worker'da 5, worker2'de 4, worker3'te 4 pod

Güvenmediğim için kontrol ettiğim iki şey:

  • Kendi kurulumunu devralmak hiçbir şeyi yeniden başlatmadı. Argo CD pod'larının hepsi helm install'daki oluşturulma zamanını korudu. Render edilen objeler birebir aynıydı, server-side apply hiçbir pod template'ini değiştirmedi.
  • Helm hook'u tekrar çalıştı. Chart'ın pre-install hook'u (argocd-redis-secret-init, Redis parolasını oluşturan bir Job) Argo CD'de PreSync hook'una dönüştü ve bir kez daha çalıştı. Idempotent olduğu için sorun değil; ama Argo CD'nin Helm çalıştırmadığını, chart'ı render edip Helm hook'larını kendi hook'larına eşlediğini de hatırlatıyor.

Bundan sonra helm upgrade çalıştırmayın. Objelerin sahibi Argo CD; sh.helm.release.v1.argocd.v1 release secret'ı sıfırıncı günün eskimiş bir kaydı olarak kalıyor.

Adım 3 — Ürün tarafı

platform-gitops içindeki products-appset.yaml ürün kaydı: ürün başına bir liste elemanı, ortamlarla çaprazlanıyor.

# platform-gitops/applicationsets/products-appset.yaml (generators + source)
  generators:
    - matrix:
        generators:
          - list:
              elements:
                - product: product-helloapi
                  productRepoURL: https://github.com/miraccan00/product-helloapi-gitops.git
          - list:
              elements:
                - env: dev
                - env: prod
  template:
    metadata:
      name: "{{ .product }}-{{ .env }}"
    spec:
      project: products
      source:
        repoURL: "{{ .productRepoURL }}"
        targetRevision: blog-04
        path: "overlays/{{ .env }}"

products AppProject'i sadece product-*-gitops adlı repoları ve product-* adlı namespace'leri kabul ediyor; cluster-scoped hiçbir şeye izin yok. Bir ürün ekibi yanlışlıkla argocd namespace'ine deploy edemez.

Ürün reposu kendi chart'ını kustomize ile sarıyor:

# product-helloapi-gitops/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

helmGlobals:
  chartHome: ../../

helmCharts:
  - name: chart
    releaseName: product-helloapi
    version: "0.2.0"
    valuesFile: values.yaml

Values dosyasında kustomize.buildOptions: --enable-helm bu yüzden var. O olmadan repo-server'daki kustomize (bu image'da v5.8.1) şu hatayla duruyor:

`: must specify --enable-helm

Servis iki ortamda da hellofiber'dan build edilen image ile cevap veriyor:

$ make hello
Hello from helloapi [DEV] (env=development, version=sha-d96497a)
Hello from helloapi [PROD] (env=production, version=sha-d96497a)

UI'da: Applications sayfasında product-helloapi-prod kartına tıklayın (ya da doğrudan http://localhost:8080/applications/argocd/product-helloapi-prod) ve ağaç araç çubuğundaki Show ApplicationSet parent node düğmesine (1) basın. En soldaki soluk düğüm bu Application'ı üreten products ApplicationSet'i; sağa doğru Service, Deployment → ReplicaSet → üç pod. Kartın altındaki products etiketi AppProject.

product-helloapi-prod ağacı: products ApplicationSet'i, Application, Service, Deployment, ReplicaSet ve üç pod

Adım 4 — Değişiklik çıkarmak bir commit

Prod 2 replica'dan 3'e çıkıyor. product-helloapi-gitops/overlays/prod/values.yaml içinde tek satır, commit, push. kubectl yok.

Webhook olmadan Argo CD Git'i her 120 saniyede, üstüne 60 saniyeye kadar jitter ekleyerek yokluyor (argocd-cm içinde timeout.reconciliation ve timeout.reconciliation.jitter). Bu çalıştırmada:

3/3 ready 91s after push (polling, no webhook)

Webhook, Argo CD'ye Git'e hemen bakmasını söylüyor. Lab bir GitHub webhook'u alamıyor, o yüzden scripts/ship.sh bir webhook'un yaptığı şeyi yapıyor, Application'a bir refresh annotation'ı koyuyor:

product-helloapi-dev: 943189e -> da92092, Synced/Healthy 4s after refresh

91 saniyeye karşı 4. Production'da webhook'u kurun; insanları "sadece bu seferlik" kubectl'e iten şey o 3 dakikalık en kötü durum. Ekiplere ilk gün verilen sayfa docs/how-to-ship-a-change.md: her değişiklik türü nerede duruyor, nasıl geri alınır (UI'daki düğmeyle değil, PR'ı revert ederek), her status ne anlama geliyor.

UI'da: product-helloapi-dev kartına tıklayın (http://localhost:8080/applications/argocd/product-helloapi-dev), üstteki HISTORY AND ROLLBACK düğmesine basın. Her deploy bir commit: hash, yazar, commit mesajı ve Initiated by: automated sync policy. Kimse düğmeye basmadı; Argo CD Git'te yeni commit'i görüp kendisi sync etti.

product-helloapi-dev History and Rollback paneli: revision da92092, "dev: new message", automated sync policy tarafından başlatıldı

Bu repolara push yetkiniz yok, o yüzden bu adımı kendi lab'ınızda birebir tekrarlayamazsınız. Aynı döngüyü ters yönden görmek için Git yerine cluster'ı değiştirin, UI'da product-helloapi-prod açıkken:

kubectl -n product-helloapi-prod scale deploy/product-helloapi --replicas=9

Bir saniye içinde Application OutOfSync ve Progressing oluyor, altı yeni pod oluşmaya başlıyor:

kubectl scale'den 0.7 saniye sonra: product-helloapi-prod OutOfSync ve Progressing, yeni pod'lar oluşuyor

Bir saniye sonra selfHeal Deployment'ı Git'teki 3'e geri çekmiş, altı pod Terminating:

selfHeal sonrası: Application yine Synced/Healthy, dokuz pod'dan altısı Terminating

İkinci kez denerseniz daha uzun sürer; neden olduğu Ne ters gitti #2'de.

Adım 5 — Bozmak

Her arızadan sonra Argo CD'nin hâlâ çalıştığını kanıtlamak için scripts/probe.sh, product-helloapi-prod için hard refresh istiyor ve yeni bir reconciledAt bekliyor. Hard refresh üç hareketli parçanın üçüne de ihtiyaç duyuyor: karşılaştırmayı controller yapıyor, bir repo-server chart'ı cache'i atlayarak yeniden render ediyor ve sonuç Redis'ten geçiyor.

Aynı kontrolü UI'dan da yapabilirsiniz: product-helloapi-prod sayfasında REFRESH'in yanındaki ok (1) → Hard Refresh (2). Sağlıklı bir cluster'da REFRESH düğmesi soluklaşıyor, LAST SYNC'in altında Refreshing rozeti çıkıyor ve birkaç saniyede kayboluyor (bu çalıştırmada 4 saniyeden kısa). Rozet kaybolmuyorsa bir şeyler ters gidiyor. LAST SYNC'teki zaman ise değişmiyor: o son sync'in zamanı ve refresh yeni bir sync başlatmıyor, sadece karşılaştırmayı yeniliyor.

product-helloapi-prod sayfasında Refresh açılır menüsü, Hard Refresh seçeneği işaretli

make failover'ı ikinci terminalde çalıştırmadan önce tarayıcıda iki sekme açın: birinde argocd Application'ı Pods görünümünde (http://localhost:8080/applications/argocd/argocd?view=pods), diğerinde product-helloapi-prod (http://localhost:8080/applications/argocd/product-helloapi-prod). Arızaları ilk sekmede izleyeceğiz, Hard Refresh'i ikincisinden isteyeceğiz. Script yaklaşık dokuz dakika sürüyor ve her aşamanın başında saat yazıyor; aşağıdaki ekranları o saatlere göre takip edin.

$ make failover
baseline: hard refresh reconciled in 2s

== 1. Redis master
12:34:22 master is argocd-redis-ha-server-1 (sentinel: 10.96.201.134), deleting
right after redis master delete: hard refresh reconciled in 6s
12:34:34 sentinel promoted 10.96.229.162 after 12s

== 2. repo-server
12:34:34 deleting argocd-repo-server-566fc4b49f-hxtxz
right after repo-server delete: hard refresh reconciled in 1s

== 3. node running the application controller
12:34:36 controller on argocd-ha-worker2; on the same node:
   argocd-application-controller-0
   argocd-redis-ha-haproxy-647d9bfdf8-5l94l
   argocd-redis-ha-server-2
   argocd-repo-server-566fc4b49f-jkcw6
12:34:36 docker stop argocd-ha-worker2
with argocd-ha-worker2 down: no reconciliation for 420s (last: 2026-09-28T09:34:34Z)
12:41:37 controller pod:
   argocd-application-controller-0 Terminating argocd-ha-worker2
   node argocd-ha-worker2 NotReady
12:41:37 docker start argocd-ha-worker2
after argocd-ha-worker2 is back: hard refresh reconciled in 31s

Argo CD HA failover süreleri: Redis master kaybı 6 sn, repo-server kaybı 1 sn, application controller'ın node'u ölünce 7 dakikadan fazla, out-of-service taint'i ile 81 sn

Redis master: Sentinel bir replica'yı 12 saniyede master yaptı (önceki bir denemede 41). O pencere içinde başlatılan refresh bile 6 saniyede bitti. Redis, Argo CD için bir cache; yavaş bir cache kesinti değil. UI'da bu arıza neredeyse görünmüyor: master silindikten bir buçuk saniye sonra worker3'te yeni Redis pod'u mavi (Progressing), Application hâlâ Healthy.

Redis master silindikten 1.5 saniye sonra Pods görünümü: argocd-ha-worker3'te bir pod Progressing, Application Healthy

repo-server: 1 saniye. İsteği diğer replica aldı. Pods görünümünde silinen pod kayboluyor, yerine gelen pod birkaç saniye mavi (Progressing) kalıyor; Application'ın health'i o sürede Progressing, sync durumu Synced. İkinci sekmeden Hard Refresh isterseniz Refreshing rozeti birkaç saniyede kayboluyor.

repo-server silindikten 1.5 saniye sonra Pods görünümü: yeni repo-server pod'u argocd-ha-worker'da Progressing, Application Synced

Controller'ın node'u: yedi dakika hiçbir şey. docker stop, Kubernetes'in gözünde ölü bir node'a benziyor: kubelet raporlamayı bırakıyor, node 40–50 saniye içinde NotReady oluyor (aşağıdaki çalıştırmada 49) ve varsayılan 5 dakikalık toleration'dan sonra pod eviction için işaretleniyor. Bir Deployment için yedek pod burada başlar. StatefulSet için başlamaz. argocd-application-controller-0 bir StatefulSet pod'u ve StatefulSet, ilkinin gittiğinden emin olmadan aynı kimlikle ikinci bir pod başlatmıyor. Kubelet ölüyken bunu kimse doğrulayamıyor, o yüzden pod node kapalı kaldığı sürece Terminating'de bekliyor. API server, iki repo-server ve üç Redis pod'undan ikisi bu süre boyunca sağlıklıydı. Argo CD sadece hiçbir şeyi reconcile etmiyordu.

UI'da bu arıza yanıltıcı. Node bir dakikadır ölü ve NotReady, ama Pods görünümü argocd-ha-worker üzerinde hâlâ beş yeşil pod gösteriyor:

Node öldükten 60 saniye sonra Pods görünümü: argocd-ha-worker'da hâlâ beş yeşil pod

UI'ın gösterdiği canlı durum değil, controller'ın en son yazdığı durum; onu güncelleyecek controller da ölü node'daki beş pod'dan biri. Altıncı dakikada ikinci sekmeye, product-helloapi-prod'a geçin ve Hard Refresh isteyin: Refreshing rozeti gitmiyor, ama Application'ın kartları yine yeşil:

Node ölüyken Hard Refresh: Refreshing rozeti takılı, Application Synced/Healthy görünüyor, son sync 8 dakika önce

Gerçeği kubectl söylüyor:

$ kubectl -n argocd get pods -o wide --field-selector spec.nodeName=argocd-ha-worker
NAME                                               READY   STATUS        NODE
argocd-application-controller-0                    1/1     Terminating   argocd-ha-worker
argocd-applicationset-controller-d864b8898-74w59   1/1     Terminating   argocd-ha-worker
argocd-redis-ha-haproxy-647d9bfdf8-8rsrb           1/1     Terminating   argocd-ha-worker
argocd-redis-ha-server-2                           3/3     Terminating   argocd-ha-worker
argocd-repo-server-566fc4b49f-dh66r                1/1     Terminating   argocd-ha-worker

Buradan çıkan izleme kuralı: Argo CD'nin sağlığını Argo CD'nin UI'ından okumayın. Application'ların reconciledAt zamanını ya da controller'ın metriklerini dışarıdan izleyin.

Bir cloud provider'da node controller, kapatılan VM'in Node objesini siliyor ve pod yoluna devam ediyor. Bare metal'de bunu kimse yapmıyor. Kubernetes'in bunun için verdiği araç out-of-service taint'i (non-graceful node shutdown, 1.28'den beri GA): node'un gerçekten gittiğini siz beyan ediyorsunuz, Kubernetes de pod'larını zorla siliyor.

$ make out-of-service
12:42:31 docker stop argocd-ha-worker2
12:43:20 argocd-ha-worker2 NotReady after 49s
12:43:20 out-of-service taint added
12:43:52 controller Ready on argocd-ha-worker, 81s after the node died
controller moved: hard refresh reconciled in 3s
12:43:57 argocd-ha-worker2 back, taint removed

UI'da taint'in etkisi Pods görünümünde görünüyor: controller artık argocd-ha-worker3'te, geri dönen argocd-ha-worker'da ise zorla silinip yeniden oluşturulan Redis ve HAProxy pod'ları Progressing.

Out-of-service taint'inden sonra Pods görünümü: argocd-ha-worker'da iki Progressing pod, worker2'de 5, worker3'te 6 pod

"Biri fark ettiğinde" yerine 81 saniye. Taint'in bir insan kararı olması bilinçli: node sadece ağdan kopmuş ama hâlâ çalışıyorsa, iki controller aynı cluster'a yazar. Komutuyla birlikte "node öldü" runbook'una koyun ve node geri geldiğinde taint'i kaldırın.

Ne ters gitti

1. Label'ı olmayan bir ConfigMap Argo CD'yi düşürüyor. Argo CD kendi ayarlarını sadece isimle değil label ile buluyor. Önceki bir işte, upstream manifest'lerin üstüne uygulanan düz bir argocd-cm patch'i büyük harflerle yazılmış bir uyarıyla başlıyordu: part-of label'ını koru, yoksa her bileşen CrashLoop'a girer. Burada o tek label'ı silerek tekrar ürettim:

$ kubectl -n argocd label cm argocd-cm app.kubernetes.io/part-of-
time="2026-09-28T09:44:50Z" level=fatal msg="configmap \"argocd-cm\" not found"
argocd-server-8646bcb568-2rtwd   0/1   CrashLoopBackOff   3 (12s ago)   51s
argocd-server-8646bcb568-bfdt7   0/1   CrashLoopBackOff   3 (12s ago)   51s

ConfigMap orada duruyor; mesaj "not found" diyor çünkü arama app.kubernetes.io/part-of: argocd ile filtreliyor. selfHeal label'ı 40 saniye içinde geri koymadı, ben elle koydum. (O anda Redis bir önceki testten hâlâ toparlanıyordu, bkz. #3; o yüzden 40 saniyeye fazla anlam yüklemem. Çöküşün kendisi anında.) Buradan iki kural çıktı: ayarlar chart'ın configs.cm / configs.params değerlerinden geçer, asla ayrı bir ConfigMap'ten değil; kendini yöneten argocd Application'ı da prune: false kalır. Aynı production notlarında Argo CD'ye kendini yönettirmenin diğer sebebi de vardı: bir kez make ile uygulanmış config aylarca kaymıştı. OIDC issuer yanlış realm'i gösteriyordu, bir secret manifest'i hiç uygulanmamıştı ve UI route'u 404 dönüyordu. Kimse fark etmedi, çünkü Git ile cluster'ı karşılaştıran hiçbir şey yoktu.

Kendi lab'ınızda denerseniz ne göreceğiniz, pod'ların o sırada yeniden başlayıp başlamadığına bağlı. İkinci çalıştırmada label'ı sildim: iki argocd-server pod'u 90 saniye boyunca Running kaldı ve UI çalışmaya devam etti, çünkü server bu ConfigMap'i açılışta okuyor. kubectl -n argocd rollout restart deploy/argocd-server ile yeni bir pod başlatınca o pod aynı configmap "argocd-cm" not found hatasıyla hemen CrashLoop'a girdi. RollingUpdate eski iki pod'u yeni pod hazır olmadan silmediği için UI yine ayaktaydı. Yani bu hata o an sessiz kalıyor, bir sonraki restart'ta, node drain'de ya da Argo CD yükseltmesinde patlıyor. Label'ı geri koyup server'ı yeniden başlatınca yedi Application'ın üçü Unknown'daydı; controller hepsini yaklaşık iki dakikada tekrar yeşile çevirdi:

kubectl -n argocd label cm argocd-cm app.kubernetes.io/part-of=argocd
kubectl -n argocd rollout restart deploy/argocd-server   # CrashLoop'taki pod'u beklememek için

2. selfHeal geri çekiliyor (backoff) ve ilk ölçümümü yanılttı. İlk failover script'im prod Deployment'ını elle 9'a çıkarıp selfHeal'in onu geri almasını ölçüyordu. İlk geri alma 1 saniye sürdü, sonrakiler 6, 54 ve 154 saniye. Bu arızalar değildi, backoff'tu. Controller logu bunu söylüyor:

Skipping auto-sync: already attempted sync to [da92092b10bb...] with timeout 0s (retrying in 4m56.613839256s)

Aynı commit üzerinde tekrarlanan self-heal'ler 2 saniye bekliyor, sonra her seferinde ×3, en fazla 300 saniye (--self-heal-backoff-timeout-seconds, -factor, -cap-seconds). İyi bir varsayılan: Argo CD'nin bir HPA ya da bir operator ile sonsuza kadar kavga etmesini engelliyor. Ama "selfHeal drift'i saniyeler içinde geri alır" cümlesi sadece ilk seferde doğru. Failover kontrollerini backoff'u olmayan hard refresh'e çevirdim.

3. Out-of-service testinden sonra Redis kendiliğinden geri gelmedi. Taint, argocd-redis-ha-server-2'yi de zorla sildi ve pod başka bir yere taşınamadı (zorunlu anti-affinity, diğer iki worker'da zaten birer Redis pod'u vardı). Node geri geldiğinde Sentinel master'ı birkaç kez değiştirdi ve server-1 startup probe'unda role=slave; repl=connect ile takılı kaldı. StatefulSet OrderedReady olduğu için server-1 Ready olmadan server-2'yi oluşturmadı. Application'lar Unknown'a düştü (UI'da hangilerinin etkilendiğini sol paneldeki SYNC STATUS filtresinden Unknown'ı seçerek görebilirsiniz), controller NOREPLICAS Not enough good replicas to write logladı. argocd-redis-ha-server-1'i silmek çözdü: 63 saniye sonra üç Redis pod'u da 3/3 idi ve yedi Application 14 saniyede tekrar yeşildi. Her seferinde olmuyor: lab'ı sıfırdan kurduğum ikinci çalıştırmada Redis 100 saniye içinde kendiliğinden 3/3'e döndü. Yine de Redis pod'u çalıştıran bir node'a out-of-service taint'i uygularsanız, sonrasında kubectl -n argocd get pods -l app=redis-ha ile kontrol edin.

4. ApplicationSet'teki sync wave süs. products-appset.yaml, ürettiği Application'lara argocd.argoproj.io/sync-wave: "0", namespaces-appset.yaml de "-1" koyuyordu; fikir namespace'lerin önce gelmesiydi. Wave'ler tek bir Application'ın sync'i içindeki kaynakları sıralar. Üretilen Application'ları ApplicationSet controller oluşturuyor, bir üst Application'ın kaynakları olarak sync edilmiyorlar; o yüzden bu annotation'lar hiçbir şeyi sıralamıyor. Bu çalıştırmada namespace Application'ları ürün Application'larından bir saniye önce bitti ve hiçbir şey fail olmadı. Bu zamanlamaydı. Serinin ilerisinde ApplicationSet'lere düzgünce bakıyoruz; şimdilik bu wave'lere güvenmeyin.

Doğrulama

make status        # 7 Application Synced/Healthy; 3 worker'a dağılmış 13 Argo CD pod'u
make hello         # iki ortam da overlay'deki image tag ile cevap veriyor
make probe         # hard refresh birkaç saniyede reconcile oluyor
kubectl -n argocd get pdb    # server, repo-server, applicationset: minAvailable 1
kubectl -n argocd get cm argocd-cm -o jsonpath='{.data.timeout\.reconciliation}'   # 120s

UI'da da aynı kontroller:

  • http://localhost:8080/applications: yedi kart, hepsi Healthy ve Synced; sol paneldeki SYNC STATUS filtresinde Synced 7, Unknown ve OutOfSync 0.
  • argocd → Pods görünümü: üç worker kartı, toplam on üç yeşil pod, mavi ya da kırmızı yok.
  • product-helloapi-prod → REFRESH oku → Hard Refresh: sayfanın üstündeki Refreshing rozeti birkaç saniyede kayboluyor.
  • ApplicationSets sayfası: namespaces ve products, ikisi de Healthy, her biri 2 Application.

Kaynaklar: argo/argo-cd chart README'si ("HA mode", en az 3 worker node), Argo CD dokümanları High Availability ve Cluster Bootstrapping (app of apps), self-heal backoff bayrakları için v3.5.3'te argocd-application-controller --help, Kubernetes dokümanı Non-Graceful Node Shutdown.

HA size ne kazandırıyor, tek tabloda

Arıza Ne oldu Sizin yapacağınız
Redis master pod'u ölür Sentinel 12–41 sn'de yeni master seçer; refresh yine 6 sn'de çalıştı hiçbir şey
Bir repo-server ölür diğeri karşılar; 1 sn hiçbir şey
Bir API server ölür UI/CLI diğerinden devam hiçbir şey
Controller'ın node'u ölür node dönene kadar reconciliation yok out-of-service taint'i → 81 sn'de geri; sonra Redis'i kontrol et
Biri argocd-cm'i elle düzenler bileşenler CrashLoop'a girer ayarlar sadece chart values üzerinden

Bu seride

Bu, Production'da GitOps serisinin ilk yazısı. Blogdaki önceki yazı: Cluster API Docker Provider Talos'u Neden Ayağa Kaldıramaz · Sonraki: Argo CD SSO: admin Parolası Yerine İnsanlar için OIDC ve RBAC. Bu lab'da Dex bilerek kapalı; geri döndüğü yer orası.

Bu yazının kodu: https://github.com/miraccan00/blog-wiki/tree/main/argocd-ha-app-of-apps

İnsanın en büyük hatalarından biri de doğru zamanı yanlış kişilerle doldurmaktır.
Charles Bukowski