Cluster API'yi Kurarak Anlamak: kind, Docker Provider ve 15 Dakikada Bir Workload Cluster

kind üzerinde Cluster API kurun, Docker provider ile workload cluster açın, ölçekleyip yükseltin; provider modelinin neden var olduğunu görün.

Yazan: Mirac Can Yılmaz 15 dk okuma

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.
  • kind 0.33, clusterctl 1.14, kubectl ve helm. macOS'te hepsini brew install kind clusterctl kubectl helm ile 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:

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

Hayatta en hakiki mürşit ilimdir.
Mustafa Kemal Atatürk