k3s 기본 내장 LoadBalancer 구현체인 ServiceLB(klipper-lb)를 MetalLB로 교체해서 "진짜 VIP 할당 + 노드 장애 시 자동 failover"를 구현하려 했다. 계획은 현재 상태 스냅샷 → ServiceLB 비활성화 → MetalLB 설치 → IP 풀 + L2Advertisement 설정 → 기존 서비스 재검증, failover 테스트 → 리소스 비교이었다.
실제로는 1단계 진단만 하고 멈춰다. 이 클러스터의 네트워크 토폴로지가 MetalLB의 두 동작 모드(L2/BGP) 어느 쪽과도 근본적으로 안 맞는다는 게 드러났고, 되돌리기 어려운 변경(ServiceLB 비활성화)까지 진행하는 대신 왜 안 맞는지 규명하고 현재 구성을 유지하는 근거를 남기는 것이 더 가치 있다고 판단.
$ kubectl get pods -n kube-system | grep svclb
svclb-traefik-fa6196ab-42gvf 2/2 Running k3s-worker1
svclb-traefik-fa6196ab-xgslk 2/2 Running k3s-worker2
svclb-traefik-fa6196ab-z2j5p 2/2 Running k3s-master
$ kubectl get svc -A | grep LoadBalancer
kube-system traefik LoadBalancer 10.43.253.202 100.103.119.1,100.122.146.63,100.83.49.100 80:31503/TCP,443:30352/TCP
LoadBalancer 타입 서비스는 Traefik 하나뿐이고, EXTERNAL-IP가 세 노드의 Tailscale IP 전부로 찍혀 있다 — klipper-lb는 노드마다 파드를 하나씨 띄워 각 노드의 지정 포트를 hostPort로 열어두는 방식.
$ kubectl top pods -n kube-system | grep svclb
svclb-traefik-fa6196ab-42gvf 0m 0Mi
svclb 파드는 CPU/메모리를 사실상 전혀 안 쓴다 — 실제 프록시 프로세스가 아니라 iptables DNAT 규칙만 심어두는 쉘 스크립트라서 상시 실행되는 워크로드가 없다. MetalLB의 speaker/controller는 실제 Go 프로세스라 구조적으로 더 많은 리소스를 쓸 수밖에 없다.
세 노드 공통 인터페이스 구성:
enp0s1 192.168.252.x/24 <BROADCAST,MULTICAST,UP,LOWER_UP>
cni0 10.42.N.1/24 (Flannel CNI 브리지)
tailscale0 100.x.x.x/32 <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP>
flannel.1 10.42.N.0/32 (VXLAN 오버레이)
핵심은 tailscale0 인터페이스 플래그의 NOARP다. 커널이 그 인터페이스에서 ARP 프로토콜 자체를 취급하지 않는다고 선언해둔 것 — Tailscale의 WireGuard 기반 point-to-point L3 터널 인터페이스라 ARP가 필요없다.
MetalLB의 L2 모드(L2Advertisement)는 "이 IP는 내가 담당한다"를 ARP/NDP 응답으로 광고하는 방식으로 동작한다. tailscale0가 ARP를 지원하지 않으므로 이 인터페이스를 대상으로 L2Advertisement를 걸면 MetalLB speaker가 ARP 응답을 보낼 방법 자체가 없다.
ARP가 정상 동작하는 진짜 L2 브로드캐스트 세그먼트는 enp0s1(192.168.252.0/24)이다. 문제는 이 대역이 Multipass가 이 세 VM을 호스팅하는 물리 macOS 머신 안에서만 유효한 사설 가상 네트워크라는 것. 이 클러스터에 실제로 붙는 모든 경로(다른 맥에서의 kubectl 접속, Tailscale Funnel로 들어오는 공인 인터넷 트래픽)는 전부 Tailscale을 거친다. 결론: enp0s1에 L2Advertisement를 걸면 기술적으로는 동작하겠지만 그 VIP는 Multipass 호스트 밖의 누구도 접근할 수 없다.
BGP를 말하는 라우터가 없고(가정용 공유기는 BGP 라우터 아님), keepalived(VRRP)도 멀티캐스트/L2 브로드캐스트에 의존하는 프로토콜이라 tailscale0에선 같은 이유로 막힌다.
klipper-lb는 "가상 IP 하나를 여러 노드가 나눠 갖는 것"이 아니라, NodePort를 세 노드 전부에 열어두고, 어느 IP로 들어오든 kube-proxy가 실제 파드가 떠있는 곳으로 다시 라우팅하는 것이다. 노드 하나가 죽어도 살아있는 나머지 두 노드의 Tailscale IP는 여전히 정상 응답한다 — 단일 VIP의 자동 failover는 아니지만 "N개 중 아무 IP나 접속 가능"이라는 실질적으로 동등한 가용성을 이미 제공.