Come (non) costruire un cluster Kubernetes HA su Proxmox¶
Volevo un cluster Kubernetes in alta disponibilità nel mio homelab. Tre nodi, resiliente, qualcosa su cui fare pratica seria, anche perché al lavoro maneggio OpenShift/OCP tutti i giorni, e avere un ambiente simile a casa mi sembrava un'ottima idea.
Quello che non mi aspettavo era di passare un'intera giornata a scoprire, uno strato alla volta, tutti i modi in cui un cluster Kubernetes può fallire silenziosamente.
Questo è il racconto e la guida tecnica che ne è uscita.
Il punto di partenza¶
Server OVH con Proxmox, 32 vCPU, 128GB di RAM. Già pieno di VM e container LXC: InfluxDB, PostgreSQL, MariaDB, Immich, Grafana, un gateway, e altro ancora. Risorse ce n'erano, ma il carico era tutt'altro che vuoto.
La prima domanda è stata: quale distribuzione Kubernetes? Ho valutato OKD (per pratica 1:1 con OpenShift), Rancher+RKE2, K3s, persino KubeSolo. Alla fine la scelta è caduta su RKE2 — proprio perché è l'upstream di OpenShift, e volevo che la pratica fosse trasferibile al lavoro.
Tre VM, tre nodi server, tutti control-plane con etcd embedded. Sulla carta, tutto ragionevole.
Il crollo¶
Le prime ore sono filate lisce: VM create via cloud-init, RKE2 installato, cluster formato, tutti e tre i nodi Ready. Poi ho provato a installare una dashboard (Headlamp) via Helm, e da lì è iniziata una discesa che definire frustrante è un eufemismo.
Primo problema: i pod di rete (Calico) andavano in crash loop con un errore criptico: Fatal glibc error: CPU does not support x86-64-v2. La causa: il tipo di CPU virtuale di default su Proxmox (kvm64) non espone alcune istruzioni moderne che i binari Calico richiedono. Fix: qm set <vmid> --cpu host su tutti i nodi, con stop/start completo (un riavvio interno non basta, la CPU viene negoziata solo alla creazione del processo QEMU).
Secondo problema: risolto quello, kube-proxy risultava silenziosamente cieco, nessuna regola di rete scritta, nessun errore visibile finché non si scavava nei log, dove saltava fuori un ClusterRole RBAC incompleto (mancavano i permessi su services ed endpointslices). Probabile relitto di un bootstrap interrotto durante le ore di instabilità iniziale.
Terzo problema, il più insidioso: il meccanismo interno con cui RKE2 gestisce i componenti di sistema via Helm (l'"Addon controller") si è letteralmente impantanato. Job Helm in crash loop, revisioni disallineate, un controller che smetteva di reagire ai cambiamenti nei manifest. Ore passate a cancellare, ricreare, forzare redeploy con il Service di Traefik che spariva e ricompariva a ogni tentativo fallito.
Alla fine, Headlamp ha funzionato. Ma il giorno dopo, al primo riavvio dei nodi, tutto è tornato daccapo instabile.
La causa vera¶
Scavando nei log di sistema delle VM, la riga che spiegava tutto:
clocksource: Long readout interval, skipping watchdog check: cs_nsec: 2460203349 wd_nsec: 2460202273
Tradotto: la VM era rimasta "congelata", non schedulata dall'hypervisor, per oltre 2.4 secondi in un colpo solo. Non era un problema di rete o di configurazione era contesa IO sul disco condiviso tra tutte le VM/container del mio Proxmox.
E qui il punto cruciale: etcd è patologicamente sensibile ai timing. Usa heartbeat ed election timeout nell'ordine di centinaia di millisecondi. Se una VM si blocca per 2+ secondi, per etcd il nodo sembra morto, e parte una cascata di rielezioni, timeout, instabilità esattamente i sintomi che stavo rincorrendo da ore, pensando fossero problemi separati.
Non era RKE2 il problema. Era la scelta di avere tre copie di etcd, ognuna sensibilissima ai millisecondi, su un host con storage condiviso e carichi misti.
Il cambio di strategia¶
A questo punto la domanda giusta non era più "quale distribuzione", ma "serve davvero etcd?". E la risposta, sorprendentemente, è no — non necessariamente.
Sia K3s che RKE2 supportano un datastore esterno al posto di etcd embedded, tramite un progetto chiamato kine, che traduce le operazioni verso un database relazionale qualsiasi — MySQL, PostgreSQL, persino SQLite in locale. Avevo già MariaDB nel mio homelab. Perché non usarlo?
Un database relazionale ha un pattern di scrittura completamente diverso da etcd: meno sensibile ai micro-stall, transazioni più tolleranti, nessun quorum Raft da sincronizzare in tempo reale tra tre copie. Esattamente il tipo di robustezza che mi serviva contro un host con IO ballerino.
Ho scelto K3s invece di tornare su RKE2 — più leggero, meno "machinery" interna che può incepparsi (niente Addon controller da inseguire), e con lo stesso identico supporto per il datastore esterno.
La rinascita¶
Tre VM pulite, stesso template Debian 13 ma con cpu: host impostato fin dall'inizio sul template (non più da correggere dopo). Un database dedicato su MariaDB. Tre comandi di installazione K3s, uno per nodo, con K3S_DATASTORE_ENDPOINT puntato al database invece che a etcd embedded.
curl -sfL https://get.k3s.io | sudo -E env \
K3S_TOKEN='...' \
K3S_DATASTORE_ENDPOINT='mysql://rke2:password@tcp(192.168.1.108:3306)/rke2' \
sh -s - server --tls-san 192.168.1.10 --tls-san 192.168.1.11 --tls-san 192.168.1.12 \
--node-ip 192.168.1.10
Tre nodi Ready in meno di sette minuti. Contro le dodici e più ore della mattina.
Cosa c'è ora¶
Il cluster finale:
- 3 nodi K3s, tutti control-plane, HA vera, datastore su MariaDB esterno
- Headlamp come dashboard principale, esposta su un sottodominio dedicato tramite Traefik + Ingress
- La dashboard nativa di Traefik stessa, esposta separatamente con basic auth per debug rapido di router e servizi
- NGINX davanti a tutto, che fa da terminazione TLS e bilancia il traffico sui tre nodi tramite le NodePort di Traefik
Stabile al riavvio. Cosa che, fino a ieri, non davo per scontata.
La topologia¶
Per chiarezza, ecco come è fatto il cluster oggi — dal traffico in ingresso fino al database esterno che ha sostituito etcd:
Il punto chiave, visibile nel diagramma: nessuna freccia di consenso tra i nodi stessi. A differenza di un cluster con etcd embedded — dove i tre nodi devono negoziare continuamente il quorum tra loro, sensibili al minimo ritardo di rete o disco reciproco, qui ogni nodo scrive e legge indipendentemente lo stato del cluster dal database esterno. È MariaDB a fare da unica fonte di verità condivisa, con un pattern di accesso molto più tollerante ai piccoli intoppi che il mio host, con i suoi carichi misti, inevitabilmente ogni tanto produce.
Cosa mi porto a casa¶
Qualche lezione, in ordine di importanza crescente:
Il problema non è mai dove sembra. Ho passato ore a inseguire sintomi diversi — crash di Calico, RBAC rotto, Helm impantanato — pensando fossero guasti indipendenti. Erano tutti conseguenze a valle della stessa causa radice: un host con storage sotto pressione, e un componente (etcd) patologicamente intollerante a quella pressione.
"Pesante" e "instabile" non sono sinonimi. Ero pronto a buttare RKE2 pensando fosse "troppo esoso" , ma il vero colpevole era una scelta architetturale (etcd embedded su storage condiviso), non la distribuzione in sé. RKE2 supporta lo stesso identico datastore esterno di K3s. Il problema non era il vestito, era la taglia sbagliata.
Se il tuo host ha carichi misti, pensa due volte prima di mettere etcd sopra. Se hai un Proxmox (o qualsiasi hypervisor) condiviso con altri workload non un server dedicato solo al cluster. Il pattern "3 nodi con etcd embedded" è una scommessa contro la sensibilità di quel componente al jitter IO. Un datastore esterno relazionale è un'assicurazione a basso costo contro un problema che, se si manifesta, è tra i più difficili da diagnosticare che abbia visto.
Alla fine ho un cluster che funziona, e un pezzo di conoscenza su RBAC, Helm internals, networking Kubernetes ed etcd che con un'installazione liscia non avrei mai toccato con mano. Non è stato il pomeriggio che avevo pianificato. Ma è stato, alla fine, un buon pomeriggio.