Lokal Kubernetes Cluster'ı ve kubectl: kind, k3d, minikube, Context'ler ve Namespace'ler
Seride kind kullanıyoruz: üç node'lu cluster tek bir YAML dosyasıyla 20 saniyede kuruluyor, lokal image kind load ile node'lara yükleniyor. kind, k3d, minikube ve Docker Desktop'ı karşılaştırıyor; kubeconfig, context ve namespace'leri, yanlış context'te sessizce çalışan komutları, :latest tag'inin ErrImagePull tuzağını, server dry run'ı, kubectl diff ile drift'i ve kubectl sürüm uyumunu 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 Docker 29 ile denenmiştir.
Önce Tahmin Et
Makinende iki kind cluster'ı var: biri Bookstore'un üç API (Application Programming Interface) pod'unu çalıştıran k8swork1-1, diğeri az önce denemek için açtığın boş k8swork1-2. Hiçbir context komutu çalıştırmadan kubectl get pods -n bookstore yazıyorsun. Çıktı ne olur: üç pod mu, bir hata mı, yoksa başka bir şey mi? İkinci soru: bookstore-api:latest image'ını kind load ile cluster'a yükledin ve bu image'la bir Deployment yarattın. Pod Running olur mu?
Lokal Cluster Seçenekleri
Kubernetes öğrenmek ya da manifest'lerini denemek için bulutta cluster açmana gerek yok. Dört yaygın seçenek var ve hepsi kendi makinende, çoğunlukla Docker üzerinde çalışır:
| Araç | Nasıl çalışır | Güçlü yanı | Dikkat |
|---|---|---|---|
| kind | Her node bir Docker container'ı; upstream Kubernetes (kubeadm) | Çok node, sürüm seçimi, CI (Continuous Integration) ortamında hızlı, config dosyasıyla tekrar üretilebilir | LoadBalancer ve Ingress kendiliğinden yok; port mapping elle |
| k3d | k3s'i (hafif Kubernetes dağıtımı) Docker içinde çalıştırır | Çok hızlı açılır, dahili load balancer ve registry komutları | k3s upstream'den biraz farklı paketlenir (Traefik, SQLite gibi varsayılanlar) |
| minikube | Docker ya da bir VM (Virtual Machine) sürücüsü üzerinde cluster | Addon'lar (metrics-server, dashboard), öğrenmeye odaklı | Varsayılanı tek node; VM sürücüsünde daha ağır |
| Docker Desktop | Ayarlardan açılan cluster; yeni sürümlerde kind provisioner'ı | Tek tık, ek araç yok | Docker Desktop'a bağlı; CI'da kullanılmaz |
Bu seride kind kullanıyoruz. Üç nedeni var. Birincisi çok node'lu cluster'ı tek bir YAML (YAML Ain't Markup Language) dosyasıyla tanımlayabiliyorsun; scheduling, node arızası ve pod dağılımı gibi konular tek node'da görünmez. İkincisi node image'ını seçerek istediğin Kubernetes sürümünü kurabiliyorsun. Üçüncüsü aynı config CI runner'ında da çalışıyor; GitHub Actions'ta manifest'leri gerçek bir cluster'a karşı test etmek için yaygın olarak kind kullanılır. Projenin kendi tanımı da dürüst: kind aslında Kubernetes'in kendisini test etmek için tasarlanmış, ama lokal geliştirme ve CI için de kullanılabiliyor.
kind ile Çok Node'lu Cluster
kind'ı ve kubectl'i proje klasörüne indirmek yeterli; ikisi de tek binary. Cluster'ı tanımlayan dosya:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30080
hostPort: 18780
protocol: TCP
- role: worker
- role: worker
Bir control plane, iki worker. extraPortMappings şimdilik önemsiz görünebilir ama altıncı yazıda dış trafiği cluster'a sokarken lazım olacak: control plane container'ının 30080 portunu makinenin 18780 portuna bağlıyor. kind'da node'lar Docker container'ı olduğu için host'tan cluster'a ulaşmanın en basit yolu bu.
Cluster'ı yaratmadan önce bir satır daha:
# kind ve kubectl bu dosyayı kullansın, ~/.kube/config'e dokunmasın
export KUBECONFIG=$PWD/kubeconfig
kind create cluster --name k8swork1-1 --config kind-bookstore.yaml \
--image kindest/node:v1.37.0@sha256:a1ed56cfb0e7b93589bdf97c8cd566405a265939e3620fc4f5de89adff580ae5
KUBECONFIG ayarlı değilse kind cluster'ın erişim bilgilerini ~/.kube/config'e yazar ve current context'i yeni cluster'a çevirir. O dosyada iş yerindeki cluster'ların da varsa bunu istemezsin; neden istemediğini birazdan göreceğiz. Node image'ını digest ile sabitlemek kind'ın sürüm notlarının önerisi: aynı tag farklı kind sürümleri için yeniden build edilebiliyor. Çıktının sonu:
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
✓ Joining worker nodes 🚜
Set kubectl context to "kind-k8swork1-1"
You can now use your cluster with:
kubectl cluster-info --context kind-k8swork1-1
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP OS-IMAGE CONTAINER-RUNTIME
k8swork1-1-control-plane Ready control-plane 41s v1.37.0 172.20.0.2 Debian GNU/Linux 13 (trixie) containerd://2.3.4
k8swork1-1-worker Ready <none> 27s v1.37.0 172.20.0.3 Debian GNU/Linux 13 (trixie) containerd://2.3.4
k8swork1-1-worker2 Ready <none> 27s v1.37.0 172.20.0.4 Debian GNU/Linux 13 (trixie) containerd://2.3.4
Yaklaşık 20 saniyede üç node'lu bir cluster. Kapatmak için kind delete cluster --name k8swork1-1; node'lar container olduğu için geride hiçbir şey kalmaz.
Lokal Image'ı Cluster'a Yüklemek
Bookstore image'larını makinende docker build ile ürettin, ama kind node'ları senin Docker image cache'ini görmez; her node kendi containerd'sini çalıştırır. Registry'ye push etmek yerine image'ı doğrudan node'lara kopyalayabilirsin:
$ kind load docker-image bookstore-api:1.0.0 --name k8swork1-1
Image: "bookstore-api:1.0.0" with ID "sha256:aa62128f22bfc997e14b9f8822f01803ac90592c6a13d87655b7b4df1c40ae72" not yet present on node "k8swork1-1-control-plane", loading...
Komut image'ı her node'a yükler. Şimdi baştaki ikinci soru. Aynı image'ı latest tag'iyle yükleyip bir Deployment yarattım:
$ docker tag bookstore-api:1.0.0 bookstore-api:latest
$ kind load docker-image bookstore-api:latest --name k8swork1-1
$ kubectl create deployment api-latest --image=bookstore-api:latest
deployment.apps/api-latest created
$ kubectl get deploy api-latest -o jsonpath='{.spec.template.spec.containers[0].imagePullPolicy}'
Always
$ kubectl get pods -l app=api-latest
NAME READY STATUS RESTARTS AGE
api-latest-76677f88bf-bftt9 0/1 ErrImagePull 0 8s
$ kubectl get events --sort-by=.lastTimestamp
Normal Pulling pod/api-latest-76677f88bf-bftt9 Pulling image "bookstore-api:latest"
Warning Failed pod/api-latest-76677f88bf-bftt9 Failed to pull image "bookstore-api:latest": failed to pull and unpack image "docker.io/library/bookstore-api:latest": failed to resolve reference "docker.io/library/bookstore-api:latest": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
Warning Failed pod/api-latest-76677f88bf-bftt9 Error: ImagePullBackOff
Image node'da olduğu hâlde pod başlamadı. Tag :latest ya da hiç yoksa, Kubernetes imagePullPolicy'yi varsayılan olarak Always yapar; kubelet her seferinde registry'ye gider, tag'in kısaltılmış hâli docker.io/library/bookstore-api olduğu için Docker Hub'a sorar ve orada böyle bir repository yok. Sürüm tag'i (1.0.0) kullandığında varsayılan IfNotPresent olur ve node'daki image kullanılır. Bu yüzden seride hep sürümlü tag kullanıyoruz; dördüncü yazıda bunun production tarafındaki nedenini de göreceğiz.
kubeconfig, Context ve Namespace
kubectl hangi cluster'la konuşacağını kubeconfig dosyasından öğrenir. Dosyada üç liste var: cluster'lar (API server adresi ve CA (Certificate Authority) sertifikası), user'lar (kimlik bilgisi) ve ikisini bir varsayılan namespace ile eşleştiren context'ler. current-context alanı, --context vermediğin her komutun nereye gideceğini belirler.
Baştaki ilk soruya gelelim. Bookstore cluster'ı çalışırken ikinci bir kind cluster'ı yarattım:
$ kind create cluster --name k8swork1-2 --image kindest/node:v1.37.0@sha256:a1ed56cf...
...
Set kubectl context to "kind-k8swork1-2"
$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
kind-k8swork1-1 kind-k8swork1-1 kind-k8swork1-1
* kind-k8swork1-2 kind-k8swork1-2 kind-k8swork1-2
$ kubectl get pods -n bookstore
No resources found in bookstore namespace.
Hata yok, exit code 0, ama üç pod da yok. Yeni cluster'ı yaratmak current context'i sessizce değiştirdi ve kubectl boş cluster'a sordu. O cluster'da bookstore namespace'i bile yok, ama kubectl bunu hata olarak söylemiyor; "bu namespace'te kaynak yok" diyor. Şimdi aynı durumu bir kubectl delete ya da kubectl apply ile düşün: komut, aklındaki cluster'da değil, current context'te çalışır.
Context'i geri almak ve sık kullandığın namespace'i context'e bağlamak:
$ kubectl config use-context kind-k8swork1-1
Switched to context "kind-k8swork1-1".
$ kubectl config set-context --current --namespace=bookstore
Context "kind-k8swork1-1" modified.
$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* kind-k8swork1-1 kind-k8swork1-1 kind-k8swork1-1 bookstore
kind-k8swork1-2 kind-k8swork1-2 kind-k8swork1-2
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
api-7dd799865f-85m2z 1/1 Running 0 11m
api-7dd799865f-j8nz5 1/1 Running 0 3m41s
api-7dd799865f-vftck 1/1 Running 0 12m
$ kubectl --context kind-k8swork1-2 get nodes
NAME STATUS ROLES AGE VERSION
k8swork1-2-control-plane NotReady control-plane 11s v1.37.0
Son komut tek seferlik bir yol gösteriyor: --context current context'i değiştirmeden başka bir cluster'a komut gönderir. Script'lerde ve CI'da hep böyle açık yazmak, current context'e güvenmekten çok daha güvenli.
Namespace'ler cluster içinde isim alanı ve yetki sınırı sağlar: aynı adlı api Deployment'ı bookstore-dev ve bookstore-staging namespace'lerinde yan yana durabilir; RBAC (Role-Based Access Control) kuralları ve ResourceQuota'lar namespace bazında tanımlanır. Ama namespace bir güvenlik duvarı değil: varsayılan olarak farklı namespace'lerdeki pod'lar birbirine ağ üzerinden ulaşabilir. Bunu 15. yazıda NetworkPolicy ile kapatacağız.
Her Gün Kullanacağın kubectl Komutları
kubectl get listeler, -o wide node ve IP (Internet Protocol) sütunlarını, -o yaml object'in tamamını gösterir. kubectl describe object'i insan için okunur hâle getirir ve en altta ilgili event'leri listeler; bir pod neden başlamıyorsa ilk bakılacak yer burası. kubectl explain ise dokümantasyonu cluster'ın kendi şemasından okur, yani sürümüne tam uyan cevabı verir:
$ kubectl explain deployment.spec.strategy.rollingUpdate.maxSurge
GROUP: apps
KIND: Deployment
VERSION: v1
FIELD: maxSurge <IntOrString>
DESCRIPTION:
The maximum number of pods that can be scheduled above the desired number of
pods. Value can be an absolute number (ex: 5) or a percentage of desired
pods (ex: 10%). This can not be 0 if MaxUnavailable is 0. Absolute number is
calculated from percentage by rounding up. Defaults to 25%.
Değişiklik yapan komutlarda iki güvenlik ağı var. kubectl diff dosyadaki manifest ile cluster'daki canlı object'i karşılaştırır. --dry-run=server ise isteği API server'a gönderir, bütün doğrulama ve admission adımlarından geçirir ama kaydetmez. Farkı görmek için geçersiz bir adla bir Deployment hazırladım:
$ kubectl apply --dry-run=client -f bad2.yaml
deployment.apps/Bookstore_API created (dry run)
$ kubectl apply --dry-run=server -f bad2.yaml
The Deployment "Bookstore_API" is invalid: metadata.name: Invalid value: "Bookstore_API": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
Client tarafı dry run "oluşturuldu" dedi; çünkü yalnız YAML'ı okuyup object'e çevirdi. Server tarafı ise adın küçük harf ve tire kuralına uymadığını yakaladı. Yanlış yazılmış bir field da aynı şekilde yakalanır: replicas yerine replica yazdığımda server dry run strict decoding error: unknown field "spec.replica" döndü. Pipeline'da manifest'leri doğrulamak için client dry run'a güvenme; ya server dry run ya da gerçek bir cluster'a karşı test kullan.
kubectl ile Cluster Arasındaki Sürüm Uyumu
$ kubectl version
Client Version: v1.37.2
Kustomize Version: v5.8.1
Server Version: v1.37.0
Kubernetes'in version skew policy'sine göre kubectl, API server'ın bir minor sürüm altında ya da üstünde desteklenir: 1.37 bir cluster için 1.36, 1.37 ve 1.38. Bilgisayarında tek bir kubectl tutup 1.34'ten 1.37'ye kadar farklı cluster'lara bağlanıyorsan, en uçtaki cluster'larla desteklenmeyen bir kombinasyonda çalışıyorsun demektir; çoğu komut yine çalışır, ama yeni ya da kaldırılmış field'larda şaşırtıcı davranışlar görürsün. Proje klasörüne cluster sürümüne uygun kubectl'i indirmek bu sorunu kökten çözer. Çıktıdaki Kustomize satırı da önemli: kubectl'in içinde gömülü bir Kustomize sürümü var ve kubectl apply -k onu kullanır; 14. yazıda buna döneceğiz.
Üretimde Dikkat
Production cluster'ının kimlik bilgisini günlük çalıştığın kubeconfig'e koyma. Her ortam için ayrı bir dosya tut ve yalnız gerektiğinde, o terminal için KUBECONFIG ile seç. Terminal prompt'unda current context'i ve namespace'i gösteren bir ayar (kube-ps1 gibi araçlar ya da kendi shell fonksiyonun) ucuz ve etkili bir sigorta.
Production'a insan eliyle yazma yetkisini en aza indir. Günlük işler için yalnız okuma yetkisi olan bir user, değişiklikler için pipeline. Elle müdahale gerekiyorsa bile önce kubectl diff ve --dry-run=server.
CI'da kind kullanıyorsan cluster'ı her job'da sıfırdan kur ve sonunda sil; node image'ını digest ile sabitle. Testlerin hangi Kubernetes sürümüne karşı koştuğunu bilmek, upgrade öncesi uyumluluk testini mümkün kılar.
Sık Düşülen Tuzaklar
Yanlış context'te komut çalıştırmak. Yukarıda gördüğün gibi bir araç (kind, bir bulut CLI'ı (Command Line Interface), bir IDE (Integrated Development Environment) eklentisi) current context'i senden habersiz değiştirebilir. Silme ve apply komutlarında --context'i açıkça yaz.
Imperative komutlarla drift yaratmak. kubectl scale ve kubectl set image denemede çok pratik, ama dosyadaki manifest'i değiştirmez. Bookstore Deployment'ında ikisini de çalıştırdıktan sonra kubectl diff farkı açıkça gösterdi:
$ kubectl scale deploy/api --replicas=5
deployment.apps/api scaled
$ kubectl set image deploy/api api=bookstore-api:1.1.0
deployment.apps/api image updated
$ kubectl diff -f api.yaml
...
spec:
progressDeadlineSeconds: 600
- replicas: 5
+ replicas: 3
revisionHistoryLimit: 10
...
spec:
containers:
- - image: bookstore-api:1.1.0
+ - image: bookstore-api:1.0.0
imagePullPolicy: IfNotPresent
Eksi satırlar cluster'daki canlı durum, artı satırlar dosyayı uygularsan olacak durum. Bir sonraki kubectl apply -f api.yaml replica sayısını 3'e, image'ı 1.0.0'a geri çeker. Production'da gece yapılan "geçici" bir kubectl scale, ertesi sabahki deploy ile sessizce kaybolur.
:latest tag'i ve imagePullPolicy. Lokal cluster'da ErrImagePull, production'da ise daha sinsi bir sorun: aynı tag farklı node'larda farklı image'lara karşılık gelebilir.
Namespace'i unutmak. -n vermediğin komut context'teki namespace'e, o da yoksa default'a gider. "Pod'larım nerede?" sorusunun yarısının cevabı budur.
Küçük Bir Deneme
★☆☆ Yukarıdaki config ile kendi kind cluster'ını kur (önce KUBECONFIG'i ayarla), kubectl get nodes -o wide ile üç node'u gör ve docker ps çıktısında aynı node'ları container olarak bul.
★★☆ bookstore-dev ve bookstore-staging adında iki namespace yarat, her birine aynı Deployment'ı uygula. Sonra her namespace için ayrı bir context tanımla (kubectl config set-context dev --cluster=... --user=... --namespace=bookstore-dev) ve context değiştirerek pod listelerini karşılaştır.
★★★ İki terminal aç: birinde KUBECONFIG'i bir dosyaya, diğerinde başka bir dosyaya ayarla ve her birinde ayrı bir kind cluster'ı yarat. Bir terminalde current context'i değiştirmenin diğerini etkilemediğini doğrula. Sonra iki dosyayı KUBECONFIG=a:b ile birleştir ve kubectl config get-contexts çıktısını yorumla.
Elimizde çalışan bir cluster ve onu güvenle yöneten bir kubectl var. Bir sonraki yazıda Kubernetes'in en küçük çalıştırma birimine iniyoruz: Bookstore API'si için bir pod spec'i yazıyor, .NET container image'larının non-root ve port 8080 varsayılanlarını, pod'un yaşam döngüsünü, init container'ları ve native sidecar'ları inceliyoruz.