Kubernetes'te bir Deployment yazıyoruz, gerisini controller hallediyor. Ama cluster'ın kendisi için böyle bir şey yok. Cluster kurmak hâlâ bir shell script, bir Terraform modülü ya da bulut konsolundan tıklamak demek. Node eklemek için de aynı script'i tekrar çalıştırıp idempotent olmasını umuyoruz. Cluster API (kısaca CAPI) tam olarak bu boşluğu dolduruyor. Cluster bir Kubernetes objesi oluyor, node'lar Machine oluyor ve node havuzunu büyütmek bir YAML alanını değiştirmekten ibaret hale geliyor.
Bu yazıda sıfırdan, kendi bilgisayarımızda bir tane kuracağız. Sonunda elimizde bir yönetim cluster'ı, bir control plane ve üç worker'dan oluşan bir workload cluster'ı olacak. Üstüne tek bir alanı değiştirerek Kubernetes sürümünü yükselteceğiz. Her şey Docker container'ı olarak çalışıyor, Cluster API'nin Docker provider'ı (CAPD) sayesinde. Bulut hesabı gerekmiyor, iş bitince make clean diyoruz ve hiçbir şey kalmıyor.
Yazıdaki bütün kodlar cluster-api-explained-with-capd klasöründe.
Gerekenler
- Docker. Ben Apple Silicon Mac'te Colima kullanıyorum, 4 CPU / 8 GB'lık bir VM ile. Docker Desktop da aynı şekilde çalışıyor, sadece aşağıda değineceğim bir sysctl farkı var.
kind0.33,clusterctl1.14,kubectlvehelm. macOS'te hepsinibrew install kind clusterctl kubectl helmile kurabilirsiniz.- Upgrade sırasında yaklaşık 6 GB boş RAM. Bu denemede boştaki değerler şöyleydi: yönetim node'u 1,5 GB, workload control plane 1 GB, her worker yaklaşık 0,5 GB.
Cluster API bilmenize gerek yok, yazı kendi başına yeterli.
Neden böyle
Cluster API cluster yönetmenin tek yolu değil. Rancher bize bir UI veriyor ve elimizdeki cluster'ı import ediyor. Terraform provider'ları cluster'ı bir kaynak olarak yaratıyor. Omni aynı işi Talos'a özel yapıyor. Cluster API'nin farkı şu: cluster'ın yaşam döngüsü Kubernetes'in içinde yaşıyor, controller'lar tarafından yönetiliyor ve zaten bildiğimiz kavramlarla çalışıyor. MachineDeployment bir Deployment gibi davranıyor. replicas değerini değiştiriyoruz, MachineSet yeni Machine yaratıyor ya da siliyor. Sürümü değiştiriyoruz, makineler sırayla yenileniyor. Serinin devamı bu modelin üstüne kurulu. O yüzden nerede tıkandığını görmeden önce (o bir sonraki yazı) nasıl çalıştığını görmemiz lazım.
Peki neden ilk denemede gerçek bir bulut yerine Docker provider? Çünkü asıl mesele provider modelini anlamak. Her provider aynı üç soruya cevap veriyor ve CAPD sayesinde bu cevapları kredi kartı girmeden görebiliyoruz:
| Provider ailesi | Cevapladığı soru | Bu lab'da |
|---|---|---|
| Infrastructure | "Bana bir makine ver" | Docker: her node için bir kindest/node container'ı |
| Bootstrap | "Bu makine nasıl Kubernetes node'u olacak" | kubeadm |
| Control plane | "Kaç control plane node'u olacak, nasıl yenilenecek" | kubeadm |
İlk satırı Hetzner, OpenStack ya da KubeVirt ile değiştirdiğimizde başka hiçbir şey değişmiyor. İkinci ve üçüncü satırı Talos ile değiştirdiğimizde ise Docker provider çalışmıyor. Sebebini bir sonraki yazıda kaynak kod üzerinden göreceğiz.
Adım 1 — Docker ile konuşabilen bir yönetim cluster'ı
CAPD workload node'larını, yönetim cluster'ının çalıştığı Docker daemon'ının üstünde container olarak yaratıyor. Bu yüzden yönetim cluster'ının Docker socket'ini görmesi gerekiyor. Aşağıdaki kind config'inde farklı olan tek şey bu.
# kind/mgmt.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: capi-mgmt
nodes:
- role: control-plane
extraMounts:
- hostPath: /var/run/docker.sock
containerPath: /var/run/docker.sock
kind create cluster --config kind/mgmt.yaml
Benim makinede 20 saniye sürdü. Şimdi provider'ları kuruyoruz. clusterctl init çekirdek controller'ı ve her aileden bir provider'ı kuruyor. Sürümleri clusterctl'nin minor sürümüne sabitledim ki lab her seferinde aynı çalışsın. CAPD bir test provider'ı olduğu için bilerek bir feature flag arkasında duruyor.
# clusterctl/init.sh
export CLUSTER_TOPOLOGY=true
clusterctl init \
--core "cluster-api:v1.14.2" \
--bootstrap "kubeadm:v1.14.2" \
--control-plane "kubeadm:v1.14.2" \
--infrastructure "docker:v1.14.2"
Installing cert-manager version="v1.21.1"
Installing provider="cluster-api" version="v1.14.2" targetNamespace="capi-system"
Installing provider="bootstrap-kubeadm" version="v1.14.2" targetNamespace="capi-kubeadm-bootstrap-system"
Installing provider="control-plane-kubeadm" version="v1.14.2" targetNamespace="capi-kubeadm-control-plane-system"
Installing provider="infrastructure-docker" version="v1.14.2" targetNamespace="capd-system"
Your management cluster has been initialized successfully!
Bu da 25 saniye. Dört controller manager, her biri kendi namespace'inde, bir de webhook'lar için cert-manager:
capd-system capd-controller-manager-f5c788445-hdlf8 1/1 Running
capi-kubeadm-bootstrap-system capi-kubeadm-bootstrap-controller-manager-5fbf8c6fff-k6w4l 1/1 Running
capi-kubeadm-control-plane-system capi-kubeadm-control-plane-controller-manager-579d6964f-dll79 1/1 Running
capi-system capi-controller-manager-68985f747c-rb44p 1/1 Running
Adım 2 — Cluster'ı tarif edip uyguluyoruz
clusterctl generate cluster komutu provider'ın şablonundan bir manifest üretiyor. Docker provider'ın development adında bir flavor'ı var ve bu flavor ClusterClass kullanıyor. Yani tekrar kullanılabilir bir cluster sınıfı tanımlıyoruz, sonra ona referans veren kısa bir Cluster objesi yazıyoruz. Üretilen dosyayı repoya commit'ledim, böylece lab GitHub'a bağımlı olmuyor.
CLUSTER_TOPOLOGY=true clusterctl generate cluster capi-lab \
--infrastructure docker --flavor development \
--kubernetes-version v1.34.0 \
--control-plane-machine-count=1 --worker-machine-count=1 > workload/cluster.yaml
Dosya 411 satır. İçindeki sekiz objenin yedisi sınıf ve şablonları. Bizim gerçekten düzenleyeceğimiz obje en sondaki:
# workload/cluster.yaml (son kısım)
kind: Cluster
metadata:
name: capi-lab
spec:
clusterNetwork:
pods: { cidrBlocks: [192.168.0.0/16] }
services: { cidrBlocks: [10.128.0.0/12] }
topology:
classRef:
name: quick-start
version: v1.34.0
controlPlane:
replicas: 1
workers:
machineDeployments:
- class: default-worker
name: md-0
replicas: 1
Not: Sınıftaki kind'lara dikkat edin. DockerMachineTemplate değil, DevClusterTemplate ve DevMachineTemplate yazıyor. v1.14 ile birlikte Docker provider ve bellek içi test provider'ı tek bir "dev" provider altında birleşti. Eski anlatımlarda Docker* isimlerini görürsünüz, objeler aynı şekilde davranıyor.
kubectl apply -f workload/cluster.yaml
Bundan sonrasını 20 saniyede bir örnekledim. Cluster API'nin bütün modeli bu ekranda:
--- t=20s ---
NAME CLUSTERCLASS AVAILABLE CP DESIRED CP AVAILABLE W DESIRED W AVAILABLE PHASE
cluster.cluster.x-k8s.io/capi-lab quick-start False 1 0 1 0 Provisioned
NAME CLUSTER NODE NAME READY PHASE AGE
machine.cluster.x-k8s.io/capi-lab-jhh88-v6r77 capi-lab False Provisioning 8s
--- t=60s ---
kubeadmcontrolplane.controlplane.cluster.x-k8s.io/capi-lab-jhh88 AVAILABLE=True INITIALIZED=true
--- t=80s ---
machine.cluster.x-k8s.io/capi-lab-jhh88-v6r77 capi-lab capi-lab-jhh88-v6r77 False Running
machine.cluster.x-k8s.io/capi-lab-md-0-rdj2m-5tstr-m7q59 capi-lab capi-lab-md-0-rdj2m-5tstr-m7q59 False Running
Önce control plane için bir Machine yaratılıyor. Worker'ın Machine'i ise control plane'deki kubeadm bir join token üretene kadar Pending durumunda bekliyor. Sonra ikisi de Provisioning, Provisioned ve Running aşamalarından geçiyor. Docker tarafına bakınca üç container görüyoruz: bir haproxy load balancer (bu DevCluster oluyor) ve iki kindest/node container'ı (bunlar da DevMachine'ler).
capi-lab-lb kindest/haproxy:v20230606-42a2262b 0.0.0.0:32768->6443/tcp
capi-lab-jhh88-v6r77 kindest/node:v1.34.0 127.0.0.1:32770->6443/tcp
capi-lab-md-0-rdj2m-5tstr-m7q59 kindest/node:v1.34.0
İki makine de Running ama READY=False. Bu bir hata değil, sebebini şimdi göreceğiz.
Adım 3 — Cluster API CNI kurmuyor
Cluster API'nin işi kubelet cluster'a katıldığında bitiyor. Ağ katmanı işletenin kararı. Bir CNI kurana kadar node'lar NotReady kalıyor. Machine objesinin condition'ı bunu açıkça söylüyor:
$ kubectl get machine capi-lab-jhh88-v6r77 -o jsonpath='{.status.conditions[?(@.type=="NodeReady")].message}'
* Node.Ready: container runtime network not ready: NetworkReady=false
reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized
Workload cluster'ın kendi kubeconfig'i var ve yönetim cluster'ında bir Secret olarak duruyor. Colima ve Docker Desktop kullananlar için küçük bir pürüz var: bu kubeconfig load balancer container'ının adresini gösteriyor, host makine oraya erişemiyor. Script'te server adresini dışarı açılan host portuna çeviriyoruz ve sadece bu lab için TLS doğrulamasını kapatıyoruz.
# scripts/cni.sh (özet)
clusterctl get kubeconfig capi-lab > capi-lab.kubeconfig
kubectl --kubeconfig capi-lab.kubeconfig config set-cluster capi-lab \
--server="https://127.0.0.1:$(docker port capi-lab-lb 6443/tcp | cut -d: -f2)" \
--insecure-skip-tls-verify=true
helm --kubeconfig capi-lab.kubeconfig upgrade --install cilium cilium/cilium \
--version 1.18.2 --namespace kube-system --set ipam.mode=kubernetes
NAME STATUS ROLES AGE VERSION
capi-lab-jhh88-v6r77 NotReady control-plane 2m10s v1.34.0
capi-lab-md-0-rdj2m-5tstr-m7q59 NotReady <none> 113s v1.34.0
NAME STATUS ROLES AGE VERSION
capi-lab-jhh88-v6r77 Ready control-plane 2m41s v1.34.0
capi-lab-md-0-rdj2m-5tstr-m7q59 Ready <none> 2m24s v1.34.0
Cilium kurulduktan 30 saniye sonra iki node da Ready oldu. Yönetim cluster'ı da aynı şeyi görüyor:
$ clusterctl describe cluster capi-lab --show-conditions=false
NAME REPLICAS AVAILABLE READY STATUS REASON
Cluster default/capi-lab, v1.34.0 2/2 2 2 True Available
├─DevCluster default/capi-lab-dg967 True Ready
├─KubeadmControlPlane default/capi-lab-jhh88, v1.34.0 1/1 1 1 True Available
│ └─Machine default/capi-lab-jhh88-v6r77, v1.34.0 1 1 1 True Ready
└─Workers
└─MachineDeployment default/capi-lab-md-0-rdj2m, v1.34.0 1/1 1 1 True Available
└─Machine default/capi-lab-md-0-rdj2m-5tstr-m7q59, v1.34.0 1 1 1 True Ready
Bu ağaç aslında CRD haritasının kendisi. Cluster objesi bir DevCluster (infrastructure), bir KubeadmControlPlane (control plane) ve bir MachineDeployment (worker'lar) sahibi. Her Machine de bir KubeadmConfig (bootstrap) ve bir DevMachine (infrastructure) sahibi. Tam çizimi docs/crd-map.md dosyasına koydum.
Adım 4 — Deployment gibi ölçekliyoruz
ClusterClass kullanırken MachineDeployment objesine dokunmuyoruz. Onun sahibi topology controller ve biz değiştirsek bile geri alıyor. Bunun yerine Cluster objesini düzenliyoruz:
kubectl patch cluster capi-lab --type merge \
-p '{"spec":{"topology":{"workers":{"machineDeployments":[{"class":"default-worker","name":"md-0","replicas":3}]}}}}'
--- t=15s ---
machinedeployment.cluster.x-k8s.io/capi-lab-md-0-rdj2m DESIRED=3 CURRENT=3 READY=1 PHASE=Running
machine.cluster.x-k8s.io/capi-lab-md-0-rdj2m-5tstr-9rvbc Provisioning 14s
machine.cluster.x-k8s.io/capi-lab-md-0-rdj2m-5tstr-m7q59 Running 3m35s
machine.cluster.x-k8s.io/capi-lab-md-0-rdj2m-5tstr-rrlng Provisioning 14s
İki yeni Machine geldi, tıpkı bir ReplicaSet'in iki yeni Pod yaratması gibi. Sonra benim bilgisayarımda beş dakika boyunca hiçbir şey olmadı.
Nerede patladı
İki yeni makine Provisioning aşamasından hiç çıkmadı. docker ps çıktısında beş yerine üç capi-lab container'ı vardı. DevMachine objesinin condition'ı systemd'yi işaret ediyordu:
CGroupsReady: Waiting for cgroups ready failed: multi-user target not reached yet
CAPD controller'ının loguna baktığımda container'ların yaratıldığını gördüm ("Creating worker machine container with image kindest/node:v1.34.0, mode kind 0.20"). Ama bir saniye sonra 255 çıkış koduyla ölmüşlerdi. Asıl hata container'ın kendi logundaydı:
Welcome to Debian GNU/Linux 12 (bookworm)!
Failed to create control group inotify object: Too many open files
Failed to allocate manager object: Too many open files
[!!!!!!] Failed to allocate manager object.
Exiting PID 1...
Her kind node'u aslında bir systemd container'ı ve systemd her unit için birkaç inotify instance'ı açıyor. Colima'nın VM'inde fs.inotify.max_user_instances değeri 128 geliyor. Yönetim node'u, control plane ve ilk worker bu hakkı zaten tüketmişti. Dördüncü systemd tek bir instance bile alamayınca daha kubelet başlamadan çıktı. Cluster API ise tam yapması gerekeni yaptı: beş dakika sonra quick-start sınıfıyla gelen MachineHealthCheck sağlıksız makineleri sildi, MachineSet iki yenisini yarattı ve onlar da aynı şekilde öldü.
Çözüm VM içinde tek bir sysctl. kind'ın kendi dokümantasyonu da aynı değerleri öneriyor:
# scripts/colima-limits.sh
colima ssh -- sudo sysctl -w fs.inotify.max_user_instances=512 fs.inotify.max_user_watches=1048576
Sonra health check'in beş dakika daha beklemesini beklemek yerine takılı makineleri elle sildim:
kubectl delete machine capi-lab-md-0-rdj2m-5tstr-b6ttp capi-lab-md-0-rdj2m-5tstr-fhdj8
t=15s ready: 2/4
t=45s ready: 2/4
t=60s ready: 4/4
NAME STATUS ROLES AGE VERSION
capi-lab-jhh88-v6r77 Ready control-plane 10m v1.34.0
capi-lab-md-0-rdj2m-5tstr-jbdzm Ready <none> 37s v1.34.0
capi-lab-md-0-rdj2m-5tstr-m7q59 Ready <none> 9m45s v1.34.0
capi-lab-md-0-rdj2m-5tstr-vzzl5 Ready <none> 37s v1.34.0
Silme işleminden Ready olmasına kadar 60 saniye geçti. Buradan iki ders çıkardım. Birincisi, hata Cluster API'nin üç kat altındaydı ama Machine objesi yine de iki komutla oraya götüren bir condition gösterdi. İkincisi, node'ları silip yeniden yaratan bir health check bulutta doğru bir varsayılan, ama dizüstünde kafa karıştırıyor. Makinelerin kaybolduğunu görürseniz hata aramadan önce bir MachineHealthCheck var mı diye bakın.
Adım 5 — Tek alan değiştirerek Kubernetes'i yükseltiyoruz
Cluster API'de Kubernetes yükseltmesi yerinde yapılmıyor. Makineler değişmez (immutable). Yeni sürümle yeni bir Machine yaratılıyor, eskisi drain edilip siliniyor. ClusterClass ile bunun tamamı tek bir alan:
kubectl patch cluster capi-lab --type merge -p '{"spec":{"topology":{"version":"v1.34.3"}}}'
kubectl get machines çıktısını 20 saniyede bir aldım (isim soneki / faz / sürüm / Ready). İşlem sırası şöyle:
t=20s 7kg6l Provisioning v1.34.3 v6r77 Running v1.34.0 jbdzm m7q59 vzzl5 Running v1.34.0
t=60s 7kg6l Running v1.34.3 v6r77 Running v1.34.0 ...
t=80s 7kg6l Running v1.34.3 v6r77 Deleting ... ← önce control plane; yenisi katılmadan eskisi gitmiyor
t=120s 7kg6l Running v1.34.3 nrmzq Provisioned v1.34.3 jbdzm m7q59 vzzl5 Running v1.34.0
t=160s 7kg6l Ready nrmzq Ready jbdzm Deleting ← sonra worker'lar, teker teker
t=180s 955h9 Provisioning
t=240s 955h9 Ready m7q59 Deleting
t=260s shs2x Provisioning
t=320s shs2x Ready vzzl5 Deleting
KubeadmControlPlane önce control plane'i yeniliyor. Yeni makineyi yaratıyor, etcd ve API server sağlıklı olana kadar bekliyor, sonra eskisini siliyor. Ardından MachineDeployment aynı şeyi worker'lar için maxSurge: 1 ile yapıyor. Yani bir eski node gitmeden önce bir yeni node ayakta oluyor. Cluster hiçbir anda öncekinden az node'a düşmedi. kindest/node:v1.34.3 imajının çekilmesi dahil toplam süre bu makinede yaklaşık 6 dakika.
NAME CLUSTERCLASS AVAILABLE CP DESIRED CP AVAILABLE W DESIRED W AVAILABLE PHASE VERSION
capi-lab quick-start True 1 1 3 3 Provisioned v1.34.3
NAME STATUS ROLES AGE VERSION
capi-lab-jhh88-7kg6l Ready control-plane 12m v1.34.3
capi-lab-md-0-rdj2m-chlzm-955h9 Ready <none> 10m v1.34.3
capi-lab-md-0-rdj2m-chlzm-nrmzq Ready <none> 11m v1.34.3
capi-lab-md-0-rdj2m-chlzm-shs2x Ready <none> 9m5s v1.34.3
Bütün node isimleri değişti. Model bu: node'u yükseltmiyoruz, değiştiriyoruz. Baştaki RAM notunun sebebi de bu, yenileme boyunca her an fazladan bir node çalışıyor.
Sürüm atlayabilir miyiz, yoksa Kubernetes'in kuralları geçerli mi?
Kurallar aynen geçerli ve Cluster API bunları bizim yerimize uyguluyor. Kubernetes'in sürüm uyumluluk politikası şunu söylüyor: API server her seferinde bir minor sürüm ilerler, önce control plane sonra worker'lar yükselir, kubelet API server'dan en fazla üç minor geride kalabilir. Aynı minor içindeki patch sürümlerinde, mesela yukarıdaki 1.34.0 → 1.34.3 geçişinde, böyle bir sınır yok.
Cluster objesinin webhook'u tam olarak bunu kodlamış. v1.14.2'deki core/webhooks/admission/cluster.go dosyasına göre sürüm sadece artabilir, iki minor birden artamaz ve önceki upgrade devam ederken değiştirilemez. Üçünü de lab'da denedim:
$ kubectl patch cluster capi-lab --type merge -p '{"spec":{"topology":{"version":"v1.36.0"}}}'
The Cluster "capi-lab" is invalid: spec.topology.version: Invalid value: "v1.36.0":
version cannot be increased from "1.34.0" to "1.36.0"
$ kubectl patch cluster capi-lab --type merge -p '{"spec":{"topology":{"version":"v1.33.4"}}}'
The Cluster "capi-lab" is invalid: spec.topology.version: Invalid value: "v1.33.4":
version cannot be decreased from "1.34.0" to "1.33.4"
$ kubectl patch cluster capi-lab --type merge -p '{"spec":{"topology":{"version":"v1.35.0"}}}'
cluster.cluster.x-k8s.io/capi-lab patched
$ kubectl patch cluster capi-lab --type merge -p '{"spec":{"topology":{"version":"v1.36.0"}}}'
The Cluster "capi-lab" is invalid: spec.topology.version: Invalid value: "v1.36.0":
version cannot be changed: [control plane is still completing a previous upgrade,
there are still MachineDeployments completing a previous upgrade: [capi-lab-md-0-hmqmq]]
Yani 1.34'ten 1.36'ya geçmek iki ayrı patch demek. İkincisi ancak bütün makineler 1.35 olduktan sonra kabul ediliyor. Sıralamayı (önce control plane, sonra MachineDeployment'lar) da topology controller kendisi yapıyor, bizim elle sıralamamız gerekmiyor.
Not: Webhook'un bizim yerimize kontrol etmediği iki şey var. Hedef sürüm için bir kindest/node imajı (gerçek bir provider'da makine imajı) var mı ve kullandığımız eklentiler o sürümü destekliyor mu. İmaj yoksa upgrade başlıyor ama yeni control plane makinesi Provisioning aşamasında takılı kalıyor. Bir de kaçış kapısı var: unsafe.topology.cluster.x-k8s.io/disable-update-version-check annotation'ı bu kontrolleri kapatıyor. İsminde "unsafe" geçmesi boşuna değil.
Kurcalamak isteyenler için Kubernetes sürüm geçişi dokümantasyonu şuradan takip edilebilir. Ben upgrade planlarken bu sayfaları açık tutuyorum:
- Kubernetes sürüm uyumluluk politikası: hangi bileşen hangisinden ne kadar uzak olabilir.
- Kubernetes kubeadm cluster yükseltme: kubeadm her node'da ne yapıyor; Cluster API bunu bizim yerimize çalıştırıyor.
- Cluster API workload cluster yükseltme: işin CAPI tarafı, burada kullandığımız ClusterClass akışı dahil.
- Cluster API sürüm ve provider desteği: hangi CAPI sürümü yönetim ve workload cluster'da hangi Kubernetes sürümlerini destekliyor.
Doğrulama
kubectl get cluster capi-lab # AVAILABLE=True, VERSION=v1.34.3
kubectl --kubeconfig capi-lab.kubeconfig get nodes # 4 node Ready, hepsi v1.34.3
docker ps --format '{{.Names}}' | grep -c capi-lab # 5: lb + 1 cp + 3 worker
make clean # önce Cluster silinir (CAPI container'ları kaldırır), sonra kind
make clean önce Cluster objesini siliyor ve bekliyor. Cluster API'nin finalizer'ları DevMachine'leri kaldırıyor, böylece container'lar kind silinmeden önce gidiyor. Önce kind cluster'ını silerseniz workload container'ları ortada kalıyor ve elle temizlemeniz gerekiyor.
Sonuç
Cluster API'nin sunduğu şey aslında basit: cluster'ı da bir Kubernetes objesi gibi yönetmek. Ölçekleme bir replicas değişikliği, upgrade bir version değişikliği. Kuralları biz hatırlamak zorunda değiliz, webhook hatırlıyor. Docker provider ile bunu bir öğleden sonra kendi bilgisayarınızda deneyebilirsiniz.
Bu yazı Çıplak Metalden Tenant Cluster'a serisinin ilk yazısı. Sıradaki yazı Talos Linux'u çalıştırarak anlamak. Serinin geri kalanında kullanacağımız işletim sistemi bu ve az önce kullandığımız Docker provider'ın neden yetmeyeceğini de orada göreceğiz.
Bu yazının kodu: https://github.com/miraccan00/blog-wiki/tree/main/cluster-api-explained-with-capd