In der Cloud ist ein Service vom Typ LoadBalancer ein gelöstes Problem — der Provider zaubert eine IP herbei. Auf Bare-Metal ist genau das die Lücke: Wer vergibt die IP, und wer sorgt dafür, dass das Netzwerk sie auch findet? In meinem Lab erledigt das
Cilium
— als CNI und gleichzeitig als BGP-Sprecher, der die Service-IPs an meine
Ubiquiti UDM-SE
annonciert.
Cilium als CNI#
Cilium ist auf jedem Cluster die einzige CNI, installiert direkt über die Talos-Maschinenkonfiguration. Es ersetzt kube-proxy vollständig (kubeProxyReplacement: true) und bewegt das gesamte Service- und NetworkPolicy-Handling per
eBPF
in den Kernel. Pod-IPAM läuft im kubernetes-Modus; die Verschlüsselung des Pod-Traffics ist bewusst aus, weil die clusterübergreifenden Pfade ohnehin über das WireGuard des Headscale-Tailnets laufen.
Spannend wird es beim Übergang vom Cluster ins LAN.
Das Problem: IPs, die das LAN kennt#
Ein LoadBalancer-Service braucht zwei Dinge: eine IP aus einem definierten Pool und einen Weg, wie der Router im LAN erfährt, dass diese IP hinter den Cluster-Knoten liegt. Den Pool definiere ich als CiliumLoadBalancerIPPool — ein kleines /28 reicht für mein Lab:
1apiVersion: cilium.io/v2
2kind: CiliumLoadBalancerIPPool
3metadata:
4 name: worker-ip-pool
5spec:
6 allowFirstLastIPs: No
7 blocks:
8 - cidr: "10.103.0.0/28"
Für die Bekanntmachung gäbe es zwei Wege: L2-Announcements (Gratuitous ARP, simpel, aber auf ein einzelnes Broadcast-Segment beschränkt und mit einem einzigen Knoten als Failover-Engpass) oder BGP. Ich nutze BGP, weil es die Service-IPs als echte, geroutete Präfixe ins LAN propagiert, sauber über mehrere Knoten lastverteilt und mit der UDM-SE ohnehin einen vollwertigen BGP-Sprecher als Gegenstelle vorfindet.
Die BGP-Control-Plane#
Ciliums BGP-Control-Plane wird per Helm-Value aktiviert (bgpControlPlane.enabled: true). Den Rest beschreiben drei CRDs. Zuerst die Cluster-Config: Welche Knoten sprechen mit wem? Bei mir sind das die Worker (Label bgp-policy: worker), die sich ihren Peer elegant selbst suchen — über das Default-Gateway:
1apiVersion: cilium.io/v2
2kind: CiliumBGPClusterConfig
3metadata:
4 name: cilium-bgp
5spec:
6 nodeSelector:
7 matchLabels:
8 bgp-policy: worker
9 bgpInstances:
10 - name: "instance-64512"
11 localASN: 64512
12 peers:
13 - name: "peer-65535"
14 peerASN: 65535
15 autoDiscovery:
16 mode: "DefaultGateway"
17 defaultGateway:
18 addressFamily: ipv4
19 peerConfigRef:
20 name: "cilium-peer"
autoDiscovery: DefaultGateway ist mein Lieblingsdetail: Statt die Peer-IP hart zu verdrahten, peert jeder Worker einfach mit seinem Default-Gateway — also der UDM-SE. Zieht der Cluster in ein anderes Netz um, muss ich hier nichts anfassen.
Ebenso wichtig ist, welche Knoten der nodeSelector trifft: Nur Worker mit dem Label bgp-policy: worker werden zu BGP-Speakern. Die Control-Plane-Knoten (bgp-policy: controller) bleiben bewusst draußen — sie tragen keinen Ingress-Verkehr und sollen keine Service-IPs ankündigen. Dieser Selector ist ein stiller Single-Point-of-Failure: Trifft er versehentlich keinen Knoten, lernt die UDM-SE keine einzige VIP, und sämtliche Dienste sind von außen tot — ohne dass auch nur ein Pod-Log das Wort „BGP" in den Mund nimmt.
Die CiliumBGPPeerConfig legt die Session-Eigenschaften fest (IPv4 unicast, Graceful Restart) und welche Advertisements gelten, und die CiliumBGPAdvertisement sagt schließlich, was annonciert wird — nämlich die LoadBalancer-IPs:
1apiVersion: cilium.io/v2
2kind: CiliumBGPAdvertisement
3metadata:
4 name: bgp-advertisements
5 labels:
6 advertise: bgp
7spec:
8 advertisements:
9 - advertisementType: "Service"
10 service:
11 addresses:
12 - LoadBalancerIP
Im Ergebnis sieht die Topologie so aus — meine Worker (AS 64512) als eBGP-Peers der UDM-SE (AS 65535):
Die Gegenstelle: BGP auf der UDM-SE#
Damit die Session zustande kommt, muss die UDM-SE als Neighbor konfiguriert sein. Die UniFi-Geräte bringen unter der Haube
FRR
mit, sodass sich eine simple BGP-Konfiguration hinterlegen lässt, die meine AS 64512 als Nachbarn akzeptiert und die gelernten /32-Routen aus dem LB-Pool ins LAN verteilt.
vtysh -c 'show bgp summary', ob die Session zur Cluster-AS wieder steht.Fazit#
Mit ein paar CRDs wird Cilium vom reinen CNI zum vollwertigen BGP-Sprecher. Jeder neue LoadBalancer-Service zieht automatisch eine IP aus dem /28, die Sekunden später im ganzen LAN geroutet ist — ohne MetalLB, ohne ARP-Tricks, ohne manuelles Zutun. Genau diese IPs sind später die Basis dafür, dass meine Dienste auch über das Tailnet
erreichbar werden.
