Mert Özen Her Satırda Daha İleri
DevOps Kubernetes — #04

Kubernetes Deployment ve ReplicaSet: Label'lar, Rolling Update, Rollout Geçmişi ve Geri Alma

Mert Özen 8 Kas 2026 10 dk 5 görüntülenme
Kubernetes Deployment ve ReplicaSet: Label'lar, Rolling Update, Rollout Geçmişi ve Geri Alma

Deployment pod'larını label selector ile tanır, her pod template'i için bir ReplicaSet yaratır ve rolling update'i maxSurge, maxUnavailable ve minReadySeconds ile yürütür. Dört replica'lık rollout'u saniye saniye izliyor, var olmayan bir tag'in progressDeadlineSeconds'a takılmasını, rollout undo'nun Git'le yarattığı farkı, pause ile tek rollout'u, change-cause tuzağını ve immutable image tag'lerini gerçek çıktılarla görüyoruz.

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

Önce Tahmin Et

Bookstore API'sinin (Application Programming Interface) dört replica'sı 1.1.0 sürümüyle çalışıyor. Deployment'ta maxSurge: 1, maxUnavailable: 0 ve progressDeadlineSeconds: 120 var. CI (Continuous Integration) pipeline'ı yanlışlıkla registry'de olmayan 1.2.0 tag'ini deploy ediyor. Rollout boyunca kaç pod trafik karşılar: dört mü, üç mü, sıfır mı? İki dakika sonra kubectl rollout status ne der, Kubernetes kendiliğinden eski sürüme döner mi?

Label ve Selector: Deployment'ı Pod'lara Bağlayan İp

Önceki yazıda tek başına pod yaratmamamız gerektiğini gördük. Bookstore API'sinin production'a yakın Deployment'ı şöyle:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: bookstore
  labels:
    app.kubernetes.io/name: api
    app.kubernetes.io/part-of: bookstore
  annotations:
    kubernetes.io/change-cause: "release 1.0.0"
spec:
  replicas: 4
  revisionHistoryLimit: 5
  minReadySeconds: 5
  progressDeadlineSeconds: 120
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app.kubernetes.io/name: api
  template:
    metadata:
      labels:
        app.kubernetes.io/name: api
        app.kubernetes.io/part-of: bookstore
    spec:
      containers:
        - name: api
          image: bookstore-api:1.0.0
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /health/ready
              port: http
            periodSeconds: 2

Deployment pod'larını isimden değil label'dan tanır. spec.selector "şu label'ları taşıyan pod'lar benim" der, spec.template.metadata.labels ise yaratılan her pod'a bu label'ları basar. İkisinin uyuşması zorunlu; uyuşmazsa API server Deployment'ı kabul etmez. Label adları için app.kubernetes.io/name ve app.kubernetes.io/part-of gibi Kubernetes'in önerdiği ortak label'ları kullanıyoruz; araçlar (dashboard'lar, Helm, Argo CD (Continuous Delivery)) bunları tanır.

Readiness probe'a şimdilik takılma; ayrıntısı sekizinci yazıda. Burada tek bir işi var: Deployment'a "bu pod trafik almaya hazır" sinyalini vermek. Rolling update bu sinyal olmadan kördür.

$ kubectl apply -f api-deployment.yaml
deployment.apps/api created
$ kubectl get pods -l app.kubernetes.io/name=api --show-labels
NAME                   READY   STATUS    RESTARTS   AGE   LABELS
api-6ddb5d7947-5zx5r   1/1     Running   0          8s    app.kubernetes.io/name=api,app.kubernetes.io/part-of=bookstore,pod-template-hash=6ddb5d7947
api-6ddb5d7947-bf4pj   1/1     Running   0          8s    app.kubernetes.io/name=api,app.kubernetes.io/part-of=bookstore,pod-template-hash=6ddb5d7947
api-6ddb5d7947-whqpn   1/1     Running   0          8s    app.kubernetes.io/name=api,app.kubernetes.io/part-of=bookstore,pod-template-hash=6ddb5d7947
api-6ddb5d7947-xk2jw   1/1     Running   0          8s    app.kubernetes.io/name=api,app.kubernetes.io/part-of=bookstore,pod-template-hash=6ddb5d7947

Bizim yazmadığımız bir label var: pod-template-hash=6ddb5d7947. Bunu Deployment controller'ı ekliyor ve pod adının ortasındaki parça da o. Bir sonraki bölümün anahtarı bu hash.

Selector'ın bir özelliği daha var: apps/v1'de değiştirilemez. Selector'a ikinci bir label eklemeyi denedim:

$ kubectl apply -f api-selector.yaml
The Deployment "api" is invalid: spec.selector: Invalid value: {"matchLabels":{"app.kubernetes.io/name":"api","app.kubernetes.io/part-of":"bookstore"}}: field is immutable

Selector'ı değiştirmenin tek yolu Deployment'ı silip yeniden yaratmak. Bu yüzden selector'a yalnız uygulamanın kimliğini (ad, gerekirse bileşen) koy; sürüm, environment ya da takım adı gibi değişebilecek label'ları template'e ekle ama selector'a koyma.

ReplicaSet'in Rolü

Deployment pod'ları doğrudan yaratmaz. Her farklı pod template'i için bir ReplicaSet yaratır; pod'ları ReplicaSet yaratır ve sayısını korur. Template'in hash'i hem ReplicaSet'in adına hem de pod'ların pod-template-hash label'ına yazılır; böylece aynı Deployment'ın farklı sürümlerinin pod'ları birbirine karışmaz.

$ kubectl get rs -l app.kubernetes.io/name=api
NAME             DESIRED   CURRENT   READY   AGE
api-6ddb5d7947   4         4         4       8s

Image'ı, bir environment variable'ı, bir probe'u ya da resource'ları, kısacası spec.template altındaki herhangi bir şeyi değiştirdiğinde yeni bir hash ve yeni bir ReplicaSet doğar. replicas'ı değiştirmek ise template'e dokunmaz; mevcut ReplicaSet'in sayısı değişir, rollout başlamaz. Eski ReplicaSet'ler 0 replica'yla saklanır; revisionHistoryLimit (varsayılan 10, bizde 5) kaç tanesinin tutulacağını belirler. Geri alma bu saklanan ReplicaSet'lerle yapılır. ReplicaSet'i elle yönetmen gerekmez; Deployment'ın altındaki bir ReplicaSet'i scale edersen Deployment controller'ı onu hemen geri düzeltir.

Rolling Update Nasıl İlerler?

Image'ı 1.1.0 yapıp change-cause'u güncelledim ve ReplicaSet sayılarını her saniye kaydettim. Sütunlar: ReplicaSet, istenen, ready, available.

$ kubectl apply -f api-deployment-1.1.0.yaml
deployment.apps/api configured
03:46:15 api-6ddb5d7947   4     4        4|api-b995dcccb    1     <none>   <none>|
03:46:17 api-6ddb5d7947   4     4     4|api-b995dcccb    1     1     <none>|
03:46:21 api-6ddb5d7947   3     3     3|api-b995dcccb    2     1     1|
03:46:23 api-6ddb5d7947   3     3     3|api-b995dcccb    2     2     1|
03:46:27 api-6ddb5d7947   2     2     2|api-b995dcccb    3     2     2|
03:46:29 api-6ddb5d7947   2     2     2|api-b995dcccb    3     3     2|
03:46:31 api-6ddb5d7947   2     2     2|api-b995dcccb    3     3     3|
03:46:34 api-6ddb5d7947   1     1     1|api-b995dcccb    4     4     3|
03:46:38 api-6ddb5d7947   0     <none>   <none>|api-b995dcccb    4     4        4|

Parametreleri bu tablodan okuyalım. maxSurge: 1, istenen sayının (4) en fazla 1 fazlasına, yani toplam 5 pod'a izin veriyor; ilk satırda eski 4 pod dururken yeni ReplicaSet'te 1 pod açıldı. maxUnavailable: 0, available pod sayısının hiçbir an 4'ün altına düşmemesi demek; bu yüzden eski bir pod ancak yeni bir pod available olduktan sonra kapatılıyor. minReadySeconds: 5 ise "ready olduktan sonra 5 saniye çökmeden kalırsa available say" diyor: 03:46:17'de ready olan pod, 03:46:21'de available sayıldı ve eski ReplicaSet 3'e indi. Dört pod'luk rollout 23 saniye sürdü ve hiçbir an kapasite 4 pod'un altına inmedi.

Varsayılan değerler maxSurge: 25% ve maxUnavailable: 25%; yüzde, surge için yukarı, unavailable için aşağı yuvarlanır. 4 replica'da ikisi de 1 eder ve varsayılanla rollout sırasında bir pod eksik çalışabilirsin. Kapasitesi tam hesaplanmış bir API için maxUnavailable: 0 daha güvenli; node'larda ek pod'a yer yoksa da maxSurge: 0, maxUnavailable: 1 ile önce kapatıp sonra açarsın. İkisi birden 0 olamaz.

İkinci strateji Recreate: önce bütün eski pod'ları kapatır, sonra yenilerini açar. Kesinti yaratır ama iki sürümün aynı anda çalışmasının mümkün olmadığı durumlarda (örneğin eski ve yeni sürümün paylaşamadığı bir şema ya da tek yazıcılı bir dosya) doğru seçimdir. Hangi stratejinin ne zaman seçileceği, blue-green ve canary ile karşılaştırması ilerideki Deployment Stratejileri serisinin Rolling Deployment yazısının konusu; burada yalnız mekaniği görüyoruz.

Takılan Rollout ve progressDeadlineSeconds

Şimdi baştaki soru. Var olmayan 1.2.0 tag'ini uyguladım:

$ kubectl apply -f api-deployment-1.2.0.yaml
deployment.apps/api configured
$ kubectl rollout status deploy/api --timeout=180s
Waiting for deployment "api" rollout to finish: 0 out of 4 new replicas have been updated...
Waiting for deployment "api" rollout to finish: 1 out of 4 new replicas have been updated...
error: deployment "api" exceeded its progress deadline
$ kubectl get pods -l app.kubernetes.io/name=api
NAME                  READY   STATUS         RESTARTS   AGE
api-8596656d5-kvgvv   0/1     ErrImagePull   0          2m1s
api-b995dcccb-7p526   1/1     Running        0          2m21s
api-b995dcccb-gc6zd   1/1     Running        0          2m33s
api-b995dcccb-r6m9g   1/1     Running        0          2m27s
api-b995dcccb-x9nlk   1/1     Running        0          2m16s
$ kubectl get deploy api -o jsonpath='{range .status.conditions[*]}{.type}={.status} {.reason}: {.message}{"\n"}{end}'
Available=True MinimumReplicasAvailable: Deployment has minimum availability.
Progressing=False ProgressDeadlineExceeded: ReplicaSet "api-8596656d5" has timed out progressing.

Dört eski pod rollout boyunca trafik almaya devam etti; maxSurge: 1 yalnız bir yeni pod açılmasına izin verdi, o da image'ı çekemedi, maxUnavailable: 0 yüzünden hiçbir eski pod kapatılmadı. Kullanıcı açısından kesinti yok. Bu, maxUnavailable: 0'ın asıl değeri: kötü bir sürüm kapasiteyi yemez.

120 saniye sonra Deployment'ın Progressing koşulu False oldu ve reason ProgressDeadlineExceeded. kubectl rollout status exit code 1 ile çıktı; pipeline'ın deploy adımını kırmızıya çevirecek sinyal bu. Ama dikkat: Kubernetes kendiliğinden geri dönmez. Deadline yalnız bir durum bildirir; bozuk ReplicaSet orada ErrImagePull'da bekler. Geri dönmek senin ya da pipeline'ının işi. Otomatik geri alma ve metriklerle karar veren rollout'lar Argo Rollouts gibi araçların ve ilerideki Deployment Stratejileri serisinin konusu.

Rollout Geçmişi, Geri Alma ve Pause

$ kubectl rollout history deploy/api
deployment.apps/api
REVISION  CHANGE-CAUSE
1         release 1.0.0
2         release 1.1.0
3         release 1.2.0
$ kubectl rollout history deploy/api --revision=2
deployment.apps/api with revision #2
Pod Template:
  Labels:	app.kubernetes.io/name=api
	app.kubernetes.io/part-of=bookstore
	pod-template-hash=b995dcccb
  Annotations:	kubernetes.io/change-cause: release 1.1.0
  Containers:
   api:
    Image:	bookstore-api:1.1.0

CHANGE-CAUSE sütunu Deployment'taki kubernetes.io/change-cause annotation'ından gelir. Eskiden kubectl apply --record bu annotation'ı otomatik yazardı; o flag deprecated, bu yüzden annotation'ı manifest'te ya da pipeline'da kendin yazıyorsun. Şimdi geri alalım:

$ kubectl rollout undo deploy/api
Warning: resource deployments/api was previously managed with 'kubectl apply'. Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation, which may cause unexpected behavior on future 'kubectl apply' operations. Consider using 'kubectl apply' with your previous configuration file instead.
deployment.apps/api rolled back
$ kubectl rollout history deploy/api
REVISION  CHANGE-CAUSE
1         release 1.0.0
3         release 1.2.0
4         release 1.1.0
$ kubectl get rs -l app.kubernetes.io/name=api
NAME             DESIRED   CURRENT   READY   AGE
api-6ddb5d7947   0         0         0       2m55s
api-8596656d5    0         0         0       2m7s
api-b995dcccb    4         4         4       2m39s

Üç şey dikkat çekiyor. Birincisi, geri alma yeni bir revision yaratır: revision 2 kayboldu, aynı template revision 4 olarak geri geldi. İkincisi, geri alma hızlı: b995dcccb ReplicaSet'i zaten vardı, yalnız 4'e scale edildi. Üçüncüsü ve en önemlisi kubectl 1.37'nin uyarısı: rollout undo cluster'ı değiştirir ama Git'teki dosyayı değiştirmez. Dosya hâlâ 1.2.0 diyorsa bir sonraki kubectl apply bozuk sürümü geri getirir. Production'da asıl geri alma yolu Git'teki manifest'i düzeltip yeniden uygulamak; rollout undo acil durumda zaman kazandıran bir araç.

Birden fazla değişikliği tek bir rollout'ta toplamak için pause kullanılır:

$ kubectl rollout pause deploy/api
deployment.apps/api paused
$ kubectl set env deploy/api Bookstore__StoreName="Bookstore Istanbul"
deployment.apps/api env updated
$ kubectl set resources deploy/api -c api --requests=cpu=100m,memory=128Mi
deployment.apps/api resource requirements updated
$ kubectl rollout resume deploy/api
deployment.apps/api resumed
$ kubectl rollout history deploy/api
REVISION  CHANGE-CAUSE
1         release 1.0.0
3         release 1.2.0
4         release 1.1.0
5         release 1.1.0

İki değişiklik tek bir yeni ReplicaSet ve tek bir revision (5) üretti. Ama revision 5'in change-cause'u yanlış: annotation'ı güncellemediğim için Deployment'taki eski değer kopyalandı. Geçmişe güvenmek istiyorsan change-cause'u her değişiklikte yazan bir pipeline şart.

Image Tag'leri ve imagePullPolicy

Rolling update ve geri alma, her revision'ın tam olarak hangi image'ı çalıştırdığını bilmeye dayanır. bookstore-api:latest gibi değişen bir tag bu varsayımı bozar: revision 3 ile revision 4 aynı tag'i gösteriyorsa "geri al" komutu hiçbir şeyi geri almaz, üstelik farklı node'lar tag'in farklı anlardaki hâllerini çekmiş olabilir. İkinci yazıda gördüğümüz gibi :latest varsayılan olarak imagePullPolicy: Always da getirir.

Production için iki kural yeterli. Her build'e değişmeyen bir tag ver: semantic version (1.1.0), commit SHA (Secure Hash Algorithm) ya da ikisi (1.1.0-3f2c9ab). Registry'de tag'lerin üzerine yazmayı kapat (çoğu registry'de "immutable tags" ayarı var). Daha sıkı bir garanti için image'ı digest ile referans ver: registry.example.com/bookstore-api@sha256:.... Digest içeriğin hash'idir, değiştirilemez; Kustomize ile CI'dan tag ya da digest set etmeyi 14. yazıda göreceğiz.

Üretimde Dikkat

Rolling update'in güvenli olması üç şeye bağlı: doğru bir readiness probe (pod gerçekten hazır olmadan trafik almasın), düzgün bir graceful shutdown (eski pod kapanırken istekler yarıda kalmasın; onuncu yazı) ve geriye uyumlu değişiklikler (rollout boyunca eski ve yeni sürüm aynı veritabanına ve aynı client'lara aynı anda hizmet verir). Üçüncüsü özellikle şema değişikliklerinde kritik; expand-contract yaklaşımı ilerideki Deployment Stratejileri serisinde.

progressDeadlineSeconds'ı uygulamanın gerçekçi başlama süresine göre ayarla ve pipeline'da kubectl rollout status --timeout ile bekle. Varsayılan 600 saniye; hızlı açılan bir API için on dakika boyunca kırık bir deploy'u fark etmemek demek.

Kapasiteyi rollout'u hesaba katarak planla. maxSurge kadar fazladan pod için node'larda CPU (Central Processing Unit) ve bellek yeri olmalı; yoksa yeni pod Pending'de kalır ve rollout deadline'a takılır.

Sık Düşülen Tuzaklar

Selector'a değişebilecek label koymak. Sürüm label'ı selector'daysa bir sonraki sürümde Deployment'ı silmek zorunda kalırsın. Selector'ı sade tut.

Aynı selector'ı paylaşan iki Deployment. İki Deployment aynı label'larla pod seçerse birbirlerinin pod'larını sahiplenmeye çalışırlar. Her Deployment'ın label kümesi benzersiz olmalı.

rollout undo'dan sonra dosyayı düzeltmemek. Yukarıdaki uyarıyı ciddiye al; aksi hâlde bir sonraki apply bozuk sürümü geri getirir.

Readiness probe'suz rolling update. Probe yoksa container başladığı an ready sayılır; uygulama daha port'u açmadan eski pod kapatılır ve kısa süreli 502'ler görürsün.

Change-cause'u elle tutmaya çalışmak. Unutulan bir annotation, revision 5'teki gibi yanlış bir geçmiş üretir. Ya pipeline'a yazdır ya da geçmiş için Git'i kaynak kabul et.

Küçük Bir Deneme

★☆☆ Deployment'ı maxSurge: 25%, maxUnavailable: 25% varsayılanlarıyla uygula ve aynı 1.0.0 → 1.1.0 rollout'unu kaydet. Available pod sayısının 3'e düştüğü anları bul.

★★☆ strategy.type: Recreate ile aynı rollout'u yap ve bir terminalde while true; do curl -s -o /dev/null -w "%{http_code}\n" localhost:18710/; sleep 0.2; done çalıştırırken (port-forward Service'e yapılmalı; beşinci yazıya bak) kaç isteğin başarısız olduğunu say.

★★★ Var olmayan bir tag'le deploy eden, kubectl rollout status --timeout=60s başarısız olursa kubectl rollout undo çalıştıran ve exit code 1 dönen küçük bir bash script yaz. Sonra bu yaklaşımın Git ile cluster arasında yarattığı farkı kubectl diff ile göster.

Pod'larımız artık kontrollü bir şekilde güncelleniyor, ama her yeni pod yeni bir IP (Internet Protocol) adresiyle geliyor. Bir sonraki yazıda bu değişen adreslerin önüne sabit bir ad koyan Service'e bakıyoruz: ClusterIP, NodePort, LoadBalancer, cluster DNS'i (Domain Name System) ve EndpointSlice'lar.