Aggiungere nodi a uno Spazio
Uno Spazio gira su macchine che sono tue. Aggiungerne una sono tre passi: prendi un ticket, prepara la macchina, esegui il join. Ci vogliono pochi minuti e funziona uguale che la scatola sia una VM cloud o un server nel tuo rack.
Cosa serve a una macchina
- Linux a 64 bit, con systemd. Qualsiasi distribuzione moderna.
- Almeno 2 GB di RAM, 4 GB per qualcosa di serio. Due core sono un minimo sensato.
- Nessun pacchetto Kubernetes. Lo script di join installa containerd e un kubelet che corrisponde alla versione del tuo control plane. Una macchina che porta già stato Kubernetes va prima resettata.
- Accesso in uscita al control plane del tuo Spazio su TCP 6443 e 8088, più DNS, NTP e HTTPS per il pull delle immagini. Non serve che nulla sia raggiungibile da internet: il nodo chiama fuori e tiene la connessione aperta.
- Swap disattivato, o un kubelet configurato per tollerarlo.
- Un nome stabile. Il nome scelto viene fissato nel ticket di join e non si può cambiare dopo senza rifare il join.
Il tuo nodo non ha bisogno di un indirizzo pubblico raggiungibile
kubehz non si connette mai al tuo nodo. Tutto (anche kubectl logs e kubectl exec) torna indietro attraverso il tunnel che il tuo nodo ha aperto. Per questo la porta di kubelet resta chiusa nel firewall qui sotto.
1. Crea un ticket di join
In dashboard apri il tuo Spazio e scegli Aggiungi nodo. Dai alla macchina il nome che porterà nel cluster e ottieni un ticket.
Da CLI:
lo kubehz join worker-1La CLI crea il ticket per lo Spazio che appartiene alla directory del cluster attivo. Le servono quattro cose: un cluster.lok8s.yaml con kind: Kubehz e spec.kubehz.hosting: shared, un lo provision completato per quel cluster (è lui a creare lo Spazio) e KUBEHZ_TOKEN esportato nella shell (un token API generato in dashboard sotto Accesso → Token API). Senza, usa la dashboard.
Tre cose sul ticket, tutte volute:
- Viene mostrato una volta sola. Nulla lo conserva: né il nostro database né la dashboard. Se lo perdi, creane un altro; un “rimostramelo” non esiste.
- Vale per un nodo, una volta, ed è legato al nome esatto che hai scelto. Un ticket per
worker-1non può registrareworker-2. - Scade in circa dieci minuti. Crealo quando sei alla macchina, non la sera prima.
2. Prepara la macchina
Il firewall del nodo
Il perimetro del nodo è un servizio per te, non un controllo su di te: hai root e puoi cambiarlo o rimuoverlo. I controlli veri della piattaforma stanno lato cluster. Questa ricetta tiene piccola la superficie d’attacco della macchina e lascia intatto il tessuto di rete del cluster.
Salva come /etc/nftables.d/kubehz-node.nft e applica con nft -f /etc/nftables.d/kubehz-node.nft. Rieseguirlo sostituisce la tabella precedente, quindi è sicuro ripeterlo, e sicuro anche via SSH, perché le connessioni stabilite vengono accettate per prime.
table inet kubehz_node {}
flush table inet kubehz_node
table inet kubehz_node {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
# Cluster fabric interfaces. Traffic on cilium_wg0 has already proved
# itself to WireGuard; dropping post-decryption flows here is how you
# break kubectl logs/exec without ever seeing why.
iifname "cilium_wg0" accept
iifname "cilium_vxlan" accept
iifname "cilium_host" accept
iifname "cilium_net" accept
iifname "lxc*" accept
# ICMP carries path-MTU discovery. Pod traffic is double-encapsulated, so
# the usable MTU is smaller than the wire's — block these and you get
# silent stalls instead of errors.
icmp type {
echo-request, echo-reply, destination-unreachable,
time-exceeded, parameter-problem
} accept
icmpv6 type {
echo-request, echo-reply, destination-unreachable,
time-exceeded, parameter-problem, packet-too-big,
nd-neighbor-solicit, nd-neighbor-advert,
nd-router-solicit, nd-router-advert
} accept
# WireGuard: every node-to-node path rides this. The peers are other
# machines in the cluster with arbitrary addresses, so the source cannot
# be pinned — WireGuard's own keys are the authentication.
udp dport 51871 accept
# VXLAN (pod traffic, normally inside WireGuard).
udp dport 8472 accept
# Your SSH. Pin it to your own addresses if you can:
# tcp dport 22 ip saddr { 203.0.113.0/24 } accept
tcp dport 22 accept
# NodePorts: closed. Uncomment only if you deliberately want to expose
# them on this machine's public address.
# tcp dport 30000-32767 accept
# udp dport 30000-32767 accept
counter
}
# Cilium's datapath owns pod forwarding — the perimeter must not fight the
# CNI. Pod-level rules belong in your NetworkPolicies.
chain forward { type filter hook forward priority filter; policy accept; }
chain output { type filter hook output priority filter; policy accept; }
}Due porte sono deliberatamente non aperte:
- 10250 (kubelet).
kubectl logsedexecarrivano dal tunnel che il tuo nodo ha aperto, quindi l’API di kubelet non deve mai affacciarsi sulla rete. Aprirla aggiunge rischio e non porta nulla. - NodePort. Commentate sopra; abilitale solo se lo intendi davvero.
Se la macchina sta dietro un firewall cloud (Hetzner Cloud Firewalls, un security group, un’appliance), replica lì la stessa forma. Due livelli che concordano vanno bene; un firewall cloud che è in silenzioso disaccordo con l’host è il motivo per cui un nodo entra e un’ora dopo è irraggiungibile.
Percorso: una VM Hetzner Cloud
Niente di speciale. Una cx22 o più grande con un’immagine standard; lo script di join porta con sé containerd e kubelet. Se colleghi un Hetzner Cloud Firewall, consenti in ingresso UDP 51871 e 8472 e il tuo SSH; in uscita può restare aperto.
Percorso: bare metal, o una macchina nel tuo rack
Anche qui niente di speciale, con due cose da verificare:
- Il NAT va bene, un NAT simmetrico non sempre. Il nodo chiama fuori, quindi un normale NAT di casa o ufficio funziona. WireGuard tra il tuo nodo e gli altri ha bisogno di UDP 51871 attraverso il NAT in entrambe le direzioni. La maggior parte ce la fa, certi carrier-grade NAT no.
- MTU. Se il tuo collegamento è già ridotto (PPPoE, un tunnel), il traffico dei pod viene incapsulato due volte sopra a quello. Lascia ICMP aperto (la ricetta sopra lo fa) e il path MTU discovery sistema il resto.
3. Esegui il join
Il ticket arriva con uno script. La dashboard lo mostra subito sotto il ticket, come campo da copiare; lo kubehz join lo scrive in $TMPDIR/kubehz-join-<node>.sh (leggibile solo dal proprietario) e stampa la riga scp/ssh; l’API lo restituisce nel campo script del ticket (POST /api/spaces/{id}/join-token). Leggilo, poi eseguilo come root sulla macchina; contiene il ticket, quindi cancellalo una volta che il nodo è entrato. Installa containerd e un kubelet che corrisponde alla versione del tuo control plane, legge la CA del cluster dal control plane stesso e la verifica contro il tuo ticket (una macchina può entrare solo nel control plane che ha creato il suo ticket, quindi non c’è alcun file CA da scaricare né nulla da confrontare a mano), scrive una configurazione di bootstrap puntata al control plane del tuo Spazio (join.endpoint sullo Spazio nell’API) e avvia kubelet. Su una macchina con systemd-resolved, lo script punta kubelet a /run/systemd/resolve/resolv.conf, così i pod ricevono i veri resolver upstream invece dello stub 127.0.0.53. Il nodo poi:
- presenta il ticket e ottiene il proprio certificato client,
- si registra con il nome fissato dal ticket,
- riceve etichetta e taint per il tuo tenant: è questo che tiene i pod degli altri clienti fuori dalla tua macchina, ed è applicato dalla piattaforma, non dal nodo,
- riceve il CNI e diventa
Ready.
Il primo join richiede qualche minuto, in gran parte per il pull delle immagini.
Dopo
- Il nodo compare con il suo stato nella pagina del tuo Spazio.
kubectl get nodescon la tua kubeconfig mostra i tuoi nodi. - Rimuovere un nodo è Rimuovi registrazione in dashboard. Ne ritira le credenziali e lo toglie dal cluster; i tuoi carichi si ripianificano sui nodi rimanenti. La macchina continua a girare: spegnerla, e fermare quello che ti costa, tocca a te.
- Rientrare con lo stesso nome funziona: rimuovi la registrazione, ripara la macchina, crea un ticket nuovo per lo stesso nome e rifai il join.
Quando un nodo non entra
| Sintomo | Causa abituale |
|---|---|
| Il comando di join dice che il ticket non è valido | È scaduto (dieci minuti), era già usato o è stato creato per un altro nome di nodo. Creane uno nuovo. |
| kubelet riprova e non si registra mai | 6443 in uscita bloccata, o il nome del control plane non si risolve sulla macchina. Verifica con curl -v dal nodo. |
| I pod non risolvono i nomi, il nodo sì | kubelet legge un resolver stub. Lo script gestisce systemd-resolved; con un altro resolver locale, punta resolvConf nella configurazione di kubelet a un file con veri server upstream. |
Il nodo si registra ma resta NotReady | Il CNI sta ancora partendo, o le porte del tessuto (UDP 51871/8472) sono bloccate in ingresso. Controlla il firewall su entrambi i livelli. |
kubectl logs/exec vanno in timeout, il resto funziona | Il tunnel è su ma il traffico di ritorno è filtrato, di solito una regola che scarta l’interfaccia cilium_wg0. |
I pod restano Pending su un nodo Ready | Il pod non tollera il taint del tuo tenant. Fai il deploy nel namespace del tuo Spazio, dove le tolerance sono impostate per te, invece che in un namespace creato in altro modo. |
Stato del documento
| Aspetto | Dettaglio |
|---|---|
| Stato | live: flusso di join come rilasciato (ticket → preparazione → join); ricetta firewall validata con nft |
| Ultima revisione | 2026-09-05 |