Mert Özen Her Satırda Daha İleri
DevOps GitOps ve Argo CD — #03

Argo CD'de Sync, Health ve Diff: Automated Sync, Prune, Self-Heal ve Sync Option'ları

Mert Özen 15 Kas 2026 10 dk 5 görüntülenme
Argo CD'de Sync, Health ve Diff: Automated Sync, Prune, Self-Heal ve Sync Option'ları

Argo CD'de sync status Git ile cluster'ın aynı olup olmadığını, health status ise cluster'daki nesnelerin çalışıp çalışmadığını söyler; ikisi bağımsızdır. Synced ama Degraded bir deploy'u, self-heal'in elle yapılan scale'i bir saniyede geri almasını ama elle eklenen env değişkenini görmemesini, prune'un bir StatefulSet'i silmesini ve Prune=false, ignoreDifferences, RespectIgnoreDifferences ile sync option'larını gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler kind 0.33 ile Kubernetes 1.37, Argo CD 3.6.1, Kustomize 5.8 ve .NET 10 ile denenmiştir.

Önce Tahmin Et

Bookstore API'sinin dev Application'ında automated sync ve self-heal açık. İlk yazıdaki olay tekrar yaşanıyor: biri kubectl scale ile replica'yı 3'e çıkarıyor, bir de kubectl set env ile Logging__LogLevel__Default=Debug ekliyor. Argo CD bu iki değişikliği ne kadar sürede geri alır? İpucu: iki cevap da "üç dakika" değil ve ikisinden biri hiç geri alınmıyor.

Sync Status ve Health Status: İki Ayrı Soru

Bir önceki yazıda sitedeki serilerin ortak örnek uygulaması Bookstore'u (bir online kitapçı) Argo CD'nin ilk Application'ıyla dev ortamına koyduk. Argo CD her Application için iki bağımsız durum hesaplar. Sync status Git ile cluster'ı karşılaştırır: Synced ya da OutOfSync. Health status cluster'daki nesnelerin çalışıp çalışmadığına bakar: Healthy, Progressing, Degraded, Suspended, Missing ya da Unknown. İkisinin neden ayrı olduğunu görmek için registry'de olmayan bir tag'i dev'e commit'ledik. Base'e önce progressDeadlineSeconds: 120 ekledik; Kubernetes serisinin dördüncü yazısında gördüğümüz gibi bu süre dolunca Deployment rollout'u "takıldı" diye işaretler.

Sync Status:        Synced to main (8852c9d)
Health Status:      Progressing

GROUP  KIND         NAMESPACE      NAME      STATUS  HEALTH       HOOK  MESSAGE
apps   Deployment   bookstore-dev  api       Synced  Progressing        deployment.apps/api unchanged

Sync başarılı: Git'teki Deployment cluster'da. Yeni pod ise ErrImagePull'da ve eski 1.1.0 pod'u trafiği taşımaya devam ediyor. İki dakika sonra:

Health Status:      Degraded

GROUP  KIND         NAMESPACE      NAME      STATUS  HEALTH    HOOK  MESSAGE
apps   Deployment   bookstore-dev  api       Synced  Degraded        deployment.apps/api unchanged
Updated health status: Progressing -> Degraded (Caused by apps/Deployment:bookstore-dev/api)
ReplicaSet "api-57ffbcfc94" has timed out progressing.

Synced ve Degraded aynı anda. Event mesajındaki "(Caused by ...)" eki 3.6 ile geldi: health değişikliğine hangi resource'un sebep olduğunu en fazla üç tane olarak yazıyor. Düzeltme yine Git'te: git revert commit'i automated sync ile uygulandı ve uygulama Healthy'ye döndü. Argo CD kendi başına eski sürüme dönmez; automated sync açıkken argocd app rollback da kullanılamaz, geri dönüşün yolu Git.

Health Nasıl Hesaplanır?

Argo CD standart Kubernetes tipleri için built-in health check'lerle gelir. Deployment, ReplicaSet, StatefulSet ve DaemonSet için observed generation'ın desired generation'a eşit olmasına ve güncellenmiş replica sayısının istenen sayıya ulaşmasına bakar; progressDeadlineSeconds aşılınca Degraded olur. Service yalnız LoadBalancer tipindeyse adres atanmasını bekler. PersistentVolumeClaim için Bound olmak, Job için tamamlanmak gerekir. Uygulamanın toplam health'i en kötü resource'un health'idir; tek bir Degraded Deployment bütün Application'ı Degraded yapar.

CRD'ler (Custom Resource Definition; Kubernetes'e eklenen özel nesne tipleri) için durum farklı: her operator status alanını kendi biçiminde yazar. Argo CD'nin içinde yüzlerce CRD için hazır Lua script'leri var; 3.6 bunlara CloudNativePG'nin Database ve Backup nesnelerini, cert-manager'ın CertificateRequest, ACME (Let's Encrypt'in kullandığı otomatik sertifika protokolü) Order ve Challenge nesnelerini, Velero, Tekton, Kyverno ve Gateway API'nin TLSRoute ve ListenerSet tiplerini ekledi. Hazırı olmayan bir CRD için argocd-cm ConfigMap'ine resource.customizations.health.<group>_<kind> anahtarıyla kendi Lua script'ini yazarsın; script obj değişkenindeki nesneye bakıp status ve message döner. 3.6'nın bir yeniliği de silinmekte olan resource'lar: varsayılan check artık bekleyen finalizer'ları listeliyor ve custom check'ler bir deletionMessage dönebiliyor; "Pending deletion"da takılı bir nesnenin neyi beklediğini görmek kolaylaştı.

Bir de şunu bil: Application nesnesinin kendi health check'i 1.8'de kaldırıldı. App of Apps yapısında üst Application, alt Application'ların health'ini varsayılan olarak beklemez; bu konuya beşinci yazıda döneceğiz.

Automated Sync, Prune ve Self-Heal

Config repo'sundaki Application tanımına sync policy ekledik ve kubectl apply ile uyguladık:

  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - PruneLast=true
Sync Policy:        Automated (Prune)
Sync Status:        Synced to main (274d30f)
Health Status:      Healthy

Üç ayar üç ayrı şey söylüyor. automated: Application OutOfSync olduğunda Argo CD sync'i kendisi başlatır; Git'te yeni commit görünce artık bizim argocd app sync yazmamıza gerek yok. Argo CD aynı commit için başarısız olmuş bir otomatik sync'i tekrar denemez; aynı commit'te başarılı bir sync varsa da ikinci kez sync yapmaz, self-heal açık değilse. selfHeal: değişiklik Git'ten değil cluster'dan gelirse de sync yapar. prune: Git'ten silinen resource'ları cluster'dan siler; varsayılanı güvenlik için kapalı. Önce Tahmin Et'in ilk yarısı:

scale at 06:12:52.465
06:12:50.459 1
06:12:52.713 3
06:12:53.265 1

Yarım saniyede bir spec.replicas'ı okuyan bir döngü 3'ü yalnız bir kez gördü; bir saniye dolmadan değer tekrar 1'di. Polling'in üç dakikası burada geçerli değil: controller Kubernetes API'sini watch ediyor, canlı nesne değişince hemen karşılaştırıyor. Application'ın event'leri:

Normal   OperationStarted     application/bookstore-api-dev   Initiated automated sync to '274d30fe5d72ce9c05587873ab0b800f9d2772cc'
Normal   ResourceUpdated      application/bookstore-api-dev   Updated sync status: Synced -> OutOfSync
Normal   ResourceUpdated      application/bookstore-api-dev   Updated health status: Healthy -> Progressing (Caused by apps/Deployment:bookstore-dev/api)
Normal   OperationCompleted   application/bookstore-api-dev   Sync operation to 274d30fe5d72ce9c05587873ab0b800f9d2772cc succeeded
Normal   ResourceUpdated      application/bookstore-api-dev   Updated sync status: OutOfSync -> Synced
Normal   ResourceUpdated      application/bookstore-api-dev   Updated health status: Progressing -> Healthy

Arka arkaya gelen drift'lerde Argo CD kendini frenler: argocd-cmd-params-cm'deki varsayılanlara göre self-heal denemeleri arasında 2 saniyeden başlayıp üçer kat büyüyen ve 300 saniyede duran bir backoff var. Cluster'daki bir başka controller aynı alanla sürekli kavga ediyorsa (Argo CD yazar, öteki geri yazar) bu backoff API server'ı korur, ama kavgayı bitirmez; onu bitirecek olan aşağıdaki ignoreDifferences.

Self-Heal'in Görmediği Drift

Önce Tahmin Et'in ikinci yarısı:

kubectl -n bookstore-dev set env deployment/api Logging__LogLevel__Default=Debug
Sync Status:        Synced to main (274d30f)
Health Status:      Healthy
ASPNETCORE_ENVIRONMENT Database__Provider ConnectionStrings__BookstoreDb Logging__LogLevel__Default

Env değişkeni kaldı ve Application Synced. Health kısa bir süre Progressing'e geçti (yeni ReplicaSet açıldı) ama sync status hiç OutOfSync olmadı. Sebep ilk yazıdaki kubectl apply ile aynı: Argo CD'nin varsayılan diff'i last-applied-configuration annotation'ını kullanan üç yönlü bir karşılaştırma ve Git'te hiç olmamış bir liste elemanını "başkasının eklediği alan" sayıyor. Bu bir hata değil, bilinçli bir tasarım: aksi hâlde mutating webhook'ların eklediği sidecar'lar ya da controller'ların yazdığı alanlar yüzünden hiçbir uygulama Synced olamazdı. ServerSideApply=true sync option'ıyla tekrar denedik; 3.6'da bu seçenek diff'i de server-side diff'e çeviriyor, ama sonuç değişmedi: alanın sahibi kubectl set env.

Bu tür bir drift'i silmenin yolu bir replace sync'i:

argocd app sync bookstore-api-dev --replace --resource apps:Deployment:bookstore-dev/api
Deployment api deployment.apps/api replaced
ASPNETCORE_ENVIRONMENT Database__Provider ConnectionStrings__BookstoreDb

Replace, nesneyi Git'teki hâliyle baştan yazar; Git'te olmayan her alan gider. Tehlikesi de burada: HPA (HorizontalPodAutoscaler; yüke göre replica sayısını ayarlayan Kubernetes nesnesi) ya da bir operator'ın yazdığı alanlar da gider. Ayrıca immutable alanı değişen bir nesnede replace silip yeniden yaratmaya kadar gidebilir. Replace=true'yu bir Application'ın kalıcı sync option'ı yapma; böyle bir drift'i görürsen elle, tek resource için ve bilerek kullan.

Prune: Git'ten Silinen Silinir

Prune açıkken Git'ten bir dosya kaldırmak o resource'u silmek demek. Önce bir kazayı deneyelim. Dev overlay'inde resources satırını yorum satırına alıp commit'ledik; overlay artık boş bir çıktı üretiyor:

CONDITION  MESSAGE                                                                                                     LAST TRANSITION
SyncError  Skipping sync attempt to [5ca0c415e447b2d5e5ddc6008f0d60af3afad242]: auto-sync will wipe out all resources  2026-11-15 06:14:43 +0300 +03

Argo CD durdu. allowEmpty varsayılan olarak kapalı ve "her şeyi sil" sonucunu otomatik sync ile uygulamıyor. Bu korumanın sınırı da var: yalnız sonuç tamamen boşsa çalışıyor. İkinci denemede base'den yalnız postgres.yaml'ı çıkardık, sanki veritabanını başka bir klasöre taşıyıp taşıma commit'ini unutmuşuz gibi:

GROUP  KIND         NAMESPACE      NAME      STATUS     HEALTH   HOOK  MESSAGE
apps   StatefulSet  bookstore-dev  postgres  Succeeded  Pruned         pruned
       Service      bookstore-dev  api       Synced     Healthy
apps   Deployment   bookstore-dev  api       Synced     Healthy
NAME                                    STATUS   VOLUME                                     CAPACITY
persistentvolumeclaim/data-postgres-0   Bound    pvc-ec9a9806-7bca-41b9-bd61-8359f3d7005c   1Gi

StatefulSet ve postgres Service'i silindi, 15 saniye sonra API'nin readiness probe'u veritabanına ulaşamadı ve Application Progressing'e düştü. Veri ise kurtuldu: StatefulSet'in volumeClaimTemplates'inden doğan PVC'yi (PersistentVolumeClaim; pod'un istediği kalıcı disk) Argo CD yaratmadığı için takip etmiyor, dolayısıyla prune etmiyor. git revert commit'iyle StatefulSet geri geldi, aynı PVC'ye bağlandı ve uygulama Healthy oldu.

Veritabanı gibi kaybı pahalı nesneler için resource düzeyinde bir fren var:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  annotations:
    # Git'ten kaybolsa bile veritabanını otomatik olarak silme.
    argocd.argoproj.io/sync-options: Prune=false

Aynı kazayı annotation'la tekrarladık:

Sync Status:        OutOfSync from main (8cfb652)

GROUP  KIND         NAMESPACE      NAME      STATUS     HEALTH   HOOK  MESSAGE
apps   StatefulSet  bookstore-dev  postgres  OutOfSync  Healthy        ignored (no prune)

StatefulSet yerinde kaldı ve Application OutOfSync olarak "burada Git'te olmayan bir şey var" demeye devam etti. Annotation'ı yalnız StatefulSet'e koyduğumuz için postgres Service'i yine silindi; fren, nesne nesne konur. PruneLast=true ise silme işlemlerini sync'in sonuna bırakır: yeni nesneler önce yaratılır, eskiler en son silinir. Bir resource'u taşırken (eski adla silinip yeni adla yaratılırken) arada bir boşluk kalmasını önler.

ignoreDifferences: Git'in Sahibi Olmadığı Alanlar

Self-heal açıkken bir HPA replica sayısını değiştirirse ne olur? Argo CD her seferinde Git'teki değere geri yazar, HPA tekrar değiştirir. Kubernetes serisinin on birinci yazısında bu tuzağı, on dördüncü yazısında da Kustomize tarafındaki çözümünü görmüştük: HPA varken manifest'ten replicas'ı kaldırmak. Argo CD tarafındaki karşılığı bu alanı karşılaştırmadan çıkarmak:

  # Replica sayısının sahibi Git değil, HPA (ya da olay anında nöbetçi).
  ignoreDifferences:
    - group: apps
      kind: Deployment
      name: api
      jsonPointers:
        - /spec/replicas

Elle 3'e çıkardık ve Application Synced kaldı. Sonra Git'te alakasız bir değişiklik yaptık (revisionHistoryLimit: 5) ve otomatik sync çalıştı:

2d54b18 api: keep five old ReplicaSets
Sync Status:        Synced to main (2d54b18)
api   1/1   1     1     6m4s

Replica yine 1. ignoreDifferences yalnız karşılaştırmayı etkiliyor; sync sırasında Git'teki manifest'in tamamı uygulanıyor ve replicas: 1 de onunla gidiyor. Sync'in de bu alanlara dokunmaması için ayrı bir option gerekiyor:

    syncOptions:
      - PruneLast=true
      - RespectIgnoreDifferences=true
5489d22 api: keep three old ReplicaSets
Sync Status:        Synced to main (5489d22)
api   3/3   3     3     6m21s
3

Bu sefer sync revisionHistoryLimit'i 3'e çekti ve replica sayısı 3'te kaldı. Mutating webhook'ların eklediği alanlar, cert-manager'ın doldurduğu caBundle'lar ve operator'ların yönettiği alanlar için aynı yöntem kullanılır; jsonPointers yerine jqPathExpressions ya da belirli bir field manager'ın sahip olduğu alanları dışarıda bırakan managedFieldsManagers da yazılabilir. Aynı kural bütün uygulamalar için geçerliyse argocd-cm'de sistem genelinde tanımlanabilir.

Sync Option'ları

Sync davranışını değiştiren option'lar Application'da (spec.syncPolicy.syncOptions) ya da tek bir resource'ta (argocd.argoproj.io/sync-options annotation'ı) yazılır. En sık kullanılanlar:

OptionNe yaparDikkat
CreateNamespace=trueDestination namespace'i yoksa yaratırNamespace'in label'ları için managedNamespaceMetadata gerekir
ServerSideApply=truekubectl'in server-side apply'ını kullanır; büyük CRD'ler ve alan sahipliği için3.6'dan beri diff de server-side diff; API server'a dry-run yükü
PruneLast=trueSilmeleri sync'in sonuna bırakırWave'lerle birlikte dördüncü yazıda
ApplyOutOfSyncOnly=trueYalnız OutOfSync resource'ları uygularBinlerce resource'lu uygulamalarda sync'i kısaltır
Prune=falseResource'u prune'dan korurAnnotation olarak, nesne nesne
Replace=trueapply yerine replace (gerekirse create)Başkalarının yazdığı alanları siler; kalıcı ayar yapma
RespectIgnoreDifferences=trueignoreDifferences'taki alanları sync'te de korurYalnız var olan nesnelerde etkili

Üretimde Dikkat

Automated sync ve self-heal dev'de baştan açılabilir; prod'da önce bir süre manual sync ile Argo CD'nin ne göreceğini izlemek mantıklı. Prune'u açmadan önce Application'daki OutOfSync listesini temizle ve kritik nesnelere (veritabanı, PVC, namespace) Prune=false koy. Alarmı health'e kur, sync status'e değil: Synced ama Degraded bir uygulama, OutOfSync ama Healthy bir uygulamadan daha acildir. argocd_app_info metriği ve notifications bunun için var (son yazı). Sync'in de bir zaman sınırı yok: controller.sync.timeout.seconds varsayılanı 0, yani takılan bir sync sonsuza kadar bekleyebilir; uzun işler için bunu ve retry ayarlarını bilinçli seç.

Sık Düşülen Tuzaklar

Self-heal açıkken olay anında kubectl ile hotfix yapmaya çalışmak. Değişiklik bir saniye içinde geri alınır ve olayın ortasında bunu anlamak zaman kaybettirir. Ya düzeltmeyi Git'e yaz ya da önce Application'da otomatik sync'i kapat (spec.syncPolicy.automated.enabled: false), sonra bunu Git'e de yansıt.

Prune'u ilk günden açıp yanlış bir path ya da eksik bir resources satırıyla namespace'i boşaltmak. allowEmpty yalnız tamamen boş sonucu durdurur, yarısını silen bir commit'i durdurmaz.

Synced'i "her şey yolunda" diye okumak. Synced ve Degraded aynı anda olabilir.

ignoreDifferences yazıp RespectIgnoreDifferences'ı unutmak; HPA'nın kararı bir sonraki sync'te ezilir.

Self-heal'in her drift'i kapattığını sanmak. Git'te hiç olmamış alanlar diff'te görünmez.

Küçük Bir Deneme

★☆☆ Self-heal açıkken Deployment'a kubectl label ile yeni bir label ekle, sonra Git'te zaten olan app.kubernetes.io/part-of label'ının değerini değiştir. Argo CD hangisini geri alıyor? Neden?

★★☆ Bir HPA ekle (metrics-server kurulu değilse minReplicas: 2 yeterli) ve ignoreDifferences olmadan Argo CD ile HPA'nın replica alanında nasıl kavga ettiğini event'lerden izle. Sonra iki farklı çözümü dene: overlay'den replicas'ı kaldırmak ve ignoreDifferences ile RespectIgnoreDifferences.

★★★ Kendi CRD'n için (ör. bir Book CRD'si) status.phase alanına bakan bir Lua health check yaz: Ready ise Healthy, Failed ise Degraded, aksi hâlde Progressing. Check'i argocd-cm'e ekleyip bir nesnenin durumunu değiştirerek UI'da sonucu doğrula.

Sync, health ve diff'in neyi gördüğünü artık biliyoruz. Bir sonraki yazıda sync'in içindeki sıraya bakıyoruz: migration Job'ını deploy'dan önce çalıştırmak için sync wave'ler ve hook'lar.