Problema

Los entornos de borde suelen requerir servidores pequeños, silenciosos y de bajo consumo. Un clúster HA con tres SBCs ofrece redundancia, pero el coste de hardware, NVMe y switches PoE rápidamente supera el presupuesto de un proyecto doméstico o de una oficina remota. Además, la gestión de un sistema operativo tradicional (Armbian, Debian, etc.) genera deriva de configuración: paquetes instalados manualmente, servicios que se activan o desactivan sin control y actualizaciones que pueden romper la estabilidad. El reto es conseguir un nodo único que ejecute Kubernetes de forma fiable, con una superficie de ataque mínima y sin necesidad de intervención manual después del despliegue inicial.

Causa

  1. Dependencia de un SO mutable – Cuando el nodo corre un Linux genérico, cualquier cambio manual (instalar podman, modificar systemd) queda fuera del control de GitOps. La deriva hace que la reproducibilidad sea casi imposible.
  2. Modelo push de configuración – Herramientas como Ansible ejecutan tareas vía SSH. Si la conexión se corta, el nodo queda en estado parcial, lo que obliga a depurar manualmente.
  3. Exposición de puertos host – Mapear puertos directamente al host obliga a gestionar colisiones y reglas de firewall en cada nodo, algo que escala mal en entornos con varios servicios.
  4. Almacenamiento local sin orquestación – Usar discos NVMe sin un provisionador dinámico obliga a crear volúmenes manualmente, lo que complica la migración de workloads.

Solución

Adoptar una arquitectura basada en Talos Linux como sistema operativo inmutable, Cilium como CNI con soporte nativo para la Gateway API L2, y Flux CD para GitOps. El flujo es:

  1. Flash de Talos – Talos se escribe directamente en la eMMC o en una tarjeta microSD mediante una imagen firmada. La configuración se mantiene en un archivo YAML que Talos consume a través de su API.
  2. Cluster de un solo nodo – Talos permite crear simultáneamente el control‑plane y el worker en la misma máquina, evitando componentes externos.
  3. Cilium con L2 Announcements – Cilium anuncia una IP virtual mediante ARP, de modo que el Ingress/Envoy se expone en una dirección distinta a la IP de gestión del nodo. Esto elimina la necesidad de mapear puertos host.
  4. Local Path Provisioner – Se despliega como DaemonSet y utiliza el NVMe directamente, ofreciendo PersistentVolumeClaims sin depender de NFS o CSI externos.
  5. Flux CD + SOPS/age – El repositorio Git contiene los manifests de la aplicación, los secretos cifrados y la configuración de Talos. Flux observa el repo, aplica cambios y actualiza imágenes automáticamente.

Paso a paso resumido

  • Preparar la imagen: descargar Talos v1.13.5+ para ARM64 y flashear con talosctl.
  • Generar config: talosctl gen config <cluster-name> <endpoint> crea controlplane.yaml y worker.yaml. Editar para incluir la IP estática del SBC y la ruta del disco NVMe.
  • Aplicar configuración: talosctl apply-config --insecure --nodes <IP> --file controlplane.yaml.
  • Instalar Cilium: usar el Helm chart oficial con cilium install --set kubeProxyReplacement=strict.
  • Crear L2 Gateway: definir un CiliumL2AnnouncementPolicy que publique la IP del Ingress.
  • Desplegar Local Path: kubectl apply -f https://github.com/rancher/local-path-provisioner/raw/master/deploy/local-path-storage.yaml.
  • Bootstrapping Flux: flux bootstrap github --owner=<org> --repository=<repo> --branch=main --path=./clusters/<cluster-name>.

Cuándo aplicar esta solución

  • Edge node con recursos limitados – Cuando el presupuesto permite una sola SBC con NVMe y se necesita alta disponibilidad de software, no de hardware.
  • Entornos donde la deriva de configuración es inaceptable – Si la política de la empresa exige que cualquier cambio pase por revisión de código.
  • Necesidad de IP dedicada para servicios – Cuando se quiere separar la IP de gestión de la IP de los servicios expuestos sin usar NAT o puertos host.
  • No se requiere HA a nivel de nodo – Si la tolerancia a fallos se delega a la capa de aplicación (replicas en la nube) y el nodo solo actúa como punto de entrada.

No es recomendable cuando:

  • Se necesita redundancia física (p.ej., en una fábrica donde el nodo puede fallar).
  • El SBC no soporta eBPF o la versión de kernel requerida por Cilium.
  • El equipo de operaciones prefiere un SO tradicional con acceso a paquetes apt y systemd.

Código

# 1. Descargar y flash Talos (asume microSD en /dev/sdb)
curl -LO https://github.com/siderolabs/talos/releases/download/v1.13.5/talos-arm64-amd64.raw.gz
gunzip talos-arm64-amd64.raw.gz
sudo dd if=talos-arm64-amd64.raw of=/dev/sdb bs=4M status=progress conv=fsync

# 2. Generar configuración base
talosctl gen config radxa-single-node 192.168.1.100

# 3. Editar controlplane.yaml (añadir static IP, disco NVMe)
#   network:
#     interfaces:
#       - device: eth0
#         addresses:
#           - 192.168.1.100/24
#         routes:
#           - network: 0.0.0.0/0
#             gateway: 192.168.1.1
#   machine:
#     install:
#       disk: /dev/nvme0n1

# 4. Aplicar configuración al nodo recién arrancado
talosctl apply-config --insecure --nodes 192.168.1.100 --file controlplane.yaml

# 5. Instalar Cilium vía Helm
helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --namespace kube-system \
  --set kubeProxyReplacement=strict \
  --set l2announcements.enabled=true

# 6. Crear L2 AnnouncementPolicy (guardado en k8s.yaml)
cat <<EOF | kubectl apply -f -
apiVersion: cilium.io/v2
kind: CiliumL2AnnouncementPolicy
metadata:
  name: edge-ingress
spec:
  interfaces:
    - eth0
  ipPools:
    - cidr: 192.168.1.200/32
EOF

# 7. Desplegar Local Path Provisioner
kubectl apply -f https://github.com/rancher/local-path-provisioner/raw/master/deploy/local-path-storage.yaml

# 8. Bootstrap Flux (asume repo ya creado)
flux bootstrap github \
  --owner=myorg \
  --repository=edge-cluster \
  --branch=main \
  --path=./clusters/radxa-single-node

Verificación

  1. Talos: talosctl health --nodes 192.168.1.100 debe devolver healthy.
  2. Kubernetes: kubectl get nodes muestra un nodo con Ready y control-plane,worker.
  3. Cilium: cilium status indica cilium-agent y cilium-operator en Running.
  4. L2 IP: desde otro host, ping 192.168.1.200 debe responder; arp -a muestra la MAC del SBC asociada a esa IP.
  5. Flux: flux get sources git muestra el repositorio sincronizado y flux get kustomizations lista los objetos aplicados.
  6. PVC: crear un PVC con storageClassName: local-path y verificar que el pod se monta en /var/lib/kubelet/pods/.../volumes/kubernetes.io~csi/....

Notas adicionales

  • Copia de seguridad de la configuración Talos: exporta el estado con talosctl backup --nodes <IP> --output backup.talos. Restaurar con talosctl restore.
  • Actualizaciones de Talos: usar talosctl upgrade permite aplicar parches sin tocar el disco, manteniendo la inmutabilidad.
  • Limitaciones de eBPF: en algunos kernels ARM64 el módulo bpfilter puede estar deshabilitado; verifica con sysctl net.ipv4.ip_forward.
  • Monitoreo ligero: Prometheus + Grafana pueden correr como Helm releases; el consumo de recursos en una SBC con 32 GB RAM sigue siendo aceptable.
  • Seguridad de los secretos: SOPS + age funciona bien con claves almacenadas en un vault externo; evita colocar la clave privada en el repo.