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
- 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.
- 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.
- 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.
- 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:
- 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.
- Cluster de un solo nodo – Talos permite crear simultáneamente el control‑plane y el worker en la misma máquina, evitando componentes externos.
- 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.
- Local Path Provisioner – Se despliega como DaemonSet y utiliza el NVMe directamente, ofreciendo PersistentVolumeClaims sin depender de NFS o CSI externos.
- 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>creacontrolplane.yamlyworker.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
CiliumL2AnnouncementPolicyque 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
- Talos:
talosctl health --nodes 192.168.1.100debe devolver healthy. - Kubernetes:
kubectl get nodesmuestra un nodo conReadyycontrol-plane,worker. - Cilium:
cilium statusindica cilium-agent y cilium-operator en Running. - L2 IP: desde otro host,
ping 192.168.1.200debe responder;arp -amuestra la MAC del SBC asociada a esa IP. - Flux:
flux get sources gitmuestra el repositorio sincronizado yflux get kustomizationslista los objetos aplicados. - PVC: crear un PVC con
storageClassName: local-pathy 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 contalosctl restore. - Actualizaciones de Talos: usar
talosctl upgradepermite aplicar parches sin tocar el disco, manteniendo la inmutabilidad. - Limitaciones de eBPF: en algunos kernels ARM64 el módulo
bpfilterpuede estar deshabilitado; verifica consysctl 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.