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

Lokal Kubernetes Cluster'ı ve kubectl: kind, k3d, minikube, Context'ler ve Namespace'ler

Mert Özen 8 Kas 2026 10 dk 3 görüntülenme
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ışırGüçlü yanıDikkat
kindHer 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 üretilebilirLoadBalancer ve Ingress kendiliğinden yok; port mapping elle
k3dk3s'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)
minikubeDocker ya da bir VM (Virtual Machine) sürücüsü üzerinde clusterAddon'lar (metrics-server, dashboard), öğrenmeye odaklıVarsayılanı tek node; VM sürücüsünde daha ağır
Docker DesktopAyarlardan açılan cluster; yeni sürümlerde kind provisioner'ıTek tık, ek araç yokDocker 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.