Problema

Los ingenieros que están empezando a buscar su primer puesto de DevOps suelen necesitar un entorno que combine varias capas: virtualización, orquestación de contenedores, gestión de configuración y observabilidad. El desafío es armar una infraestructura que sea lo suficientemente “enterprise” para el CV, pero que siga siendo manejable en un hardware doméstico (por ejemplo, un PC gaming usado). Además, la solución debe ofrecer almacenamiento fiable para varios terabytes de medios familiares y permitir pruebas de pipelines CI/CD, IaC y backup sin depender de la nube pública.

En la práctica, el patrón recurrente es: un solo nodo físico que ejecuta un hipervisor, varios VMs y clústers de Kubernetes, todo conectado a un pool ZFS y expuesto mediante un ingress seguro. Cuando la arquitectura no está bien alineada, aparecen cuellos de botella en la red, pérdida de datos por configuraciones ZFS incorrectas y fallos de despliegue por falta de sincronización entre Terraform y Ansible.

Causa

  1. Separación insuficiente entre capa de infraestructura y capa de aplicación
    Ejecutar Terraform directamente contra el hipervisor y, al mismo tiempo, usar Ansible dentro de la VM sin un paso de “plan” claro genera drift. Cada herramienta intenta “ser dueña” del mismo recurso (por ejemplo, una VM creada por Terraform y modificada por Ansible).

  2. Almacenamiento ZFS sin planificación de datasets
    Crear un único pool ZFS y montar todo bajo /mnt sin dividir en datasets para logs, backups y datos de medios provoca problemas de rendimiento y de retención de snapshots.

  3. Ingress y certificación mal orquestados
    Traefik sin MetalLB en modo “layer‑2” puede colapsar cuando el nodo pierde la IP virtual. Además, cert‑manager necesita un ClusterIssuer configurado antes de que cualquier Ingress solicite certificados; de lo contrario, los pods quedan en estado CrashLoopBackOff.

  4. Backup de Kubernetes sin integración de Volumes
    Velero funciona bien con snapshots de discos, pero si los PersistentVolumeClaims (PVC) están en un pool ZFS que no está siendo snapshotado, los datos de la aplicación no se respaldan.

  5. CI/CD auto‑hosted sin aislamiento de recursos
    Un runner de GitHub Actions ejecutándose en la misma VM que el clúster k3s compite por CPU y RAM, lo que genera builds lentos y fallos intermitentes.

Solución

1. Definir una arquitectura de capas clara

  • Capa 1 – Hipervisor: Proxmox VE como único punto de entrada. Usa el provider oficial de Terraform para describir VMs, redes y discos.
  • Capa 2 – Sistema operativo base: Ubuntu Server 24.04 LTS en cada VM. Configura cloud‑init para aplicar la misma plantilla de usuario y claves SSH.
  • Capa 3 – Orquestación: k3s dentro de una VM dedicada (4 CPU, 8 GB RAM). Instala Helm y ArgoCD para GitOps.
  • Capa 4 – Servicios de soporte: Otra VM para Vault, Traefik, MetalLB y cert‑manager. Mantén los componentes críticos fuera del clúster para que puedan seguir operando si k3s falla.
  • Capa 5 – Almacenamiento: ZFS en el host físico, con datasets específicos:
    • tank/media → NFS exportado a la VM de Jellyfin.
    • tank/k8s → Zvol usado como PV para k3s.
    • tank/backups → destino de Restic y Velero.

2. IaC coherente con Terraform → Ansible

  1. Terraform: crea VMs, redes y discos. Exporta los IPs como outputs.
  2. Ansible: consume los outputs mediante terraform_output plugin y aplica configuraciones de sistema (instalación de Docker, k3s, etc.).
  3. Pipeline CI: GitHub Actions ejecuta terraform planterraform applyansible-playbook. Usa un runner auto‑hosted en la VM de “infra”.

3. Configuración de networking robusta

  • MetalLB en modo “layer‑2” con un rango de IPs estáticas dentro de la subred LAN.
  • Traefik como IngressController, habilitando entryPoints.websecure y certificatesResolvers.le.acme.email.
  • cert‑manager con un ClusterIssuer tipo letsencrypt-prod. Aplica el Issuer antes de cualquier Ingress.

4. Backup integral

  • Restic: monta tank/backups y ejecuta backups diarios de los directorios NFS y de los volúmenes de Docker.
  • Velero: configura el plugin de aws apuntando a MinIO (S3 compatible) y habilita snapshotLocations que usan zfs snapshots del dataset tank/k8s.
  • Programa una cron en la VM de backup que haga zfs snapshot -r tank/k8s@$(date +%Y%m%d%H%M) antes de que Velero inicie su backup.

5. Aislamiento de recursos para CI/CD

  • Reserva 2 CPU y 4 GB RAM al runner mediante cgroups o systemd slices.
  • Usa Docker-in-Docker (DinD) con --storage-driver overlay2 para evitar conflictos con el runtime de k3s.
  • Limita la concurrencia de jobs en el archivo runner.yaml.

Cuándo aplicar esta solución

  • Síntomas típicos: despliegues que fallan por recursos insuficientes, pérdida de datos después de una actualización de ZFS, certificados que no se renuevan y pods que se quedan en Pending por falta de PV.
  • Entorno adecuado: un solo nodo físico con al menos 12 TB de discos, 32 GB RAM y CPU de 6‑8 cores. Ideal para pruebas de IaC, pipelines CI/CD y gestión de medios familiares.
  • Exclusiones: si dispones de varios nodos físicos o un entorno de nube dedicado, la arquitectura monolítica de un solo host pierde sentido; en ese caso, distribuye los componentes en diferentes máquinas y usa un CNI como Calico.

Código

# Terraform: crear una VM para k3s
resource "proxmox_vm_qemu" "k3s" {
  name        = "k3s-master"
  target_node = "pve"
  iso         = "local:iso/ubuntu-24.04-live-server-amd64.iso"
  cores       = 4
  memory      = 8192
  scsihw      = "virtio-scsi-pci"
  bootdisk    = "scsi0"
  disk {
    size    = "80G"
    type    = "scsi"
    storage = "local-lvm"
    iothread = true
  }
  network {
    model  = "virtio"
    bridge = "vmbr0"
  }
  cloudinit {
    user = "ubuntu"
    password = "securepass"
    ssh_keys = [file("~/.ssh/id_rsa.pub")]
  }
}
output "k3s_ip" {
  value = proxmox_vm_qemu.k3s.ipv4_address
}
# Ansible: instalar k3s usando el output de Terraform
- hosts: "{{ hostvars['localhost']['terraform_output']['k3s_ip'] }}"
  become: true
  tasks:
    - name: Instalar paquetes requeridos
      apt:
        name: [curl, gnupg2]
        state: present
        update_cache: yes
    - name: Instalar k3s
      shell: |
        curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik" sh -
      args:
        creates: /usr/local/bin/k3s

Verificación

  1. Infraestructura

    • terraform apply debe terminar sin cambios pendientes.
    • ansible-playbook -i inventory site.yml debe completar sin errores y dejar el servicio k3s activo (systemctl status k3s).
  2. Ingress y TLS

    • Ejecuta kubectl get ingress -A y verifica que la columna ADDRESS muestre una IP de MetalLB.
    • curl -k https://<host>/ debe devolver el certificado de Let’s Encrypt (verifica con openssl s_client).
  3. Backup

    • Revisa el último snapshot ZFS: zfs list -t snapshot -r tank/k8s.
    • En la VM de backup, confirma que Restic muestra un repositorio válido: restic snapshots.
  4. CI Runner

    • En la UI de GitHub, dispara un workflow que ejecute terraform plan. El job debe terminar con “Success”.

Notas adicionales

  • ZFS tuning: habilita compression=lz4 y recordsize=128K en los datasets de medios para ahorrar espacio sin penalizar el rendimiento de los backups.
  • MetalLB en modo L2 funciona bien en redes domésticas sin routers que bloqueen ARP; si el router lo hace, cambia a modo BGP y configura un vecino externo.
  • ArgoCD: usa appProject con destinations limitados a k3s para evitar despliegues accidentales en la VM de infraestructura.
  • Velero: cuando usas MinIO como backend, asegura que la política de bucket permita s3:PutObject y s3:DeleteObject; de lo contrario, los backups fallan silenciosamente.
  • Monitorización: Grafana dashboards predefinidos para Proxmox, k3s y ZFS están disponibles en la comunidad; importarlos ahorra tiempo y brinda alertas tempranas de saturación de disco.