Önceki iki yazıda aynı laboratuvarı iki ayrı köşeden kurduk. Birincisinde Cluster API'nin Docker provider'ı (CAPD) bir kindest/node container'ını 80 saniyede Kubernetes node'una çevirdi. İkincisinde Talos'u Docker'da çalıştırdık ve docker exec ... sh komutunun "executable file not found" ile döndüğünü gördük. İkisini birleştirip "CAPD ile Talos cluster'ı açayım" dediğinizde çalışmıyor. Bu bir eksik özellik değil: CAPD'nin node'dan beklediği üç şeyin üçü de Talos'ta bilerek yok.
Bu yazı o üç şeyi CAPD kaynak kodunda gösteriyor, aynı üç kontrolü düz docker ile hem kind hem Talos imajına uygulayan 40 saniyelik bir script veriyor (kind 3/3 geçiyor, Talos 0/3) ve Sidero Labs'in kendi deposunda Ağustos 2026'da aynı duvara çarpıp kapatılan PR'ı kanıt olarak koyuyor. Sonunda neden "henüz desteklenmiyor" değil "desteklenemez" dediğimizi ve bunun yerine ne kullanacağınızı bileceksiniz.
Yazıdaki script why-capd-cannot-run-talos klasöründe.
Gerekenler
- Cluster API'yi Kurarak Anlamak: kind, Docker Provider ve 15 Dakikada Bir Workload Cluster
- Talos Linux'u Çalıştırarak Anlamak: Shell Yok, Tek Makine Konfigürasyonu, gRPC ile Konuşulan İşletim Sistemi
- Script'i çalıştırmak isterseniz sadece Docker. Bu koşuda Colima 0.8 (Apple Silicon, VM kernel 6.8), Docker 27,
kindest/node:v1.34.0veghcr.io/siderolabs/talos:v1.13.10. Cluster kurulmuyor, iki container açılıp kapanıyor.
Bu yazı iki temel yazının üstüne kurulu; aşağıda ikisinden de sadece burada lazım olanı özetliyorum.
Önceki iki yazıdan lazım olan
01'den: CAPD bir node'u nasıl kuruyor. Cluster API işi üç provider ailesine bölüyor. Infrastructure "bana bir makine ver", bootstrap "bu makine nasıl node olacak", control plane "kaç control plane olacak ve nasıl yenilenecek" sorusunu cevaplıyor. 01'deki lab'da infrastructure CAPD'ydi (her node bir kindest/node container'ı), diğer ikisi kubeadm. Kubeadm bootstrap provider'ı her Machine için bir cloud-config üretiyor ve bunu bir Secret'a yazıyor. CAPD container'ı açıyor, sonra o cloud-config'i container'ın içinde komut komut çalıştırıyor. Aynı yazının "Nerede patladı" bölümünde bu sürecin ilk adımına takılmıştık. DevMachine condition'ı CGroupsReady: Waiting for cgroups ready failed: multi-user target not reached yet diyordu, çünkü container'ın içindeki systemd inotify limitine takılıp ölmüştü. Bu mesajı aklınızda tutun, aşağıda tekrar çıkacak.
02'den: Talos'ta neler yok. Talos'ta SSH, shell, paket yöneticisi ve systemd yok. Node'un tamamı tek bir machine config belgesi, değiştirmenin tek yolu 50000 portundaki gRPC API. Docker modunda imajın entrypoint'i /sbin/init, o da machined. docker exec ile sh ya da ls çalıştırmaya çalıştığımızda binary bulunamadı. Bu bir sıkılaştırma ayarı değil, binary'ler imajda hiç yok. Config'i de talosctl cluster create docker üretip node'lara veriyordu.
01 CAPD'nin node'dan ne istediğini, 02 Talos'un neyi vermediğini gösterdi. Bu yazı ikisini yan yana koyuyor.
Neden böyle
Karar: bu bir kurulum yazısı değil, "neden çalışmaz" yazısı. Kanıt olarak kaynak kod alıntısı ve CAPD'nin yaptığı kontrollerin elle tekrarı yeterli. CAPD + Talos bootstrap provider'ı (CABPT) + Talos control plane provider'ı (CACPPT) ile bir yönetim cluster'ı kurup Machine'lerin takılmasını izlemek de mümkündü. Bunu reddettim, çünkü aynı sonucu dakikalar süren bir kurulumun sonunda tek bir condition mesajı olarak gösterir ve sebebini açıklamaz. Script ise CAPD'nin sırayla sorduğu her soruyu ayrı ayrı soruyor ve hangi cevabın nerede "hayır" olduğunu gösteriyor.
Adım 1 — CAPD bir node'dan ne bekliyor
Kaynak kubernetes-sigs/cluster-api deposunda, test/infrastructure/docker/ altında. CAPD'nin test dizininde durması da bir şey söylüyor: bu provider Cluster API'nin kendi testleri için yazıldı. DevMachine reconcile'ı (reconcilers/backends/docker/dockermachine_backend.go) container'ı açtıktan sonra bootstrap'e geçmeden önce bir "cgroups ready" görevi çalıştırıyor. Görevin zaman aşımı 30 saniye ve çağırdığı fonksiyon şu (internal/docker/machine.go, kısaltılmış):
var waitUntilLogRegExp = regexp.MustCompile("Reached target .*Multi-User System.*")
func (m *Machine) WaitForMultiUserTarget(ctx context.Context, containerRuntime container.Runtime) error {
logs, err := containerRuntime.GetContainerLogs(ctx, m.container.Name)
if !waitUntilLogRegExp.MatchString(logs) {
return pkgerrors.New("multi-user target not reached yet")
}
return m.WaitForCrictlPs(ctx)
}
func (m *Machine) WaitForCrictlPs(ctx context.Context) error {
err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 4*time.Second, true, func(ctx context.Context) (bool, error) {
ps := m.Command("crictl", "ps")
return ps.Run(ctx) == nil, nil
})
}
Birinci sözleşme bu: container'ın logunda systemd'nin "Reached target Multi-User System" satırı görünmeli, sonra container içinde crictl ps başarılı olmalı. 01'de gördüğümüz multi-user target not reached yet mesajı buradan geliyor.
Bu kontrol geçerse CAPD önce bootstrap'in daha önce başlayıp başlamadığına bakıyor. Bunu da container'ın içinde bir shell komutuyla yapıyor:
cmd := m.container.Commander.Command("/bin/sh", "-c",
"test -f /run/cluster-api/capd.bootstrap.started && echo \"true\" || echo \"false\"")
Sonra bootstrap verisini komutlara çeviriyor (GetBootstrapCommands). Desteklenen iki biçim var, cloud-config ve ignition. Cloud-config tarafında parser'ın tanıdığı modüller de ikiyle sınırlı (internal/provisioning/cloudinit/adapter.go):
switch name {
case writefiles:
return newWriteFilesAction()
case runcmd:
return newRunCmdAction()
default:
// TODO Add a logger during the refactor and log this unknown module
return newUnknown(name)
}
write_files da dosyaları docker exec ile /bin/sh -c "cat > <yol> /dev/stdin" çalıştırarak yazıyor. Üretilen her komut container içinde tek tek koşturuluyor. Bu adımın zaman aşımı varsayılan 5 dakika ve kodda şu not duruyor: "when bootstrap fails on a Machine, there is no retry".
Özetle CAPD'nin node'dan beklediği üç şey şunlar:
| # | CAPD'nin beklediği | Kaynakta | kindest/node | Talos |
|---|---|---|---|---|
| 1 | PID 1 systemd, logda "Multi-User System" | WaitForMultiUserTarget |
var | yok (/sbin/init = machined) |
| 2 | container içinde crictl |
WaitForCrictlPs |
/usr/local/bin/crictl |
yok |
| 3 | /bin/sh ve config'in shell komutlarına çevrilmesi |
CheckForSentinelFile, write_files, runcmd |
/usr/bin/sh, bash, kubeadm |
yok; config shell ile değil API ile uygulanıyor |
Adım 2 — Talos config'i CAPD'ye nasıl görünüyor
Bu yolda bootstrap verisini Talos'un bootstrap provider'ı CABPT üretiyor. Secret'ı yazan fonksiyon (siderolabs/cluster-api-bootstrap-provider-talos, controllers/secrets.go, writeBootstrapData) tek bir anahtar koyuyor:
Data: map[string][]byte{
"value": data,
},
format anahtarı yok. CAPD'nin okuduğu taraf da şöyle:
format := s.Data["format"]
if len(format) == 0 {
format = []byte(bootstrapv1.CloudConfig)
}
Yani Talos'un machine config'i CAPD'ye cloud-config olarak görünüyor. Machine config'in üst seviye anahtarları version, machine, cluster ve bunların hiçbiri write_files ya da runcmd değil. Parser hepsini "bilinmeyen modül" olarak geçiyor, üstelik log bile yazmadan (koddaki TODO'ya bakın). Config'in Talos'a ulaşabileceği bir yol da yok.
02'de machine config'i talosctl cluster create docker üretiyordu. Onu container'a nasıl verdiğine bakınca fark ortaya çıkıyor. talosctl'nin Docker provisioner'ı (siderolabs/talos, pkg/provision/providers/docker/node.go) container'ı yaratırken iki ortam değişkeni veriyor: PLATFORM=container ve USERDATA=<base64 machine config>. Config container açılırken içeride hazır oluyor, sonradan içeri komut çalıştırılarak verilmiyor. CAPD'de bootstrap verisinin container'a ulaştığı tek yol ise container açıldıktan sonra docker exec ile çalıştırılan komutlar. İki model birbirine hiç değmiyor.

Adım 3 — Aynı kontrolleri elle yapmak
Bunu cluster kurmadan görmenin yolu, CAPD'nin sorduğu üç soruyu düz docker ile sormak. Script imajı açıyor, CAPD'nin 30 saniyelik görevi gibi logda multi-user satırını bekliyor, sonra crictl ps ve /bin/sh -c deniyor. Talos container'ı talosctl'nin kullandığı bayraklarla açılıyor ama USERDATA verilmiyor, tıpkı CAPD'nin yapacağı gibi.
# scripts/capd-checks.sh (özü)
target=$(docker logs "$NAME" 2>&1 | grep -E 'Reached target .*Multi-User System') # WaitForMultiUserTarget
docker exec "$NAME" crictl ps # WaitForCrictlPs
docker exec "$NAME" /bin/sh -c 'echo shell ok' # runcmd / write_files
$ make check
== kindest/node:v1.34.0
0) container: running
1) WaitForMultiUserTarget
OK [ OK ] Reached target multi-user.target - Multi-User System.
2) WaitForCrictlPs: crictl ps
OK
3) runcmd: /bin/sh -c
OK shell ok
== ghcr.io/siderolabs/talos:v1.13.10
0) container: running
1) WaitForMultiUserTarget
FAIL multi-user target not reached yet
2) WaitForCrictlPs: crictl ps
FAIL OCI runtime exec failed: exec failed: unable to start container process: exec: "crictl": executable file not found in $PATH: unknown
3) runcmd: /bin/sh -c
FAIL OCI runtime exec failed: exec failed: unable to start container process: exec: "/bin/sh": stat /bin/sh: no such file or directory: unknown
Toplam 36 saniye. Talos container'ı ayakta, çökmüyor. Logu tam olarak beklenen şeyi söylüyor:
[talos] platform information {"component": "early-startup", "mode": "container"}
[talos] service[apid](Waiting): Waiting for service "containerd" to be "up", config to be ready
Talos config bekliyor, CAPD systemd bekliyor. İkisi de beklemeye devam ediyor. Gerçek bir CAPD kurulumunda bu, DevMachine'in 01'deki aynı CGroupsReady condition'ında False olarak kalması demek. Bu sefer sebep ölen bir systemd değil, hiç olmayan bir systemd. Birinci kontrol geçilse bile ikinci ve üçüncüde duracaktı. Yani sorun sırayla düzeltilebilecek tek bir hata değil.
İmajların içine doğrudan bakınca da aynı tablo çıkıyor:
$ make inspect
== kindest/node:v1.34.0
usr/bin/bash
usr/bin/kubeadm
usr/bin/sh
usr/bin/systemctl
usr/local/bin/crictl
== ghcr.io/siderolabs/talos:v1.13.10
(none of them)
Talos imajının dosya sisteminde 587 girdi var. /usr/bin altında containerd, runc, iptables/nftables, LVM ve dosya sistemi araçları var. Shell yok, systemd yok (sadece systemd-udevd var, o da init değil), crictl yok, cloud-init yok.
Nerede patladı: Sidero'nun kendi denemesi
Bunu ilk deneyen ben değilim. Sidero Labs'in kendi control plane provider deposunda 4 Ağustos 2026'da PR #266 açıldı: "feat(ci): migrate e2e integration tests from AWS to Docker (CAPD)". Amaç CACPPT'nin uçtan uca testlerini AWS kimlik bilgisi gerektirmeden, yerel bir CAPD kurulumunda koşturmaktı. PR bir DockerProvider test altyapısı, bir e2e-docker.sh script'i ve Makefile değişiklikleri içeriyordu. İki gün sonra merge edilmeden kapatıldı. Kapatan kişinin yorumu:
Closed due to CAPD that cannot be used only as infrastructure provider with cacppt and cabpt.
Yani Talos'un kendi provider'larının bakımcıları da CAPD'yi CABPT ve CACPPT'nin altında infrastructure provider olarak kullanamadı. Bu yazıdaki üç madde de o cümlenin açılımı. 01'deki provider tablosu "infrastructure satırını değiştirirsen başka hiçbir şey değişmez" izlenimi veriyor. Tersi doğru değil: CAPD'nin "infrastructure" katmanı bootstrap'ın nasıl çalışacağını da varsayıyor. Kubeadm tarzı bir imaj, systemd ve shell bekliyor.
Buradan çıkan genel ders şu: bir provider'ın yerine başkası geçebilir mi diye bakarken sadece CRD'lere bakmak yetmiyor. Bootstrap verisinin makineye hangi yoldan ulaştığına da bakmak gerekiyor. Gerçek infrastructure provider'lar (bulut, KubeVirt, bare-metal) bu veriyi user-data, config-drive ya da nocloud olarak makineye veriyor ve Talos tam olarak bunu okuyor. CAPD ise veriyi shell ile makinenin içine yazıyor.
Peki ne kullanmalı
- Mac'te hızlıca bir Talos cluster'ı:
talosctl cluster create docker. 02'de yaptığımız buydu ve Talos'un kendi Docker provisioner'ı config'iUSERDATAile doğru yoldan veriyor. Cluster API yok. - Cluster API + Talos: makineyi config'i user-data olarak okuyabilen bir infrastructure provider açmalı. Serinin devamında KubeVirt VM'leri üstünde CABPT + CACPPT ile bunu yapacağız (11 → 13 → 15). Orada da tuzaklar var, onları o yazılarda anlatacağım.
- Talos filosu yönetimi: Sidero'nun kendi ürünü Omni. Lisansı ve CAPI provider'larının durumunu 19. yazıda karşılaştırıyorum.
Doğrulama
cd why-capd-cannot-run-talos
make check # kind: 3× OK, Talos: 3× FAIL (yukarıdaki çıktı)
make inspect # kind'da sh/bash/systemctl/crictl/kubeadm var, Talos'ta hiçbiri yok
docker ps -a --filter name=capd-check # boş: script container'ları çıkarken siliyor
Kaynaklar, Eylül 2026 itibarıyla main dalları:
- kubernetes-sigs/cluster-api: test/infrastructure/docker/internal/docker/machine.go (WaitForMultiUserTarget, WaitForCrictlPs, CheckForSentinelFile, GetBootstrapCommands), test/infrastructure/docker/reconcilers/backends/docker/dockermachine_backend.go (getBootstrapData), test/infrastructure/docker/internal/provisioning/cloudinit/adapter.go.
- siderolabs/cluster-api-bootstrap-provider-talos: controllers/secrets.go (writeBootstrapData).
- siderolabs/talos: pkg/provision/providers/docker/node.go (PLATFORM=container, USERDATA).
- siderolabs/cluster-api-control-plane-provider-talos#266, 2026-08-06'da merge edilmeden kapatıldı.
Bu seride
Önceki: Talos Linux'u Çalıştırarak Anlamak: Shell Yok, Tek Makine Konfigürasyonu, gRPC ile Konuşulan İşletim Sistemi · Sonraki: Tek Kutu, Gerçek VM'ler: Yönetim Cluster'ı Olarak k3s, Cilium, KubeVirt ve CDI. CAPD'nin veremediği şeyi, yani config'i user-data ile okuyan gerçek bir makineyi, orada KubeVirt ile kuruyoruz.
Bu yazının kodu: https://github.com/miraccan00/blog-wiki/tree/main/why-capd-cannot-run-talos