En el Episode 3 dejé al cluster con storage replicado: PVCs de Longhorn que sobreviven a que se caiga un worker entero.
Pero un cluster que sólo se consulta con kubectl port-forward es una fortaleza sin puertas. Llega el momento de exponer servicios al LAN — y acá el homelab se choca con una de esas diferencias silenciosas entre la nube y casa.
El problema: type: LoadBalancer “simplemente funciona”… en la nube
En cualquier cloud, creás un Service con type: LoadBalancer y a los segundos aparece una IP. Eso es porque hay un cloud controller del proveedor escuchando: reserva una IP en su infraestructura, la conecta al cluster, y listo.
En un lab casero ese controller no existe. El Service queda así para siempre:
NAME TYPE EXTERNAL-IP PORT(S)
mi-app LoadBalancer <pending> 80:31500/TCP
NodePort es la salida rápida, pero es fea: un puerto alto por app, y la IP del nodo hardcodeada donde quieras consumirla. Lo que quiero es lo que tiene la nube: IPs de mi LAN, limpias, una por servicio.
MetalLB en modo L2 lo arregla con un truco viejo y querido: ARP. Un worker del cluster “responde” por la IP cuando alguien en la LAN pregunta quién la tiene — y el router ni se entera. Sin tocar el router, sin DNS, sin nada del lado de la red.
1. Namespace privilegiado
El componente que hace la magia L2 —el speaker— necesita hostNetwork y NET_RAW: habla ARP directamente en la red del nodo. Con el enforce: baseline global del cluster, en Talos eso requiere pod security privileged (archivo metalLb/namespace.yaml):
kubectl apply -f metalLb/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: metallb-system
labels:
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/warn: privileged
Mismo patrón que Longhorn en el episodio anterior: el label va antes de instalar, no después.
2. Instalar (chart 0.16.1)
helm repo add metallb https://metallb.github.io/metallb && helm repo update
helm install metallb metallb/metallb -n metallb-system --version 0.16.1 \
-f metalLb/values.yaml
metalLb/values.yaml tiene una sola línea — desactivar FRR-K8s:
frrk8s:
enabled: false
FRR es para anunciar las IPs por BGP, que es lo que usaría si tuviera un router que hable BGP. Para L2/ARP no hace falta, y no tiene sentido cargar el pod extra.
3. El pool de IPs
Dos recursos CRD (metalLb/ip-pool.yaml): el rango de IPs que MetalLB puede repartir, y el “anuncio” que dice que se reparten por L2:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default-pool
namespace: metallb-system
spec:
addresses:
- 192.168.68.200-192.168.68.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default-l2
namespace: metallb-system
spec:
ipAddressPools:
- default-pool
kubectl apply -f metalLb/ip-pool.yaml
Tres decisiones acá, ninguna obvious:
- El rango está fuera del DHCP del router. Si el router puede asignar
192.168.68.205a un teléfono y MetalLB también se la da a un Service, tenés un conflicto de IP que aparece y desaparece. Un rango que nadie más puede tocar es requisito, no detalle. - Con kube-proxy en
nftablesno hace faltastrictARP. Ese flag es sólo para IPVS, y la mitad de los tutoriales que vas a encontrar te lo hacen configurar igual. Talos usanftablespor defecto: no tocar. - Los speakers corren sólo en los workers. Talos etiqueta el control plane con
node.kubernetes.io/exclude-from-external-load-balancers, así que MetalLB automáticamente no lo considera para anunciar IPs. Un nodo menos expuesto a la LAN, gratis.
4. Probar
Un Service de prueba (metalLb/test-lb.yaml) — un deployment mínimo con type: LoadBalancer:
kubectl apply -f metalLb/test-lb.yaml
kubectl get svc lb-test
NAME TYPE EXTERNAL-IP PORT(S)
lb-test LoadBalancer 192.168.68.200 80:31234/TCP
<pending> duró lo que duró: MetalLB le asignó una IP del pool al vuelo. Y la prueba real, desde otra máquina de la LAN, no desde el cluster:
curl http://192.168.68.200
Si responde, el ARP hizo su magia: la LAN entera ya ve una IP que no existe en ningún router ni en ninguna VM — la “responde” un pod.
kubectl delete -f metalLb/test-lb.yaml
Lo que me llevo de este episodio
<pending>eterno no es un bug: es que falta el controller. MetalLB es ese controller, en versión casera.- El pool va fuera del DHCP. El conflicto de IPs silencioso es el bug más difícil de cazar de esta configuración.
strictARPes para IPVS. Connftables, no tocar — por más que los tutoriales lo digan.- El control plane queda afuera solo. El label que Talos ya pone hace el trabajo; nada que configurar.
- FRR-K8s desactivado. BGP es para cuando el router hable BGP; ARP es para hoy.
Próximo episodio
Los Services ya consiguen IPs de la LAN. Pero una IP por servicio escala mal: lo que falta es un único punto de entrada que reparta por nombre de host y con candado — el ingress. En el próximo episodio: Traefik + cert-manager, con CA propia y cero DNS.
Mi router ahora le entrega tráfico a IPs que nunca asignó.
No le avisé, y no parece importarle. 😎