Problema
Implementar un clúster Kubernetes totalmente declarativo en un entorno on‑premise suele chocar con dos limitaciones típicas de los hipervisores tradicionales: la ausencia de un balanceador de carga gestionado y la falta de volúmenes de bloque “cloud‑ready”. Cuando se intenta replicar la experiencia de los módulos de Terraform diseñados para proveedores de nube (por ejemplo, Hetzner o AWS), la infraestructura subyacente de Proxmox no ofrece directamente esas piezas. El resultado es que, aunque la creación de VMs y la instalación de Talos pueden automatizarse, la exposición del API de Kubernetes y la persistencia de datos requieren soluciones manuales o hacks que rompen la promesa de “apply‑only”.
Causa
-
Sin Load Balancer nativo – Los proveedores de nube entregan un L4/L7 LB que recibe el tráfico del API y de los servicios tipo LoadBalancer. Proxmox solo expone redes L2/L3; no hay un recurso equivalente a un “cloud‑lb”.
-
Dirección IP del API flotante – En la nube, el endpoint del API suele ser un DNS que apunta a un LB. En Proxmox hay que crear una VIP que flote entre los nodos de control plane, normalmente usando ARP o gratuitous‑ARP.
-
Almacenamiento persistente – Los discos de bloque de la nube se crean con una sola llamada API. En Proxmox, crear un disco vacío requiere una combinación de API + CLI (
qm set), y la gestión de volúmenes para Longhorn o Piraeus añade complejidad. -
Credenciales de gestión – Los módulos de Terraform para la nube usan tokens con permisos limitados. En Proxmox, la API no permite crear discos vacíos sin acceso SSH al host, lo que obliga a generar un usuario/role con permisos de “read‑only” más un acceso SSH para la fase de provisioning.
-
Integración con el Cloud‑Controller‑Manager (CCM) – El CCM de Proxmox es menos maduro que los de los proveedores de nube, por lo que la reconciliación de nodos y la eliminación automática de objetos Node cuando una VM desaparece no ocurre de forma implícita.
Solución
Una estrategia reutilizable consiste en combinar Terraform (para la infraestructura), Talos (para el SO immutable) y Cilium (para la capa de red y los LoadBalancer L2). Los pasos clave son:
-
Crear un usuario/role dedicado en Proxmox
- Role con
VM.Audit,Datastore.AllocateSpace,Pool.AllocateyNode.Config.Read. - Token de API para Terraform y clave SSH para ejecutar
qm setcuando sea necesario.
- Role con
-
Definir una red L2 compartida
- Usa un
bridgeen cada nodo PVE y habilitapromiscpara que los paquetes ARP de la VIP sean aceptados. - Configura una dirección IP virtual (por ejemplo, 192.168.10.10/24) que Talos asignará como
vipen los control‑plane.
- Usa un
-
Provisionar VMs con Terraform
- Recurso
proxmox_vm_qemupara cada nodo, especificando la ISO de Talos generada por Image Factory. - Después de la creación, ejecuta un
null_resourceque llama aqm setpara añadir discos vacíos (Longhorn o Piraeus) y habilitar la interfaz de red adicional.
- Recurso
-
Bootstrap de Talos
- Usa
talosctldentro de unlocal-execpara aplicar eltalosconfigy ejecutartalosctl cluster create. - En la configuración de Talos, define
clusterEndpointcomo la VIP de capa 2 y habilitaciliumcomo CNI conkubeProxyReplacement: strict.
- Usa
-
Instalar Cilium con L2 announcements
- Aplica el manifiesto oficial de Cilium con
--set kubeProxyReplacement=strict. - Habilita
loadBalanceren modol2para que los servicios tipo LoadBalancer se anuncien mediante ARP en la LAN.
- Aplica el manifiesto oficial de Cilium con
-
Gestión de nodos mediante CCM
- Despliega el Proxmox CCM (
sergelogvinov/proxmox-ccm) como un Deployment. - El CCM detecta cambios en la API de Proxmox y actualiza los objetos Node; cuando una VM se elimina, el Node correspondiente se borra automáticamente.
- Despliega el Proxmox CCM (
-
Almacenamiento persistente
- En workers, crea un segundo disco con
qm sety monta comoLonghornoPiraeus. - Configura los
StorageClasscorrespondientes para que los PVC usen el disco adicional.
- En workers, crea un segundo disco con
-
Actualizaciones y rolling upgrades
- Define un
null_resourceque ejecutatalosctl upgradecon health‑checks entre nodos. - Terraform controla la versión deseada en la variable
talos_version; cualquier cambio dispara el proceso de upgrade.
- Define un
Cuándo aplicar esta solución
- Entornos homelab o edge donde Proxmox es el hipervisor dominante y se necesita una instalación declarativa de Kubernetes sin depender de herramientas externas como Ansible.
- Requerimientos de alta disponibilidad en la capa de control plane, pero sin acceso a un LB de nube.
- Necesidad de almacenamiento local gestionado por Longhorn o Piraeus en lugar de volúmenes de nube.
No es adecuada cuando:
- Se necesita autoscaling automático basado en métricas de Kubernetes, ya que el autoscaler oficial no soporta Proxmox.
- El entorno requiere firewall distribuido gestionado por el CNI; en este caso se debe usar la firewall del host o una solución externa.
Código
# 1. Crear usuario y token en Proxmox (ejecutar en el host PVE)
pveum useradd terraform@pve -password 'StrongPass123!'
pveum roleadd TerraformReadOnly -privs "VM.Audit Datastore.AllocateSpace Pool.Allocate Node.Config.Read"
pveum aclmod / -user terraform@pve -role TerraformReadOnly
pveum token add terraform@pve token=terraform-token -privsep 0
# 2. Variables de Terraform (terraform.tfvars)
proxmox_endpoint = "https://pve.example.com:8006/api2/json"
proxmox_user = "terraform@pve!terraform-token"
proxmox_ssh_user = "root"
proxmox_ssh_key = "~/.ssh/id_rsa"
# 3. Recurso VM con disco extra (main.tf)
resource "proxmox_vm_qemu" "worker" {
name = "k8s-worker-${count.index}"
target_node = var.pve_node
iso = var.talos_iso
cores = 4
memory = 8192
network {
model = "virtio"
bridge = "vmbr0"
}
# disco del sistema
disk {
size = "30G"
type = "scsi"
storage = "local-lvm"
}
# disco para storage persistente
provisioner "local-exec" {
command = <<EOT
ssh -i ${var.proxmox_ssh_key} ${var.proxmox_ssh_user}@${var.pve_node} \
"qm set ${self.id} -scsi2 local-lvm:10"
EOT
}
}
# 4. Bootstrap Talos (bootstrap.tf)
resource "null_resource" "talos_bootstrap" {
depends_on = [proxmox_vm_qemu.worker]
provisioner "local-exec" {
command = "talosctl apply-config --insecure --nodes ${proxmox_vm_qemu.worker.*.ipv4_address} -f talosconfig.yaml"
}
}
Verificación
-
Comprobar la VIP
ping -c 3 192.168.10.10La respuesta debe alternar entre los nodos de control plane.
-
Validar el API de Kubernetes
kubectl get nodesTodos los nodos deben aparecer en
Ready. -
Probar un LoadBalancer
kubectl apply -f https://k8s.io/examples/service/load-balancer-example.yaml kubectl get svcLa IP externa debe ser una dirección en la LAN anunciada por Cilium.
-
Verificar el storage
kubectl get sc kubectl run test-pvc --image=busybox --restart=Never --overrides='{"spec":{"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"test-pvc"}}]}}' --command -- sleep 3600El pod debe montar el PVC sin errores.
Notas adicionales
- La creación de discos vacíos mediante
qm setes la única forma de evitar que el API de Proxmox devuelva errores de “disk not found”. Mantén la clave SSH en un vault para no exponerla en el código. - Cilium en modo
l2depende de que los switches de la LAN soporten ARP proxy; en redes con VLAN aisladas puede ser necesario habilitarproxy_arpen el bridge. - Cuando actualices Talos, revisa que la versión de Cilium sea compatible; de lo contrario el
kubeProxyReplacementpuede romper la conectividad de los servicios. - El CCM de Proxmox todavía está en fase beta; monitoriza los logs del Deployment para detectar eventos de “node not found” y corrígelos manualmente si aparecen.