~/yazilar/k3s-ile-tek-dugumlu-kubernetes

k3s ile tek düğümlü bir Kubernetes kümesi

9 dk okuma

curl -sfL https://get.k3s.io -o k3s-install.sh
less k3s-install.sh

k3s'in dokümantasyonu kurulum için tek satır veriyor: curl -sfL https://get.k3s.io | sh -. Çalışıyor. Ama internetten gelen bir script'i okumadan, üstelik root yetkisi isteyecek şekilde doğrudan shell'e vermek iyi bir alışkanlık değil. Ben önce dosyaya indirip bakıyorum. Script uzun, hepsini satır satır okumak gerekmiyor. Neyi nereden indirdiğine, /usr/local/bin altına ne koyduğuna ve hangi systemd servisini oluşturduğuna bakmak yeterli. Bir de hash doğrulaması yaptığını görmek içimi rahatlatıyor.

Neden k3s

Kubernetes'i öğrenmek için ilk engel genelde kurulumun kendisi oluyor. kubeadm ile control plane kurmak, CNI seçmek, ingress controller eklemek, depolama sınıfı tanımlamak ayrı ayrı iş. k3s bunların hepsini tek bir binary içinde getiriyor: containerd, Flannel (CNI), CoreDNS, Traefik (ingress controller), ServiceLB (basit bir LoadBalancer), local-path-provisioner (yerel disk üzerinden kalıcı depolama) ve metrics-server. Tek düğümde etcd yerine SQLite kullanıyor, bu da belleği epey rahatlatıyor.

Uyumlu bir Kubernetes dağıtımı olduğu için burada öğrendiğin kubectl komutları ve YAML'lar başka bir kümede de aynı şekilde çalışıyor. Farkı paketlemede.

Tek düğümlü küme öğrenmek, test ortamı kurmak ve küçük iç servisler için uygun. Sunucu çökerse her şey gidiyor, yedeklilik yok. Üretimde yüksek erişilebilirlik istiyorsan en az üç sunucu düğümü gerekiyor, onu bu serinin kapsamı dışında bırakıyorum.

Ön koşullar

  • Ubuntu Server'ın güncel LTS sürümü kurulu bir sanal makine. İlk adımlar ve SSH sıkılaştırma yazılarındaki ayarlar yapılmış olsun.
  • En az 2 vCPU ve 2 GB RAM. Üstüne birkaç uygulama koyacaksan 4 GB daha rahat.
  • Sabit IP. Örneklerde 192.0.2.10 ve hostname k8s01 kullanıyorum.
  • 80 ve 443 portlarını kullanan başka bir şey olmasın. Docker serisinde kurduğumuz Traefik aynı makinede çalışıyorsa k3s'in Traefik'i ile çakışır.

Docker kurulu olmasına gerek yok. k3s kendi containerd'sini getiriyor, Docker ile işi yok.

Önce Docker gözüyle kavramlar

Kubernetes'in sözlüğü ilk bakışta kalabalık geliyor. Ama Docker ve Compose ile çalıştıysan karşılıkların çoğunu zaten biliyorsun:

Docker / ComposeKubernetes
docker run ile çalışan containerPod
Compose'daki bir servis, restart ve replika sayısıylaDeployment
Compose ağında servis adıyla DNS çözümlemesiService
Traefik label'ları ya da önündeki reverse proxyIngress
Named volumePersistentVolumeClaim
.env dosyasıConfigMap ve Secret

Pod, Kubernetes'in çalıştırdığı en küçük birim. İçinde bir container olur, bazen birkaç tane. Aynı pod'daki container'lar aynı IP'yi ve aynı localhost'u paylaşıyor. Compose'da network_mode: "service:app" ile bir container'ı diğerinin ağına bağlamak gibi düşünebilirsin. Pod'lar geçici: ölen pod geri gelmiyor, yerine yenisi açılıyor ve yeni bir IP alıyor. Bu yüzden pod'u elle oluşturmak pek yapılmıyor.

Deployment, "bu image'dan üç kopya çalışsın" diyen nesne. Compose'da docker compose up dediğinde servisler ayağa kalkıyor ama biri ölürse yeniden başlatmak restart politikasına kalıyor, makine giderse hiçbir şey olmuyor. Deployment'ta ise istediğin durumu yazıyorsun, Kubernetes sürekli mevcut durumla karşılaştırıp aradaki farkı kapatıyor. Pod silinirse yenisini açıyor, image etiketini değiştirirsen pod'ları sırayla yenisiyle değiştiriyor.

Service, pod'ların önüne konan sabit bir isim ve IP. Compose'da web servisine diğer container'lardan http://web diye ulaşıyordun, Compose bunu kendi DNS'iyle çözüyordu. Kubernetes'te pod IP'leri sürekli değiştiği için aynı işi Service yapıyor: web adında bir Service, etiketi eşleşen bütün pod'lara trafiği dağıtıyor.

Ingress ise dışarıdan gelen HTTP isteğini host adına ve yola bakıp doğru Service'e yönlendiren kural. Ingress kendi başına bir şey yapmıyor, onu okuyan bir ingress controller gerekiyor. k3s'te bu controller varsayılan olarak Traefik. Yani Traefik yazısında label'larla yaptığımız işi burada Ingress nesneleriyle yapıyoruz.

Kurulum

Ayarları komut satırı parametresi olarak vermek yerine bir config dosyasına yazıyorum. Sonradan neyi neden açtığımı görmek kolay oluyor. Dosya kurulumdan önce yerinde olursa k3s ilk açılışta onu okuyor:

sudo mkdir -p /etc/rancher/k3s
sudo tee /etc/rancher/k3s/config.yaml > /dev/null <<'EOF'
node-name: k8s01
write-kubeconfig-mode: "0600"
tls-san:
  - k8s01.example.com
  - 192.0.2.10
EOF

tls-san, API sunucusunun sertifikasına eklenecek ek isimler. Kümeyi kendi bilgisayarından yönetmek istersen sertifikada bu isimlerin olması gerekiyor, yoksa kubectl sertifika hatası veriyor. write-kubeconfig-mode için 0644 önerildiğini görebilirsin. Yapma, kubeconfig dosyası kümenin admin anahtarı ve sunucudaki herkesin okuyabilmesi gereken bir şey değil.

Script ortam değişkenleriyle yönetiliyor. Hangi sürümü kurduğumu bilmek istediğim için sürümü sabitliyorum:

sudo INSTALL_K3S_VERSION="v1.33.4+k3s1" sh k3s-install.sh
[INFO]  Using v1.33.4+k3s1 as release
[INFO]  Downloading hash https://github.com/k3s-io/k3s/releases/download/...
[INFO]  Downloading binary https://github.com/k3s-io/k3s/releases/download/...
[INFO]  Verifying binary download
[INFO]  Installing k3s to /usr/local/bin/k3s
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  Creating /usr/local/bin/crictl symlink to k3s
[INFO]  Creating /usr/local/bin/ctr symlink to k3s
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-uninstall.sh
[INFO]  systemd: Creating service file /etc/systemd/system/k3s.service
[INFO]  systemd: Enabling k3s unit
[INFO]  systemd: Starting k3s

kubectl, crictl ve ctr ayrı programlar değil, hepsi k3s binary'sine giden sembolik link. Servis ayakta mı diye bakıyorum:

systemctl status k3s --no-pager | head -5

kubectl'i kendi kullanıcınla çalıştırmak

Normal kullanıcıyla kubectl çalıştırınca karşına çıkan hata şu:

$ kubectl get nodes
WARN[0000] Unable to read /etc/rancher/k3s/k3s.yaml, please start server with --write-kubeconfig-mode or --write-kubeconfig-group to modify kube config permissions
error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied

Beklenen bir şey, dosyayı bilerek 0600 yaptık. Her komutu sudo ile çalıştırmak yerine dosyanın bir kopyasını kendi ev dizinime alıyorum:

mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown "$(id -u):$(id -g)" ~/.kube/config
chmod 600 ~/.kube/config
echo 'export KUBECONFIG=$HOME/.kube/config' >> ~/.bashrc
echo 'source <(kubectl completion bash)' >> ~/.bashrc
source ~/.bashrc

Tamamlama satırı isteğe bağlı ama kubectl ile pod adı yazarken çok zaman kazandırıyor. Şimdi tekrar:

kubectl get nodes
kubectl get pods -A
NAME    STATUS   ROLES                  AGE   VERSION
k8s01   Ready    control-plane,master   2m    v1.33.4+k3s1

NAMESPACE     NAME                                      READY   STATUS      RESTARTS   AGE
kube-system   coredns-...                               1/1     Running     0          2m
kube-system   helm-install-traefik-crd-...              0/1     Completed   0          2m
kube-system   helm-install-traefik-...                  0/1     Completed   1          2m
kube-system   local-path-provisioner-...                1/1     Running     0          2m
kube-system   metrics-server-...                        1/1     Running     0          2m
kube-system   svclb-traefik-...                         2/2     Running     0          1m
kube-system   traefik-...                               1/1     Running     0          1m

Hepsi kube-system namespace'inde. helm-install-... pod'larının Completed durumda olması normal, Traefik'i kurup çıkan tek seferlik işler. Traefik'in Helm ile kurulduğunu buradan da görüyorsun, serinin üçüncü yazısında buna döneceğim.

Kümeyi kendi bilgisayarından yönetmek istersen aynı dosyayı oraya kopyala ve içindeki server: https://127.0.0.1:6443 satırını https://k8s01.example.com:6443 yap. tls-san ayarını bu yüzden yaptık.

Güvenlik duvarı

UFW açıksa k3s'in pod ve servis ağlarından gelen trafiğe izin vermek gerekiyor. Yoksa pod'lar birbirine ya da CoreDNS'e ulaşamıyor ve hata mesajları hiç de güvenlik duvarını işaret etmiyor. k3s'in varsayılan ağları 10.42.0.0/16 (pod'lar) ve 10.43.0.0/16 (servisler):

sudo ufw allow from 10.42.0.0/16 to any comment 'k3s pods'
sudo ufw allow from 10.43.0.0/16 to any comment 'k3s services'
sudo ufw allow 80,443/tcp
sudo ufw allow from 192.0.2.0/24 to any port 6443 proto tcp comment 'k8s api'

6443, Kubernetes API'si. Bunu herkese açmak yerine sadece yönetim ağına açıyorum. UFW'nin temel kullanımını SSH yazısında anlatmıştım.

İlk Deployment

Compose dosyası yazmadan docker run ile bir container açar gibi, YAML yazmadan bir Deployment oluşturuyorum. YAML tarafına bir sonraki yazıda geçeceğim, şimdilik nesnelerin nasıl davrandığını görmek istiyorum:

kubectl create deployment web --image=nginx:1.27-alpine --replicas=2
kubectl get deployments,pods -o wide
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web   2/2     2            2           25s

NAME                       READY   STATUS    RESTARTS   AGE   IP          NODE
pod/web-6b9c7d8f5d-4kq2m   1/1     Running   0          25s   10.42.0.9   k8s01
pod/web-6b9c7d8f5d-x7prt   1/1     Running   0          25s   10.42.0.10  k8s01

Etiketi bilerek yazdım. nginx deseydim latest gelirdi ve iki hafta sonra aynı komut başka bir sürüm kurardı.

Şimdi Deployment'ın ne iş yaptığını görmek için pod'lardan birini siliyorum:

kubectl delete pod web-6b9c7d8f5d-4kq2m
kubectl get pods
NAME                   READY   STATUS    RESTARTS   AGE
web-6b9c7d8f5d-x7prt   1/1     Running   0          2m
web-6b9c7d8f5d-p9zcl   1/1     Running   0          4s

Silinen pod gitti, yerine farklı isimde yenisi geldi. Deployment "iki kopya olsun" diyordu, bir kopya eksilince tamamladı. Docker'da docker rm -f ile sildiğin container geri gelmez, fark burada.

Günlük işlerde en çok kullandığım komutlar da şunlar:

kubectl logs deploy/web            # deployment'taki bir pod'un logu
kubectl describe pod web-6b9c7d8f5d-x7prt
kubectl exec -it deploy/web -- sh  # docker exec'in karşılığı
kubectl scale deploy/web --replicas=3

describe çıktısının en altındaki Events bölümü, bir pod neden açılmıyor sorusunun cevabını çoğu zaman içeriyor. Image çekilemedi mi, bellek mi yetmedi, volume mu bağlanamadı, ilk bakılacak yer orası.

Service ile sabit bir isim

Pod IP'leri az önce gördüğün gibi değişiyor. Önlerine bir Service koyuyorum:

kubectl expose deployment web --port=80
kubectl get svc web
NAME   TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
web    ClusterIP   10.43.118.27   <none>        80/TCP    5s

ClusterIP tipi sadece küme içinden erişilebilir. Bunu test etmek için geçici bir pod açıp oradan istek atıyorum:

kubectl run tmp --rm -it --restart=Never --image=busybox:1.36 -- wget -qO- http://web

Ekrana nginx'in karşılama sayfası düşüyorsa Service çalışıyor demek. web ismini CoreDNS çözdü. Başka bir namespace'ten erişirken tam isim web.default.svc.cluster.local oluyor. Compose'daki servis adıyla erişimin aynısı, sadece namespace katmanı eklenmiş.

Sadece kendi makinende hızlıca bakmak istiyorsan kubectl port-forward svc/web 8080:80 de iş görüyor. Ama bu bir test aracı, kalıcı erişim için kullanma.

Ingress ile dışarı açmak

k3s'te Traefik zaten çalışıyor ve ServiceLB sayesinde sunucunun 80 ve 443 portlarını dinliyor:

kubectl -n kube-system get svc traefik
NAME      TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)                      AGE
traefik   LoadBalancer   10.43.52.14   192.0.2.10    80:31620/TCP,443:30914/TCP   20m

EXTERNAL-IP sütununda <pending> görüyorsan büyük ihtimalle 80 ya da 443'ü başka bir süreç tutuyor. sudo ss -tlnp | grep -E ':(80|443) ' ile kim olduğuna bak.

Şimdi web.k8s.example.com adına gelen istekleri web Service'ine yönlendiren bir Ingress kuralı yazıyorum:

kubectl create ingress web --class=traefik \
  --rule="web.k8s.example.com/*=web:80"
kubectl get ingress
NAME   CLASS     HOSTS                 ADDRESS      PORTS   AGE
web    traefik   web.k8s.example.com   192.0.2.10   80      6s

DNS kaydı henüz yoksa curl'e adresi elle veriyorum:

curl -s --resolve web.k8s.example.com:80:192.0.2.10 http://web.k8s.example.com/ | head -4

Karşılama sayfasının ilk satırları geliyorsa zincir tamam: istek Traefik'e geldi, Traefik Ingress kuralına bakıp web Service'ine verdi, Service de pod'lardan birine iletti. Kalıcı kullanım için DNS'te web.k8s.example.com adına 192.0.2.10 için bir A kaydı açmak yeterli. Daha pratiği *.k8s.example.com için tek bir wildcard kayıt, sonraki her uygulama için DNS'e dokunmak gerekmiyor.

Host adı tutmayan bir istek atarsan Traefik 404 page not found dönüyor. Bu bir hata değil, Traefik'in "bu isim için kuralım yok" deme şekli. İleride Ingress yazarken 404 görürsen ilk bakacağın şey host adı.

Image'lar nerede duruyor

Docker'dan gelenlerin en çok takıldığı yer burası. Sunucuda Docker kuruluysa bile docker images ya da docker ps k3s'in çalıştırdığı hiçbir şeyi göstermiyor, çünkü k3s kendi containerd'sini kullanıyor. Bakmak için:

sudo k3s crictl ps
sudo k3s crictl images

Aynı sebeple Docker ile yerelde build ettiğin bir image'ı Deployment'ta kullanmaya kalkarsan ErrImagePull alırsın, k3s o image'ı göremiyor. Test için image'ı elle aktarabilirsin:

docker save uygulama-web:1.0.0 | sudo k3s ctr images import -

Bu tek düğümde işe yarıyor ama kalıcı çözüm değil. Düğüm sayısı artınca her birine ayrı ayrı yüklemek gerekiyor. Düzgün yolu image'ı bir registry'ye göndermek, onu da sonraki yazıda image'ı tam adresiyle vererek kullanacağım.

Toparlamak

Denemeleri silmek için:

kubectl delete ingress web
kubectl delete svc web
kubectl delete deployment web

k3s'i tamamen kaldırmak istersen kurulumun bıraktığı script var: sudo /usr/local/bin/k3s-uninstall.sh. Bu komut kümedeki her şeyi, local-path ile oluşturulmuş volume'lar dahil siliyor. Kaldırmadan önce iki kez düşün.

Burada her şeyi komut satırından yaptım. Gerçek bir uygulama için bu yetmez, ne yaptığını tekrar üretebilmek ve gözden geçirebilmek için hepsinin YAML olarak bir repoda durması gerekiyor.

Sıradaki yazı: Kubernetes'te ilk uygulamayı yayına almak