~/yazilar/kubernetes-ilk-uygulamayi-yayina-almak
Kubernetes'te ilk uygulamayı yayına almak
içindekiler
$ kubectl -n demo get pods
NAME READY STATUS RESTARTS AGE
podinfo-5d7f8b6c4-tq8xz 0/1 ImagePullBackOff 0 3m12s
podinfo-7b9c5f6d8-2kmwp 1/1 Running 0 48m
podinfo-7b9c5f6d8-r4nlx 1/1 Running 0 48mYeni sürümün image etiketi yanlış yazıldığında tablo böyle görünüyor: yeni pod image'ı çekemiyor ama eski iki pod hâlâ Running ve trafik onlarda. Uygulamayı kullanan kimse bir şey fark etmiyor, eski sürüme dönmek de tek komut.
Bu tablo tesadüf değil, Deployment'ı doğru yazmanın sonucu. Uygulamayı baştan YAML ile yayına alıp sonunda bu durumu bilerek oluşturacağım.
Örnek uygulama olarak podinfo kullanıyorum. Kubernetes denemeleri için yazılmış küçük bir Go uygulaması: sağlık ve hazır olma uç noktaları var, ortam değişkenleriyle ayarlanabiliyor ve ekranında hangi sürümün çalıştığını gösteriyor. Kendi uygulamanı koyacaksan port ve sağlık yolu dışında değişen pek bir şey yok.
Neden YAML
Önceki yazıda her şeyi kubectl create ile yaptım. Hızlı, ama ne yaptığımın kaydı yok. Altı ay sonra "bu Ingress'i kim, hangi ayarla açtı" sorusunun cevabı hiçbir yerde durmuyor. YAML dosyaları bir repoda olunca değişiklikler gözden geçirilebiliyor, aynı uygulama başka bir kümeye birebir kurulabiliyor ve küme bir gün giderse geri getirmek kubectl apply kadar sürüyor.
Dosyaları şöyle diziyorum:
k8s/podinfo/
├── 00-namespace.yaml
├── configmap.yaml
├── deployment.yaml
├── service.yaml
└── ingress.yamlSecret listede yok, bilerek. Aşağıda nedenini anlatıyorum.
Namespace
Her uygulamayı kendi namespace'ine koyuyorum. Silmek, yetki vermek ve "bu uygulamada neler çalışıyor" diye bakmak kolaylaşıyor.
# 00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: demoSecret'ları elle oluşturacağım için namespace'i şimdiden açıyorum:
kubectl apply -f k8s/podinfo/00-namespace.yamlConfigMap
Uygulamanın gizli olmayan ayarları buraya. podinfo PODINFO_ önekli ortam değişkenlerini okuyor:
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: podinfo-config
namespace: demo
data:
PODINFO_UI_MESSAGE: "Bilgi İşlem test ortamı"
PODINFO_UI_COLOR: "#2f5d7c"Compose'daki environment bloğunun ayrı bir dosyaya çıkmış hali gibi düşünebilirsin. Fark şu: aynı ConfigMap'i birden fazla Deployment kullanabiliyor ve istersen ortam değişkeni yerine dosya olarak da bağlayabiliyorsun.
Secret
Veritabanı parolası gibi şeyler için Secret var. Ama önce şunu netleştireyim:
Görmek için bir tane oluşturup okuyorum. Parolayı komut satırına yazmamak için read -s kullanıyorum, böylece shell geçmişine düşmüyor:
read -rs -p "DB parolası: " DB_PASSWORD; echo
kubectl -n demo create secret generic podinfo-secret \
--from-literal=DB_PASSWORD="$DB_PASSWORD"
unset DB_PASSWORD
kubectl -n demo get secret podinfo-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d; echoShell geçmişi temiz kalıyor ama değer kubectl çalıştığı süre boyunca komut satırı argümanı olarak duruyor ve o sırada süreç listesinde görülebiliyor. Başka kullanıcıların da girdiği bir yönetim makinesindeysen değeri izinleri 600 olan geçici bir dosyaya yazıp --from-file ile vermek daha temiz.
Son komut parolayı olduğu gibi ekrana basıyor. Bu yüzden Secret YAML'ını repoya koymuyorum: base64 bir değeri git'e koymak, parolayı düz metin koymakla aynı şey. Repoya şifreli koymak istersen Sealed Secrets, SOPS ya da External Secrets Operator gibi araçlar var. Küçük bir ortamda Secret'ı elle oluşturmak ve parolayı kurumun parola kasasında tutmak da makul bir başlangıç.
k3s tarafında küme veritabanındaki Secret'ları diskte şifrelemek için /etc/rancher/k3s/config.yaml dosyasına secrets-encryption: true eklenebiliyor. Bunu kurulumda açmak sonradan açmaktan daha kolay.
Deployment
Asıl dosya bu. Önce tamamını veriyorum, sonra parça parça neden öyle yazdığımı anlatıyorum:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo
namespace: demo
labels:
app: podinfo
spec:
replicas: 2
revisionHistoryLimit: 5
selector:
matchLabels:
app: podinfo
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: podinfo
spec:
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:6.7.0
ports:
- name: http
containerPort: 9898
envFrom:
- configMapRef:
name: podinfo-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: podinfo-secret
key: DB_PASSWORD
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 128Mi
readinessProbe:
httpGet:
path: /readyz
port: http
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]selector.matchLabels ile template.metadata.labels aynı olmak zorunda. Deployment hangi pod'ların kendisine ait olduğunu bu etiketle buluyor, birazdan Service de aynı etiketi kullanacak.
podinfo bu parolayı kullanmıyor, Secret'ın container'a nasıl girdiğini göstermek için ekledim. Kendi uygulamanda aynı şekilde okursun.
Request ve limit
requests, zamanlayıcının pod'u yerleştirirken ayırdığı kaynak. Düğümde bu kadar boş yer yoksa pod Pending durumda bekliyor. limits ise üst sınır. Bellek sınırını aşan container OOMKilled ile öldürülüyor, böylece sızıntı yapan bir uygulama bütün düğümü sıkıştıramıyor.
CPU için bilerek limit koymadım. CPU limiti aşılınca container öldürülmüyor, kısılıyor (throttling). Düğümde boş CPU olsa bile uygulama yavaşlıyor ve bunu fark etmek zor oluyor. Bence küçük kümelerde CPU için sadece request vermek, bellek için hem request hem limit vermek daha az sürpriz çıkarıyor. Farklı düşünen ekipler de var, ortamına göre karar ver.
Değerleri tahminle başlatıp sonra gerçek kullanıma bakarak düzeltiyorum. k3s'te metrics-server kurulu geldiği için:
kubectl -n demo top podsProbe'lar
İki probe iki ayrı soruyu soruyor. Readiness: "bu pod şu an trafik almaya hazır mı?" Cevap hayırsa pod Service'in arkasından çıkarılıyor ama öldürülmüyor. Liveness: "bu pod hâlâ yaşıyor mu?" Cevap üst üste hayırsa container yeniden başlatılıyor.
Yazının başındaki tablonun sırrı readiness probe ve maxUnavailable: 0 ikilisi. Yeni pod hazır olduğunu söylemeden eskilerden biri kapatılmıyor. Yeni pod hiç hazır olamazsa (image çekilemedi, uygulama açılırken çöktü) eski pod'lar olduğu gibi çalışmaya devam ediyor.
Liveness'ı sağlık yolunu ağır kontroller yapan bir uç noktaya bağlamamaya dikkat ediyorum. Liveness veritabanına bağlanmayı da kontrol ederse, veritabanı birkaç saniye takıldığında bütün pod'lar aynı anda yeniden başlıyor ve kısa bir aksaklık kesintiye dönüyor. Liveness sadece "süreç kilitlendi mi" sorusunu sorsun, bağımlılık kontrolleri readiness'ta dursun.
Service ve Ingress
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: podinfo
namespace: demo
spec:
selector:
app: podinfo
ports:
- name: http
port: 80
targetPort: httptargetPort olarak sayı yerine Deployment'taki port adını verdim. Uygulamanın portu değişirse sadece Deployment'ı düzeltmek yetiyor.
Kurumda *.k8s.example.com için bir wildcard sertifikan varsa Ingress'te TLS'i onunla açmak en kısa yol. Wildcard sadece tek bir etiketi kapsıyor: *.example.com sertifikası podinfo.example.com için geçerli ama podinfo.k8s.example.com için değil. Sertifikayı Secret olarak yüklüyorum:
kubectl -n demo create secret tls wildcard-k8s-example-com \
--cert=wildcard.k8s.example.com.crt --key=wildcard.k8s.example.com.key# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: podinfo
namespace: demo
spec:
ingressClassName: traefik
tls:
- hosts:
- podinfo.k8s.example.com
secretName: wildcard-k8s-example-com
rules:
- host: podinfo.k8s.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: podinfo
port:
name: httppathType zorunlu bir alan. Prefix verilen yolla başlayan her isteği eşliyor, Exact sadece tam yolu. Let's Encrypt ile otomatik sertifika istiyorsan cert-manager kurmak gerekiyor, bu yazının kapsamına almadım.
Uygulamak
kubectl apply -f k8s/podinfo/
kubectl -n demo rollout status deployment/podinfonamespace/demo unchanged
configmap/podinfo-config created
deployment.apps/podinfo created
ingress.networking.k8s.io/podinfo created
service/podinfo created
deployment "podinfo" successfully rolled outapply bir dizindeki dosyaları alfabetik sırayla okuyor, namespace dosyasının başına 00- koymamın sebebi bu. Kümeyi sıfırdan kurarken namespace diğer her şeyden önce oluşuyor. Burada namespace'i daha önce açtığımız için unchanged görünüyor.
curl -s https://podinfo.k8s.example.com/ | head -20Dönen JSON'da "message" alanında ConfigMap'teki metni, "version" alanında da 6.7.0 görmelisin.
Rolling update
Yeni sürüme geçmek için deployment.yaml içinde image etiketini değiştiriyorum ve tekrar uyguluyorum. kubectl set image ile de yapılabiliyor ama o zaman repodaki dosya ile kümedeki durum ayrışıyor, bunu sevmiyorum.
sed -i 's|podinfo:6.7.0|podinfo:6.7.1|' k8s/podinfo/deployment.yaml
kubectl apply -f k8s/podinfo/deployment.yaml
kubectl -n demo annotate deployment/podinfo \
kubernetes.io/change-cause="podinfo 6.7.1" --overwrite
kubectl -n demo rollout status deployment/podinfoWaiting for deployment "podinfo" rollout to finish: 1 out of 2 new replicas have been updated...
Waiting for deployment "podinfo" rollout to finish: 1 old replicas are pending termination...
deployment "podinfo" successfully rolled outBu sırada başka bir terminalde watch -n1 kubectl -n demo get pods açık tutarsan sırayı görebilirsin: önce yeni bir pod açılıyor, hazır olunca eskilerden biri kapanıyor, sonra ikincisi. change-cause notu da geçmişte neyin neden yapıldığını okunur kılıyor:
kubectl -n demo rollout history deployment/podinfodeployment.apps/podinfo
REVISION CHANGE-CAUSE
1 <none>
2 podinfo 6.7.1Bozmak ve geri almak
Şimdi yazının başındaki duruma bilerek düşüyorum. Var olmayan bir etiket veriyorum:
sed -i 's|podinfo:6.7.1|podinfo:6.7.99|' k8s/podinfo/deployment.yaml
kubectl apply -f k8s/podinfo/deployment.yaml
kubectl -n demo get pods
kubectl -n demo describe pod -l app=podinfo | grep -A3 "Failed" Warning Failed 12s kubelet Failed to pull image "ghcr.io/stefanprodan/podinfo:6.7.99": ... not found
Warning Failed 12s kubelet Error: ErrImagePull
Normal BackOff 1s kubelet Back-off pulling image "ghcr.io/stefanprodan/podinfo:6.7.99"Uygulama hâlâ ayakta, curl ile bakınca 6.7.1 dönüyor. rollout status ise bekliyor ve varsayılan 10 dakikalık süre (progressDeadlineSeconds) dolunca hata veriyor. Bir önceki sürüme dönmek için:
kubectl -n demo rollout undo deployment/podinfo
kubectl -n demo rollout status deployment/podinfoBelirli bir revizyona dönmek istersen --to-revision=1 ekleyebilirsin. Geri almadan sonra yapılacak bir iş daha var: repodaki deployment.yaml hâlâ 6.7.99 diyor. Dosyayı düzeltmezsen bir sonraki kubectl apply bozuk sürümü geri getirir. rollout undo acil durum düğmesi, kalıcı düzeltme repoda yapılıyor.
ConfigMap değişince ne oluyor
ConfigMap'teki mesajı değiştirip kubectl apply dediğinde uygulamanın ekranında hiçbir şey değişmiyor. Ortam değişkenleri container açılırken okunuyor, çalışan pod'lar eski değerle devam ediyor. Pod'ları sırayla yeniden başlatmak gerekiyor:
kubectl -n demo rollout restart deployment/podinfoBu da normal bir rolling update gibi ilerliyor, kesinti olmuyor. Bunu her seferinde elle yapmak yerine otomatiğe bağlamanın bir yolunu Helm yazısında göstereceğim.
Sık hatalar
CrashLoopBackOff: Container açılıp hemen kapanıyor, Kubernetes de giderek uzayan aralıklarla tekrar deniyor. kubectl -n demo logs <pod> --previous çöken bir önceki denemenin loglarını gösteriyor. Eksik bir ortam değişkeni ya da ulaşılamayan bir bağımlılık en sık sebep.
Pending kalan pod: describe çıktısında 0/1 nodes are available: 1 Insufficient memory. gibi bir satır görüyorsan request değerlerin düğüme sığmıyor. Tek düğümlü kümede bu sınıra çabuk geliniyor.
OOMKilled: describe içinde Last State: Terminated ve Reason: OOMKilled görüyorsan bellek limitine çarptın. Limiti artırmadan önce uygulamanın gerçekten o kadar belleğe ihtiyacı olup olmadığına bak.
Ingress 404 dönüyor: Traefik 404 page not found dönüyorsa isteğe uyan bir kural bulamamış demektir. Host adını, ingressClassName değerini ve Ingress ile Service'in aynı namespace'te olup olmadığını kontrol et.
Service var ama cevap gelmiyor: Service'in arkasında hazır pod olmayabilir. Ya etiketler tutmuyor ya da readiness probe başarısız:
kubectl -n demo get endpointslices -l kubernetes.io/service-name=podinfoAdres listesi boşsa önce Service'teki selector ile pod etiketlerini karşılaştır, sonra describe pod içindeki Readiness probe failed satırlarına bak.
Uygulama yavaş açılıyor ve liveness öldürüyor: Açılışı bir dakika süren bir uygulamada liveness probe uygulama daha ayağa kalkmadan devreye girip container'ı yeniden başlatabiliyor ve döngüye giriyor. initialDelaySeconds değerini büyütmek yerine startupProbe tanımlamak daha temiz. Startup probe başarılı olana kadar liveness hiç çalışmıyor.
Beş dosyalık bu uygulama ikinci bir ortama, mesela bir test ortamına kurulmak istendiğinde dosyaları kopyalayıp host adlarını ve replika sayısını elle değiştirmek gerekiyor. Bu tekrarı ortadan kaldıran araç Helm.
Sıradaki yazı: Helm ile chart kurmak ve kendi chart'ını yazmak