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:
| Option | Ne yapar | Dikkat |
|---|---|---|
CreateNamespace=true | Destination namespace'i yoksa yaratır | Namespace'in label'ları için managedNamespaceMetadata gerekir |
ServerSideApply=true | kubectl'in server-side apply'ını kullanır; büyük CRD'ler ve alan sahipliği için | 3.6'dan beri diff de server-side diff; API server'a dry-run yükü |
PruneLast=true | Silmeleri sync'in sonuna bırakır | Wave'lerle birlikte dördüncü yazıda |
ApplyOutOfSyncOnly=true | Yalnız OutOfSync resource'ları uygular | Binlerce resource'lu uygulamalarda sync'i kısaltır |
Prune=false | Resource'u prune'dan korur | Annotation olarak, nesne nesne |
Replace=true | apply yerine replace (gerekirse create) | Başkalarının yazdığı alanları siler; kalıcı ayar yapma |
RespectIgnoreDifferences=true | ignoreDifferences'taki alanları sync'te de korur | Yalnı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.