Kustomize ile Environment'lar: Base, Overlay, Image Tag ve configMapGenerator
Kustomize ile ortak manifest'ler base'de durur, her ortam overlay'de yalnız farkını yazar ve configMapGenerator'ın hash suffix'i config değişince otomatik rollout tetikler. Component ile HPA/PDB paylaşımını, JSON Patch ile strategic merge patch farkını, CI'da kustomize edit set image'ı ve commonLabels'ın selector'ı değiştirip yarım kalan bir apply ile servisi nasıl kestiğini 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 (içinde Kustomize 5.8.1), tek başına Kustomize 5.8.3 ve .NET 10 ile denenmiştir.
Önce Tahmin Et
Prod overlay'ine bir satır ekliyorsun: bütün resource'lara app.kubernetes.io/version: "1.2.0" label'ı koyan commonLabels. kubectl apply -k bir hata veriyor: Deployment'ın selector'ı immutable. Soru şu: hata aldığına göre hiçbir şey değişmedi, değil mi? Servis trafiği almaya devam ediyor mu? Cevap bu yazının en pahalı dersi ve aşağıda gerçek çıktısıyla duruyor.
Sorun: Üç Ortam, Üç Kopya
Önceki yazılarda Bookstore manifest'lerini tek bir ortam için yazdık. Gerçekte en az üç ortam var: dev'de tek replica ve indirimli test fiyatları, staging'de prod'a yakın ayarlar, prod'da HPA (Horizontal Pod Autoscaler), PDB (PodDisruptionBudget) ve daha büyük resource'lar. İlk refleks klasörü üç kez kopyalamaktır. Altı ay sonra dev'deki readiness probe düzeltilmiş, prod'daki hâlâ eski; staging'e eklenen bir env değişkeni prod'da unutulmuş. Kopyalar zamanla ayrışır ve ayrışma ancak production'da fark edilir.
Kustomize bunu template'siz çözer: ortak manifest'ler bir base'de durur, her ortam bir overlay'de yalnız farkını yazar, sonuç düz YAML (YAML Ain't Markup Language) olarak üretilir. Template dili, değişken yok; her dosya kendi başına geçerli bir Kubernetes manifest'i. Kustomize kubectl'in içinde gelir (kubectl apply -k, kubectl kustomize); tek başına binary'si daha yeni sürümü ve edit komutlarını getirir. Bizim kubectl 1.37.2 içinde Kustomize 5.8.1 vardı, tek başına binary 5.8.3'tü; CI (Continuous Integration) ile lokal makinenin aynı sürümü kullanması küçük farkların sürpriz olmasını önler.
Base: Ortamdan Bağımsız Çekirdek
Klasör yapısı:
deploy/
base/
kustomization.yaml
deployment.yaml
service.yaml
appsettings.json
components/
autoscaling/
kustomization.yaml
hpa.yaml
pdb.yaml
overlays/
dev/
kustomization.yaml
appsettings.json
prod/
kustomization.yaml
appsettings.json
resources.yaml
Base'deki deployment.yaml ve service.yaml önceki yazılardakiyle aynı, namespace yazılmadan. Asıl iş kustomization.yaml'da:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
labels:
- pairs:
app.kubernetes.io/part-of: bookstore
includeSelectors: false
includeTemplates: true
configMapGenerator:
- name: api-config
files:
- appsettings.json
images:
- name: bookstore-api
newTag: 1.0.0
labels transformer'ı bütün resource'lara ve (includeTemplates ile) pod template'lerine label ekler, ama includeSelectors: false ile selector'lara dokunmaz. Bu ayrıntı Önce Tahmin Et'in cevabının yarısı. configMapGenerator, appsettings.json dosyasından bir ConfigMap üretir; serinin yedinci yazısındaki gibi API (Application Programming Interface) bu dosyayı /app/config'ten reloadOnChange ile okuyor. images ise manifest'teki bookstore-api image'ının tag'ini tek yerden belirler.
Overlay'ler: Yalnız Fark
Dev overlay'i namespace'i, config'i ve environment adını değiştirir:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: bookstore-dev
resources:
- ../../base
configMapGenerator:
- name: api-config
behavior: merge
files:
- appsettings.json
patches:
- target:
kind: Deployment
name: api
patch: |-
- op: replace
path: /spec/template/spec/containers/0/env/0/value
value: Development
Buradaki patch bir JSON (JavaScript Object Notation) Patch'tir (RFC 6902, RFC: Request for Comments); küçük, nokta atışı değişiklikler için uygun ama dizideki sıraya (containers/0/env/0) bağlı olduğu için kırılgan. Prod overlay'i ise strategic merge patch kullanıyor: Kubernetes nesnesinin kısmi bir kopyası, container'lar name'e göre eşleşiyor:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: bookstore-prod
resources:
- ../../base
components:
- ../../components/autoscaling
configMapGenerator:
- name: api-config
behavior: merge
files:
- appsettings.json
patches:
- path: resources.yaml
images:
- name: bookstore-api
newTag: 1.1.0
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
memory: 512Mi
components, birden fazla overlay'in paylaştığı isteğe bağlı parçalar için. HPA ve PDB'yi bir component'e koyduk; staging ve prod onu kullanır, dev kullanmaz. Component, HPA varken Deployment'taki replicas alanını da siliyor; on birinci yazıda gördüğümüz "her apply HPA'nın kararını ezer" tuzağının Kustomize'daki karşılığı:
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
- hpa.yaml
- pdb.yaml
patches:
- target:
kind: Deployment
name: api
patch: |-
- op: remove
path: /spec/replicas
Sonucu cluster'a göndermeden görmek için kubectl kustomize overlays/prod. Çıktının ilgili kısmı:
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/part-of: bookstore
name: api-config-tmh5dh7hbh
namespace: bookstore-prod
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/part-of: bookstore
name: api
namespace: bookstore-prod
spec:
selector:
matchLabels:
app.kubernetes.io/name: api
template:
...
image: bookstore-api:1.1.0
...
resources:
limits:
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
...
volumes:
- configMap:
name: api-config-tmh5dh7hbh
name: config
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
namespace: bookstore-prod
Üç şey dikkat çekiyor. Deployment'ta replicas yok. ConfigMap'in adına bir hash eklenmiş ve Deployment'taki referans da otomatik güncellenmiş. PDB ve HPA'da part-of label'ı yok: base'deki labels yalnız base'in resource'larına uygulanıyor, overlay'de component'ten gelenlere değil. Label'ın her yerde olmasını istiyorsan transformer'ı overlay'e taşı. İki ortamı uyguladık ve her birinin config'ini sorduk:
{"pod":"api-68775884bd-xgknm","version":"1.0.0","environment":"Development","storeName":"Bookstore (dev)","discountPercent":50,"database":"in-memory"}
{"pod":"api-7c766654d9-qgm5d","version":"1.1.0","environment":"Production","storeName":"Bookstore","discountPercent":10,"database":"in-memory"}
configMapGenerator ve Hash Suffix: Config Değişince Otomatik Rollout
Yedinci yazıda gördük: mount edilen bir ConfigMap değişince pod'lar yeniden başlamaz; config ya dosya üzerinden yavaşça yansır ya da hiç yansımaz (subPath, env). Hash suffix bunu kökünden çözer: içerik değişirse ConfigMap'in adı değişir, adı değişen referans pod template'ini değiştirir, pod template'i değişen Deployment rolling update yapar. Prod'da indirimi 10'dan 15'e çıkarıp önce kubectl diff -k ile baktık:
- name: api-config-tmh5dh7hbh
+ name: api-config-86ddt625g5
+apiVersion: v1
+data:
+ appsettings.json: |
+ {
+ "Bookstore": {
+ "StoreName": "Bookstore",
+ "DiscountPercent": 15
+ }
+ }
+kind: ConfigMap
configmap/api-config-86ddt625g5 created
service/api unchanged
deployment.apps/api configured
deployment "api" successfully rolled out
NAME DATA AGE
api-config-86ddt625g5 1 12s
api-config-tmh5dh7hbh 1 28s
{"pod":"api-57577764d5-hnlf7","version":"1.1.0","environment":"Production","storeName":"Bookstore","discountPercent":15,"database":"in-memory"}
Eski ConfigMap silinmedi. Bu hem iyi hem kötü: iyi, çünkü kubectl rollout undo eski ReplicaSet'in template'ine döner ve o template eski ConfigMap'i gösterir; config de kodla birlikte geri alınır. Kötü, çünkü her config değişikliği bir ConfigMap bırakır. Temizlik için GitOps aracının prune özelliği (ilerideki GitOps ve Argo CD (CD: Continuous Delivery) serisinde) ya da ara sıra elle temizlik gerekir; düz kubectl apply bunu yapmaz.
CI'dan Image Tag'i Set Etmek
Pipeline yeni image'ı build edip registry'ye push ettikten sonra overlay'deki tag'i günceller. Bunun için dosyayı sed ile değil kustomize edit ile değiştir:
cd deploy/overlays/prod
kustomize edit set image bookstore-api=bookstore-api:1.2.0
images:
- - name: bookstore-api
- newTag: 1.1.0
+- name: bookstore-api
+ newName: bookstore-api
+ newTag: 1.2.0
Bu diff'in yalnız bir kısmı. kustomize edit dosyanın tamamını kendi biçimine göre yeniden yazdı: bütün liste girintileri değişti, configMapGenerator'daki key'lerin sırası alfabetik oldu. Pipeline'ın açtığı her commit'te bu gürültüyü görmemek için kustomization.yaml dosyalarını baştan Kustomize'ın biçiminde tut ya da bir kez kustomize edit fix çalıştırıp öyle commit et. Production'da tag yerine digest kullanmak (kustomize edit set image bookstore-api=registry.example.com/bookstore-api@sha256:...) aynı tag'in yeniden push edilme riskini de kapatır. Bu adımın GitHub Actions tarafı GitHub Actions serisinin deploy yazılarında, image'ı cluster'a GitOps ile taşımak ise ilerideki GitOps ve Argo CD serisinde.
Tuzak: commonLabels ve Yarım Kalan Apply
Önce Tahmin Et'teki satır:
commonLabels:
app.kubernetes.io/version: "1.2.0"
# Warning: 'commonLabels' is deprecated. Please use 'labels' instead. Run 'kustomize edit fix' to update your Kustomization automatically.
configmap/api-config-86ddt625g5 configured
service/api configured
poddisruptionbudget.policy/api configured
horizontalpodautoscaler.autoscaling/api configured
The Deployment "api" is invalid: spec.selector: Invalid value: {"matchLabels":{"app.kubernetes.io/name":"api","app.kubernetes.io/version":"1.2.0"}}: field is immutable
commonLabels label'ı selector'lara da ekler. Deployment'ın selector'ı immutable olduğu için Deployment reddedildi; ama apply resource'ları tek tek gönderir ve Service ondan önce gitti. Service'in selector'ı immutable değil, yeni label'la güncellendi ve artık hiçbir pod onunla eşleşmiyor:
{"app.kubernetes.io/name":"api","app.kubernetes.io/version":"1.2.0"}
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
api-v6xgn IPv4 <unset> <unset> 4m36s
wget: can't connect to remote host (10.96.228.122): Connection refused
"Hata aldım, demek ki hiçbir şey değişmedi" varsayımı yanlış: komut hata verdi, servis kesintideydi. Bir sürüm numarasını selector'a koymak, her sürümde selector'ı değiştirmek demektir; selector'lar uygulamanın ömrü boyunca sabit kalacak label'larla kurulmalı. Kustomize 5'ten beri doğrusu labels ve includeSelectors: false. Overlay'i düzeltip tekrar uygulayınca endpoint'ler geri geldi.
Helm ile Kısa Karşılaştırma
Helm aynı problemi template'lerle çözer: values.yaml'daki değerler Go template'lerine doldurulur. Helm'in güçlü olduğu yer paketleme ve dağıtımdır: başkalarının kuracağı, onlarca ayarı olan bir uygulama (ingress controller, veritabanı operator'ü, autoscaler'lar) için chart doğal biçimdir; release geçmişi ve hook'lar da cabası. Kustomize'ın güçlü olduğu yer kendi uygulamanın ortam farklarıdır: dosyalar düz YAML olarak okunur, kubectl kustomize çıktısı tam olarak cluster'a gidecek şeydir, öğrenilecek template dili yoktur. Sık görülen kombinasyon: üçüncü parti bileşenler Helm chart'ıyla, kendi uygulaman Kustomize ile; Kustomize'ın helmCharts alanıyla bir chart'ı render edip üzerine patch yazmak da mümkün. Ayrıntılar ilerideki Helm serisinde.
Üretimde Dikkat
Overlay'lerin farkı küçük kalmalı; bir overlay base'den daha uzunsa base yanlış kurulmuştur. Her pull request'te bütün overlay'lerin kubectl kustomize çıktısını üretip bir şema doğrulamasından (ör. kubectl apply --dry-run=server) geçirmek, bozuk bir patch'i merge'den önce yakalar. Prod'a gitmeden önce kubectl diff -k çıktısını incelemek, yukarıdaki gibi beklenmedik selector değişikliklerini görmenin en ucuz yolu. Secret'ları secretGenerator ile repoya düz metin koymak cazip ama yanlış; harici secret yönetimi ilerideki Configuration ve Secret Management serisinde. Namespace'i overlay'de vermek bir ortamın manifest'lerini yanlışlıkla başka namespace'e uygulamayı zorlaştırır, ama context'i kontrol etmeyi gereksiz kılmaz (serinin ikinci yazısı). Son olarak, kubectl'in içindeki Kustomize ile CI'daki tek başına Kustomize'ın sürüm farkı nadiren de olsa farklı çıktı üretebilir; pipeline'da hangisini kullanıyorsan lokalde de onu kullan.
Sık Düşülen Tuzaklar
Ortam klasörlerini kopyalayıp zamanla ayrışmalarına izin vermek. commonLabels ile selector'lara değişken bir label koymak ve yarım kalan apply ile servisi kesmek. JSON Patch'te dizi index'lerine güvenip base'de env sırası değişince yanlış değişkeni ezmek. HPA kullanan overlay'de replicas'ı silmeyi unutmak. ConfigMap'i generator yerine düz manifest olarak yazıp config değişikliklerinde rollout'un tetiklenmemesine şaşırmak. Pipeline'da sed ile tag değiştirmek ve yanlış satırı eşleştirmek. Base'deki labels'ın component veya overlay'den gelen resource'lara uygulanacağını sanmak. Ve kubectl apply hata verdiğinde "hiçbir şey değişmedi" diye varsaymak.
Küçük Bir Deneme
★☆☆ Bir staging overlay'i ekle: namespace bookstore-staging, autoscaling component'i, prod'un resource patch'i ve DiscountPercent: 0. kubectl kustomize ile üç ortamın çıktısını diff ile karşılaştır.
★★☆ Dev overlay'indeki JSON Patch'i, base'e ASPNETCORE_ENVIRONMENT'tan önce yeni bir env değişkeni ekleyerek kır. Sonra aynı değişikliği strategic merge patch ile yazıp neden kırılmadığını açıkla.
★★★ Overlay'lerdeki eski ConfigMap'leri temizleyen bir yöntem kur: kubectl apply --prune ve ApplySet'i dene, hangi resource'ların silindiğini --dry-run=server ile önceden gör. Rollout undo'nun eski ConfigMap'e ihtiyaç duyduğunu da hesaba katarak kaç eski sürümü saklayacağına karar ver.
Ortamlar artık ayrı namespace'lerde ve tek kaynaktan üretiliyor. Ama bu namespace'lerdeki pod'lar hâlâ birbirine serbestçe erişebiliyor, varsayılan ServiceAccount'la çalışıyor ve root olabiliyor. Bir sonraki yazıda güvenliğe geçiyoruz: ServiceAccount, RBAC (Role-Based Access Control), Pod Security Standards, securityContext ve NetworkPolicy.