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

  1. 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”.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. Crear un usuario/role dedicado en Proxmox

    • Role con VM.Audit, Datastore.AllocateSpace, Pool.Allocate y Node.Config.Read.
    • Token de API para Terraform y clave SSH para ejecutar qm set cuando sea necesario.
  2. Definir una red L2 compartida

    • Usa un bridge en cada nodo PVE y habilita promisc para 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 vip en los control‑plane.
  3. Provisionar VMs con Terraform

    • Recurso proxmox_vm_qemu para cada nodo, especificando la ISO de Talos generada por Image Factory.
    • Después de la creación, ejecuta un null_resource que llama a qm set para añadir discos vacíos (Longhorn o Piraeus) y habilitar la interfaz de red adicional.
  4. Bootstrap de Talos

    • Usa talosctl dentro de un local-exec para aplicar el talosconfig y ejecutar talosctl cluster create.
    • En la configuración de Talos, define clusterEndpoint como la VIP de capa 2 y habilita cilium como CNI con kubeProxyReplacement: strict.
  5. Instalar Cilium con L2 announcements

    • Aplica el manifiesto oficial de Cilium con --set kubeProxyReplacement=strict.
    • Habilita loadBalancer en modo l2 para que los servicios tipo LoadBalancer se anuncien mediante ARP en la LAN.
  6. 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.
  7. Almacenamiento persistente

    • En workers, crea un segundo disco con qm set y monta como Longhorn o Piraeus.
    • Configura los StorageClass correspondientes para que los PVC usen el disco adicional.
  8. Actualizaciones y rolling upgrades

    • Define un null_resource que ejecuta talosctl upgrade con health‑checks entre nodos.
    • Terraform controla la versión deseada en la variable talos_version; cualquier cambio dispara el proceso de upgrade.

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

  1. Comprobar la VIP

    ping -c 3 192.168.10.10
    

    La respuesta debe alternar entre los nodos de control plane.

  2. Validar el API de Kubernetes

    kubectl get nodes
    

    Todos los nodos deben aparecer en Ready.

  3. Probar un LoadBalancer

    kubectl apply -f https://k8s.io/examples/service/load-balancer-example.yaml
    kubectl get svc
    

    La IP externa debe ser una dirección en la LAN anunciada por Cilium.

  4. 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 3600
    

    El pod debe montar el PVC sin errores.

Notas adicionales

  • La creación de discos vacíos mediante qm set es 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 l2 depende de que los switches de la LAN soporten ARP proxy; en redes con VLAN aisladas puede ser necesario habilitar proxy_arp en el bridge.
  • Cuando actualices Talos, revisa que la versión de Cilium sea compatible; de lo contrario el kubeProxyReplacement puede 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.