Argo Rollouts ile Progressive Delivery: Prometheus Metrikleriyle Otomatik Canary Analizi
Argo Rollouts, Deployment'ın yerine geçen bir Rollout ile yeni sürümü adım adım açar ve Prometheus'tan okuduğu metriklere göre ilerler ya da kendiliğinden geri döner. Bookstore API'sini Rollout'a çeviriyor, prod'da %25 ve %50 adımlı bir canary kuruyor, iyi bir sürümün geçişini ve %30 hata dönen bir sürümün 92 saniyede abort edilmesini, ardından Git'i revert ile gerçeğe uydurmayı 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, Argo Rollouts 1.10.2, Prometheus 3.16, .NET 10 ve OpenTelemetry .NET 1.21 (OpenTelemetry.Exporter.Prometheus.AspNetCore 1.21.0-beta.1) ile denenmiştir.
Önce Tahmin Et
Bookstore API'sinin 1.4.0 sürümü /api isteklerinin yaklaşık üçte birinde 500 dönüyor. Bu sürüm dev'den ve staging'den geçti, çünkü iki ortamın PostSync smoke test'i yalnız /health'e bakıyor. Prod'a giden pull request merge ediliyor; prod'da dört pod var ve yeni sürüm önce %25'e çıkıyor. Kim fark eder, ne kadar sürede ve sonra Git'te ne yazar? Yazının ortasında sorunun cevabı dakikası dakikasına duruyor: Argo Rollouts sürümü 92 saniyede geri çekti, ama hikâye orada bitmedi.
Deployment Yerine Rollout
Kubernetes'in Deployment'ı rolling update yapar: yeni pod'lar hazır oldukça eskiler kapanır ve yeni sürümün hata oranına hiç bakılmaz. Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API'si için istediğimiz şey Deployment Stratejileri serisinin canary yazısındaki model: yeni sürüm önce küçük bir payla çıkar, metrikler iyiyse büyür, kötüyse geri döner. O yazı teoriyi ve karar metriklerini anlattı; burada aynı işi bir controller'a, Argo Rollouts'a yaptırıyoruz. Rollouts, Deployment'ın yerine geçen bir Rollout CRD'si (Custom Resource Definition; Kubernetes'e eklenen özel nesne tipi) ve onu yöneten bir controller getiriyor. Kurulum resmi manifest'le, ama bir farkla:
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/download/v1.10.2/install.yaml
CustomResourceDefinition.apiextensions.k8s.io "rollouts.argoproj.io" is invalid: metadata.annotations: Too long: may not be more than 262144 bytes
Client-side apply, nesnenin tamamını last-applied-configuration annotation'ına yazar; Rollout CRD'sinin şeması bu sınırı aşıyor. İkinci yazıda Argo CD'yi kurarken yaptığımız gibi --server-side ile tekrar uyguladık. Sonra base'deki deployment.yaml'ı rollout.yaml yaptık; pod template'i aynı, değişen yalnız tip ve strateji:
# Deployment yerine Rollout (8. yazı): aynı pod template'i, üstüne bir release stratejisi.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: api
annotations:
argocd.argoproj.io/sync-wave: "1"
labels:
app.kubernetes.io/name: api
spec:
replicas: 1
revisionHistoryLimit: 3
progressDeadlineSeconds: 120
selector:
matchLabels:
app.kubernetes.io/name: api
# Adımsız bir canary Rollout rolling update gibi davranır (dev, staging);
# prod overlay'i adımları ve analizi ekler.
strategy:
canary: {}
template:
Dev ve staging'de canary istemiyoruz; orada her sürüm hemen tamamen çıksın. Prod overlay'i bir patch ile adımları, analizi ve replica sayısını ekliyor:
# Prod release'leri canary'den geçer: %25 -> %50 -> %100, arka planda Prometheus analizi.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: api
spec:
replicas: 4
strategy:
canary:
analysis:
templates:
- templateName: api-canary-health
# Analize 1. adımda (ilk setWeight'ten sonra) başla, başarısız olursa rollout'u abort et.
startingStep: 1
args:
- name: namespace
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: canary-hash
valueFrom:
podTemplateHashValue: Latest
steps:
- setWeight: 25
- pause: {duration: 2m}
- setWeight: 50
- pause: {duration: 2m}
Prod overlay'inde önceden replicas: [{name: api, count: 2}] vardı. Kustomize'ın replicas alanı yalnız bildiği tipleri değiştiriyor ve Rollout'u görünce build'i durdurdu: resource with name api does not match a config with the following GVK [Deployment... StatefulSet... ReplicaSet... ReplicationController...]. Replica sayısı bu yüzden patch'in içinde. Commit'ten sonra Argo CD üç ortamda Rollout'u yarattı, Deployment'ı PruneLast ile en son sildi. Bir Rollout ilk kez yaratıldığında adımları uygulamıyor; prod 1.2.0 ile doğrudan %100'e çıktı.
Trafik Nasıl Bölünüyor?
Plan %10, %50 ve %100 idi; biz %25 ile başladık. Sebebi trafik yönetimi: Rollout'ta trafficRouting tanımlı değilse ağırlık pod sayısıyla sağlanıyor. Dört replica'da %25 bir canary pod'u ve üç stable pod demek; Service iki ReplicaSet'in pod'larına aynı şekilde dağıttığı için trafiğin kabaca dörtte biri canary'e gidiyor. %10 için on replica gerekirdi. Kesin yüzde için Rollouts'un bir trafik yönlendiricisiyle konuşması gerekiyor: Gateway API (Kubernetes'in yeni nesil ingress API'si) için Argo projesinin plugin'i (rollouts-plugin-trafficrouter-gatewayapi, şu an 0.17.0), Istio, NGINX (yaygın bir web sunucusu ve reverse proxy) Ingress, Amazon'un Application Load Balancer'ı ve diğerleri. O zaman stable ReplicaSet %100 kapasitede kalır ve yalnız router'daki ağırlık değişir. Lab'da bir ingress controller kurmadık; replica oranı Bookstore'un dört pod'u için yeterli.
Rollouts'un ikinci stratejisi blueGreen: yeni sürüm tam kapasiteyle ayağa kalkar, bir preview Service'i üzerinden denenir ve onaydan sonra active Service tek hamlede ona çevrilir. prePromotionAnalysis ile geçişten önce analiz çalıştırılabiliyor. Bu yazıda kurmadık; teorisi Deployment Stratejileri serisinin blue-green yazısında.
Analiz İçin Metrik: Uygulama, Prometheus ve AnalysisTemplate
Canary'nin iyi olup olmadığına karar vermek için yalnız canary pod'larının metriği gerekiyor. Önce API'ye HTTP metriklerini açtık; Observability serisindeki gibi OpenTelemetry ile:
// Prometheus için HTTP sunucu metrikleri (GET /metrics): GitOps serisindeki canary analizi bunları okur.
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddPrometheusExporter());
app.MapPrometheusScrapingEndpoint();
Lab'da çalışan Prometheus (lab/prometheus/prometheus.yaml) bookstore-* namespace'lerindeki api pod'larını Kubernetes service discovery ile buluyor. Önemli satır, Argo Rollouts'un her pod'a koyduğu ReplicaSet hash'ini bir metric label'ına çevirmek:
# Argo Rollouts her pod'a ReplicaSet'inin hash'ini label olarak koyar: stable ve canary ayrılır.
- source_labels: [__meta_kubernetes_pod_label_rollouts_pod_template_hash]
target_label: rollouts_pod_template_hash
Rollout, podTemplateHashValue: Latest ile yeni ReplicaSet'in hash'ini analize argüman olarak veriyor. AnalysisTemplate iki metrik tanımlıyor: /api isteklerinde 5xx oranı ve p95 gecikme. PromQL'in ayrıntısı Observability serisinin Prometheus ve PromQL yazısında:
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: api-canary-health
spec:
args:
- name: namespace
- name: canary-hash
metrics:
- name: error-rate
# Sorgu 1 dakikalık pencere kullanıyor: canary bir dakika trafik alana kadar bekle.
initialDelay: 60s
interval: 30s
failureLimit: 1
successCondition: len(result) > 0 && result[0] < 0.05
provider:
prometheus:
address: http://prometheus.monitoring.svc.cluster.local:9090
query: |
sum(rate(http_server_request_duration_seconds_count{namespace="{{args.namespace}}", rollouts_pod_template_hash="{{args.canary-hash}}", http_route=~"/api/.*", http_response_status_code=~"5.."}[1m]) or vector(0))
/
sum(rate(http_server_request_duration_seconds_count{namespace="{{args.namespace}}", rollouts_pod_template_hash="{{args.canary-hash}}", http_route=~"/api/.*"}[1m]))
- name: p95-latency
initialDelay: 60s
interval: 30s
failureLimit: 1
successCondition: len(result) > 0 && result[0] < 0.5
provider:
prometheus:
address: http://prometheus.monitoring.svc.cluster.local:9090
query: |
histogram_quantile(0.95, sum by (le) (rate(http_server_request_duration_seconds_bucket{namespace="{{args.namespace}}", rollouts_pod_template_hash="{{args.canary-hash}}", http_route=~"/api/.*"}[1m])))
Eşikler: hata oranı %5'in, p95 yarım saniyenin altında. failureLimit: 1 bir başarısız ölçüme izin veriyor, ikincisinde analiz Failed oluyor. len(result) > 0 koşulu sorgu boş döndüğünde ölçümü başarılı saymamak için; trafik yoksa analiz geçmiyor. Analiz startingStep: 1 ile arka planda başlıyor ve rollout bitene kadar sürüyor (background analysis). Prod'a saniyede yaklaşık on istek atan bir loadgen Deployment'ı koyduk; gerçek hayatta bu trafik kullanıcılardan gelir.
İyi Bir Sürüm: 1.3.0
Metrikleri açan 1.3.0'ı altıncı yazıdaki akışla çıkardık: CI dev'e commit attı, staging ve prod'a promotion branch'leri merge edildi. Prod'daki Rollout ilk adımda (%25) şöyle görünüyordu:
kubectl argo rollouts get rollout api -n bookstore-prod --no-color
Status: ॥ Paused
Message: CanaryPauseStep
Strategy: Canary
Step: 1/4
SetWeight: 25
ActualWeight: 25
Images: localhost:52612/bookstore/api:1.2.0@sha256:<digest-1.2.0> (stable)
localhost:52612/bookstore/api:1.3.0@sha256:<digest-1.3.0> (canary)
NAME KIND STATUS AGE INFO
⟳ api Rollout ॥ Paused 4m32s
├──# revision:2
│ ├──⧉ api-6f9cb4c9ff ReplicaSet ✔ Healthy 93s canary
│ │ └──□ api-6f9cb4c9ff-cn64p Pod ✔ Running 93s ready:1/1
│ └──α api-6f9cb4c9ff-2 AnalysisRun ◌ Running 92s ✔ 4
└──# revision:1
└──⧉ api-79f5d5fb59 ReplicaSet ✔ Healthy 4m32s stable
├──□ api-79f5d5fb59-hgxhl Pod ✔ Running 4m32s ready:1/1
├──□ api-79f5d5fb59-l8mh9 Pod ✔ Running 4m32s ready:1/1
└──□ api-79f5d5fb59-xsqjd Pod ✔ Running 4m32s ready:1/1
Argo CD bu sırada uygulamayı Suspended gösteriyor; Rollout'un pause'u Argo CD'nin health modelinde "beklemede" demek, hata değil. Rollout 4 dakika 6 saniyede %100'e ulaştı; AnalysisRun 14 ölçümün hepsini başarılı geçti (hata oranı her seferinde 0, p95 yaklaşık 5 milisaniye).
Kötü Bir Sürüm: 1.4.0 ve Otomatik Geri Dönüş
Hatalı sürümü üretmek için API'ye yalnız lab'da kullanılan bir ayar ekledik: Bookstore:FaultRate sıfırdan büyükse /api isteklerinin o kadarı 500 dönüyor. CI script'i bunu bir build arg olarak image'ın içine koyuyor, böylece 1.4.0 kendi başına "bozuk bir sürüm":
FAULT_RATE=0.3 CONFIG_REPO=../bookstore-config ci/build-and-push.sh api 1.4.0
1.4.0 dev ve staging'den geçti; iki ortamda da Rollout adımsız olduğu için hemen çıktı ve PostSync smoke test'i /health'e baktığı için yeşil kaldı. Prod'a giden branch merge edildikten sonraki zaman çizelgesi (Rollout'u 15 saniyede bir okuyan bir script'in log'undan, saatler UTC (eşgüdümlü evrensel saat)):
04:57:21 Status: ◌ Progressing step 0/4 weight 0
04:57:37 Status: ॥ Paused step 1/4 weight 25 AnalysisRun api-85645b89d-3 ◌ Running
04:58:23 Status: ॥ Paused step 1/4 weight 25 AnalysisRun api-85645b89d-3 ◌ Running ✔ 1,✖ 1
04:58:53 Status: ✖ Degraded step 0/4 weight 0 AnalysisRun api-85645b89d-3 ✖ Failed ✔ 2,✖ 2
api-85645b89d-3 Failed Metric "error-rate" assessed Failed due to failed (2) > failureLimit (1)
p95-latency: Successful values=[0.004788722738625373] [0.004792792792792794]
error-rate: Failed values=[0.3097411866014056] [0.2857142857142857]
Önce Tahmin Et'in cevabı: canary başladıktan 92 saniye sonra analiz ikinci kez %5'in üstünde hata oranı ölçtü (%31 ve %29), Rollout'u abort etti, canary ReplicaSet'ini sıfıra indirdi ve stable'ı tekrar dört pod'a çıkardı. Bir buçuk dakika boyunca trafiğin dörtte biri canary'e gitti ve onun yaklaşık %30'u hata aldı; p95 gecikme iyiydi, yani tek başına gecikmeye bakan bir analiz bu sürümü geçirirdi.
Status: ✖ Degraded
Message: RolloutAborted: Rollout aborted update to revision 3: Background analysis phase error/failed: Metric "error-rate" assessed Failed due to failed (2) > failureLimit (1)
Images: localhost:52612/bookstore/api:1.3.0@sha256:<digest-1.3.0> (stable)
Abort Sonrası: Git Hâlâ 1.4.0 Diyor
Hikâye burada bitmiyor. Cluster 1.3.0 çalıştırıyor, ama Git prod için 1.4.0 diyor. Argo CD'nin bakışı:
Sync Status: Synced to main (b1d1de8)
Health Status: Degraded
argoproj.io Rollout bookstore-prod api Synced Degraded RolloutAborted: Rollout aborted update to revision 3: ...
Rollout'un spec'i Git'le aynı olduğu için Argo CD Synced diyor; sapma spec'te değil, Rollout'un status'unda. Self-heal bunu düzeltemez, düzeltmemeli de: tekrar 1.4.0'ı denemek aynı sonuca gider. GitOps'ta doğru adım Git'i gerçeğe uydurmak, yani prod promotion'ının merge commit'ini geri almak:
git revert -m 1 --no-edit e158a12
35f7a8d Revert "Merge promote/prod-api-1.4.0: bookstore-api 1.4.0 to prod"
Revert'ten sonra Argo CD uygulamayı OutOfSync gösterdi ama sync etmedi. Sebebi 1.4.0'ın sync operasyonu: Argo CD wave 1'deki Rollout'un Healthy olmasını bekliyordu, Rollout Degraded olunca task başarısız sayıldı ve automated sync aynı revizyon için retry'a geçti (Retrying attempt #5). Dördüncü yazıdaki migration hatasında gördüğümüz durum: eski operasyon bitmeden yenisi başlamıyor. argocd app terminate-op bookstore-api-prod ile eski operasyonu durdurunca revert 15 saniyede uygulandı ve Rollout Healthy oldu. Template stable ReplicaSet'inkiyle aynı olduğu için Rollouts adımları atlıyor; stable'a dönüş canary değil.
Abort'u Git'e yansıtmanın otomatik bir yolu yok; bunu bir insan ya da bir otomasyon yapmalı. Argo CD Notifications ile Rollout Degraded olduğunda ekibe haber vermek (son yazı) ve revert'i bir pull request olarak açan küçük bir job, ekiplerin sık kullandığı ikili. Argo CD UI'ı Rollout için resume, abort, retry ve promote-full gibi resource action'ları da gösteriyor; acil durumda bunlar Git'e dokunmadan Rollout'u yönetir, ama kalıcı karar yine Git'te verilmeli.
Flagger: Aynı İş, Flux Tarafından
Flux ailesinin progressive delivery aracı Flagger aynı problemi farklı bir modelle çözüyor: Deployment'ınızı değiştirmiyorsunuz, Flagger onun yanına bir Canary nesnesi ve kopyalar yaratıyor, trafiği service mesh ya da ingress üzerinden kaydırıyor ve Prometheus sorgularıyla karar veriyor. Deployment'ı Rollout'a çevirmek istemeyen, zaten Flux ve bir service mesh kullanan ekipler için doğal seçim. Argo CD ile birlikte de kullanılabiliyor, ama Argo CD'nin hazır Rollout health check'i ve UI action'ları Rollouts'u bu seride daha uyumlu kılıyor.
Üretimde Dikkat
Düşük trafikte analiz yanıltır: saniyede bir istek alan bir canary'de bir dakikalık pencerede tek bir 500 hata oranını %2'ye çıkarır, hiç istek yoksa sorgu boş döner. Eşiği trafik hacmine göre seç, minimum istek sayısını sorguya koy ve gece saatlerinde canary'yi uzat ya da durdur. HPA (Horizontal Pod Autoscaler; yüke göre pod sayısını değiştiren controller) Rollout'la çalışıyor: scaleTargetRef Rollout'u gösterir, Rollouts toplam replica sayısını adımlara göre stable ve canary arasında böler; HPA'yı Deployment'ta bırakıp Rollout'a geçmek en sık yapılan hata. Migration'lar canary ile uyuşmaz: dördüncü yazıdaki migration Job'ı Sync hook'u olarak canary başlamadan çalışıyor, yani yeni şema hem stable hem canary pod'larının önünde. Deployment Stratejileri serisinin migration yazısındaki expand-contract kuralı burada zorunlu; canary bozuk bir migration'ı geri almaz. Analizin bağlı olduğu Prometheus da bir bağımlılık: Prometheus erişilemezse ölçümler Error olur ve art arda consecutiveErrorLimit (varsayılan 4) aşılınca metrik Error durumuna düşer, rollout abort edilir; izleme sistemindeki bir arıza release'leri durdurabilir.
Sık Düşülen Tuzaklar
Aynı overlay'e iki yazar. Altıncı yazıdan kalan Image Updater dev'de 1.2.x izliyordu. CI dev'e 1.3.0 ve 1.4.0 yazdıktan iki ila üç dakika sonra Image Updater ikisinde de dev'i 1.2.1'e geri çekti: çalışan image kendi kuralına uymuyordu ve kuralın en yenisi 1.2.1'di.
b1d1de8 argocd-image-updater bookstore-api-dev: update images
bookstore/api: sha256:<digest-1.4.0> -> 1.2.1
bookstore/migrator: sha256:<digest-migrator-1.4.0> -> 1.2.1
Image Updater'ın custom resource'unu Git'ten kaldırdık. Bir overlay'e ya CI yazar ya Image Updater; ikisi birden yazarsa sonuncu kazanır.
git revert HEAD ile acele etmek. İlk denememizde merge commit'i yerine HEAD'i geri aldık; HEAD o an Image Updater'ın dev commit'iydi. Paylaşılan bir config repo'sunda revert her zaman commit'in SHA'sı (Git'in commit'e verdiği hash) ile yapılır.
Abort'tan sonra Git'i düzeltmeyi unutmak. Cluster stable sürümde ama Argo CD Degraded, sync operasyonu yeniden deniyor ve bir sonraki commit uygulanmıyor.
İlk Rollout'ta canary beklemek. Rollout ilk yaratıldığında adımları uygulamıyor; Deployment'tan geçişte ilk sürüm doğrudan %100 çıkıyor.
Yalnız gecikmeye ya da yalnız health check'e bakan analiz. 1.4.0'ın p95'i iyiydi ve /health'i sağlıklıydı.
Küçük Bir Deneme
★☆☆ kubectl argo rollouts promote api -n bookstore-prod ile bir pause adımını erken geç ve AnalysisRun'ın buna rağmen arka planda sürdüğünü doğrula. Sonra Argo CD UI'ında aynı Rollout'un resource action menüsünü bul.
★★☆ FAULT_RATE=0.03 ile 1.4.1 üret ve prod'a çıkar. Analiz geçiyor mu? failureLimit'i 0'a, eşiği %2'ye çekip tekrar dene ve düşük hata oranlarında yanlış alarm ile kaçan hata arasındaki dengeyi gözlemle.
★★★ Gateway API plugin'ini ve bir Gateway API uyumlu controller'ı kur, Rollout'a trafficRouting.plugins ekle ve adımları %10, %50, %100 yap. Stable ReplicaSet'in replica sayısını iki yaklaşımda karşılaştır.
Artık Argo CD Git'ten deploy ediyor, secret'lar Git'te güvende ve prod sürümleri metrikle korunuyor. Geriye bunu bir ekibin günlük işi hâline getirmek kalıyor: kimin neyi sync edebileceği, bildirimler, Argo CD'nin kendi metrikleri ve yükseltmeleri. Serinin son yazısında Argo CD'yi production'da işletmeye ve 2.x'ten 3.6'ya değişenlere bakıyoruz.