En el Episode 1 conté la base del homelab: Beelink + Proxmox VE, DNS local y reverse proxy. Este episodio es el siguiente paso: un cluster de Kubernetes de verdad, corriendo sobre ese mismo Proxmox, con Talos Linux.
¿Por qué Talos y no un Ubuntu + kubeadm?
La mayoría instala Kubernetes sobre Ubuntu + kubeadm y termina administrando un SO: actualizaciones, paquetes, SSH, drift. Talos da vuelta la idea: el nodo es la API. Sin shell, sin package manager, config declarativa, todo por talosctl.
Para un homelab eso significa:
- Nada de drift: no hay forma de “entrar a arreglar algo a mano” y olvidarte de qué tocaste
- Configuración declarativa: el nodo entero es un YAML que podés versionar
- Superficie de ataque chica: sin SSH ni binarios de más
- Upgrades atómicos: la imagen se reemplaza entera, con rollback
El costo es cambiar el chip: cuando algo falla, no te conectás por SSH — le preguntás a la API.
Y lo corro sobre Proxmox porque es donde ya vive el lab.
El plan
Tres VMs: un control plane y dos workers. Los workers llevan un segundo disco dedicado a Longhorn (storage replicado, episodio siguiente); el control plane solo el disco de sistema.
| VMID | Nombre | Rol | vCPU | RAM | Discos | IP |
|---|---|---|---|---|---|---|
| 210 | talos-cp-01 | control plane (etcd) | 2 | 4 GB | 30 GB sistema | 192.168.68.83 |
| 211 | talos-w-01 | worker | 4 | 6 GB | 30 GB sistema + 100 GB Longhorn | 192.168.68.82 |
| 212 | talos-w-02 | worker | 4 | 6 GB | 30 GB sistema + 100 GB Longhorn | 192.168.68.84 |
Las IPs son fijas y viven fuera del rango del DHCP del router: nadie más puede tomarlas, y los YAML del cluster no dependen de un lease que puede cambiar.
0. Variables
Todo lo que sigue se corre desde la raíz del repo, salvo donde diga “en el host Proxmox”:
export CLUSTER_NAME=talos-proxmox-cluster
export CONTROL_PLANE_IP=192.168.68.83
export WORKER1_IP=192.168.68.82
export WORKER2_IP=192.168.68.84
export TALOSCONFIG="$PWD/_out/talosconfig"
1. Crear las VMs
En el host Proxmox. En vez de repetir tres veces el mismo qm create gigante, una función de bash — con un parámetro opcional de disco de datos, que solo usan los workers:
STORAGE=local-lvm
ISO=local:iso/talos-v1.14.1-ext-metal-amd64.iso
BRIDGE=vmbr0
# data_disk (GB) es opcional: si se pasa, agrega scsi1 para Longhorn
create_talos_vm() {
local vmid=$1 name=$2 cores=$3 mem=$4 disk=$5 data_disk=${6:-}
qm create "$vmid" \
--name "$name" \
--ostype l26 \
--machine q35 \
--bios ovmf \
--efidisk0 ${STORAGE}:1,efitype=4m,pre-enrolled-keys=0 \
--cpu host \
--cores "$cores" \
--memory "$mem" \
--balloon 0 \
--scsihw virtio-scsi-pci \
--scsi0 ${STORAGE}:${disk},cache=writethrough,discard=on,ssd=1 \
--ide2 ${ISO},media=cdrom \
--boot order='scsi0;ide2' \
--net0 virtio,bridge=${BRIDGE} \
--serial0 socket
if [ -n "$data_disk" ]; then
qm set "$vmid" --scsi1 ${STORAGE}:${data_disk},discard=on,ssd=1,backup=0
fi
}
# vmid name cores mem disco longhorn
create_talos_vm 210 talos-cp-01 2 4096 30
create_talos_vm 211 talos-w-01 4 6144 30 100
create_talos_vm 212 talos-w-02 4 6144 30 100
Decisiones que no son obvias:
scsi0es el disco del sistema (Talos se instala en/dev/sda); el de Longhorn aparece como/dev/sdb.backup=0enscsi1: los datos ya van a estar replicados por Longhorn — no tiene sentido que Proxmox también los backupee.cpu hostyballoon 0: sin sorpresas de CPU emulada ni de memoria “prestada”. Kubernetes y el ballooning se llevan mal; preferís RAM fija y predecible.- La ISO es la de
factory.talos.devcon extensiones: los nodos ya traen lo necesario para Longhorn y para bare metal sin instalar nada. Ese hash largo en la URL de Image Factory es el schematic ID — identifica tu combinación de extensiones, así que el mío puede ser distinto al tuyo. --boot order='scsi0;ide2': disco primero, ISO después. Una vez instalado, la VM arranca sola sin que tengas que sacar el CD a mano.--serial0 socket: consola serie, tu única ventana al nodo cuando algo se rompe antes de que levante la API.
Gotcha: usar
virtio-scsi-pci(VirtIO SCSI). Convirtio-scsi-singleel bootstrap de Talos se cuelga. Este no te lo dice la doc oficial; te lo dice una tarde perdida.
2. Verificar los discos
Arrancás las VMs (qm start 210 211 212) y bootean la ISO en modo maintenance: sin nada instalado en disco, esperando configuración. Antes de configurar nada, un chequeo de que los nodos responden y de cómo ven sus discos (con los workers en maintenance mode):
talosctl get disks --insecure --nodes $WORKER1_IP
# debe listar sda (~32 GB) y sdb (~107 GB)
El flag --insecure es necesario acá: el nodo todavía está en maintenance mode y no hay certificados mutuos. En cuanto apliques config, se acabó el --insecure.
3. Generar la configuración
Los workers llevan un patch que monta el disco que no es de sistema en /var/mnt/longhorn, listo para el episodio siguiente (talos/longhorn-user-disk.yaml):
apiVersion: v1alpha1
kind: UserVolumeConfig
name: longhorn
provisioning:
diskSelector:
match: "!system_disk"
minSize: 50GB
grow: true
Fijate lo elegante del asunto: en Talos el “disco extra” se declara en la config del nodo — no hay mkfs, no hay fstab a mano. El diskSelector agarra el disco que no es de sistema, sea como se llame (sdb, vdb, lo que sea).
Con el patch, generás la config de todos los nodos de una:
talosctl gen config $CLUSTER_NAME https://$CONTROL_PLANE_IP:6443 \
--output-dir _out \
--install-image factory.talos.dev/metal-installer/88d1f7a5c4f1d3aba7df787c448c1d3d008ed29cfb34af53fa0df4336a56040b:v1.14.1 \
--config-patch-worker @talos/longhorn-user-disk.yaml
El --install-image apunta al mismo schematic de la ISO, así el installer y lo que bootea son exactamente la misma imagen. Esto genera tres archivos en _out/: controlplane.yaml, worker.yaml y talosconfig (tus credenciales de cliente).
_out/— talosconfig, kubeconfig, config de nodos — contiene secretos y está en.gitignore. Son las llaves del cluster: si lo perdés, perdés el acceso administrativo; si lo filtrás, peor.
4. Aplicar y bootstrapear
talosctl apply-config --insecure --nodes $CONTROL_PLANE_IP --file _out/controlplane.yaml
for ip in $WORKER1_IP $WORKER2_IP; do
talosctl apply-config --insecure --nodes $ip --file _out/worker.yaml
done
Acá Talos se instala en el disco y reinicia. A partir de este momento el nodo ya no está en maintenance mode, así que --insecure deja de ser necesario (y deja de funcionar).
Cuando el control plane vuelve a levantar, hay que inicializar etcd:
talosctl config endpoint $CONTROL_PLANE_IP
talosctl config node $CONTROL_PLANE_IP
talosctl bootstrap
talosctl kubeconfig -e $CONTROL_PLANE_IP
bootstrapcorre exactamente una vez en la vida del cluster, contra un solo nodo del control plane. Si lo corrés de nuevo o contra un segundo nodo, rompés el quorum de etcd. Es el error más caro y el más fácil de cometer.
5. Verificar el cluster
kubectl get nodes -o wide -w
El -w (watch) deja el comando abierto mostrando los cambios en vivo, así ves a los nodos pasar de NotReady a Ready sin hacer F5 con la mano.
Y en los workers, el volumen de Longhorn ya tiene que estar montado:
talosctl -n $WORKER1_IP,$WORKER2_IP get mountstatus | grep longhorn
Si CoreDNS queda raro
Los pods de CoreDNS a veces arrancan antes de que la red del cluster esté del todo lista y quedan en un estado medio zombie. Un redeploy gradual los arregla:
kubectl -n kube-system rollout restart deployment coredns
Lo que me llevo de este episodio
- No hay SSH, y está bien. Cambia el reflejo de “entro y toco”, pero a cambio el nodo siempre es lo que dice el YAML.
virtio-scsi-singlecuelga el bootstrap.virtio-scsi-pciy a otra cosa.- El disco de Longhorn se declara, no se formatea.
UserVolumeConfigcon!system_disk: el patch vale para cualquier nodo con cualquier nombre de disco. --insecurees sólo para maintenance mode. En cuanto aplicás config, se acabó.bootstrapuna sola vez. Repetirlo rompe el quorum de etcd.- Cuidá
_out/. Son las llaves del cluster.
Próximo episodio
Tenés un cluster de 3 nodos al que nadie puede hacer SSH: todo cambio pasa por la API de Talos. Pero un cluster sin storage replicado es un playground — en el Episode 3 le doy a cada worker su storage que sobrevive a que se caiga un nodo completo.
Mi cluster tiene tres nodos, cero SSH y un dueño que igual va a romper algo.
Pero esta vez, en YAML. 😎