The load balancer when there is no cloud
Proforma's hiring manager named it explicitly: MetalLB is the load balancer. That means bare-metal OpenShift with BGP-speaking switches — and it means you should know exactly where the VIPs come from, who announces them, and what breaks when.
Type: LoadBalancer needs a cloud
In AWS, type: LoadBalancer creates an ELB — the cloud controller does it. On bare metal there's no ELB, so the Service sits in <pending> forever. MetalLB fills that gap: it allocates an IP from a pool and makes the network actually deliver traffic to it. Two ways to do that — and picking right is a core on-prem skill.
| L2 mode (ARP/NDP) | BGP mode | |
|---|---|---|
| How it works | One node answers ARP for the VIP; the other speakers hold back as backup | Every speaker pegs with your routers and announces the VIP's /32 from each eligible node |
| Traffic path | All traffic → the leader node → kube-proxy → pods anywhere | Routers ECMP across all announcing nodes |
| Failure | Gratuitous ARP moves the VIP to a new leader (~seconds) | BGP withdraw; routers reconverge (with BFD, sub-second) |
| Limits | Single-node bottleneck; no true load balancing of the VIP itself | Needs BGP-capable routers; ECMP hashing quirks; more moving parts |
| Use it when | Small sites, labs, no router access | Production DC — this is Proforma's world |
Controller + speaker, that's the whole thing
controller (one)
Watches Services of type LoadBalancer, allocates IPs from IPAddressPool CRs, and hands each Service an IP. Pure control plane — it moves no traffic.
speaker (DaemonSet)
One per node. Announces the VIPs: ARP replies in L2 mode, BGP advertisements in BGP mode. Since v0.13 the BGP stack is FRR running inside the speaker — the same FRR syntax you'd use on a router.
The CRDs that define everything
IP address pool + BGP peer + advertisementapiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata: { name: apps-pool, namespace: metallb-system }
spec:
addresses: ["10.100.20.0/24"] # your routed, unused DC block
---
apiVersion: metallb.io/v1beta1
kind: BGPPeer
metadata: { name: tor-peer, namespace: metallb-system }
spec:
myASN: 65001
peerASN: 65000
peerAddress: 10.10.0.1 # ToR switch loopback
passwordSecret: { name: bgp-pass, namespace: metallb-system } # TCP MD5
holdTime: 9s
bfdProfile: fast-failover
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata: { name: apps-adv, namespace: metallb-system }
spec:
ipAddressPools: ["apps-pool"]
nodeSelectors: [{ matchLabels: { "node-role.kubernetes.io/worker": "" } }]
communities: ["65001:100"]
Client → ToR switch (ECMP) → node:VIP → kube-proxy / OVN (DNAT to a pod IP) → pod. MetalLB only ever promises delivery to the node; the Service's selector and kube-proxy handle node→pod. Two hops, two failure domains — know which one broke before you touch anything.
MetalLB Operator (Red Hat supported)
OpenShift ships MetalLB through OLM as the MetalLB Operator. Same CRDs, same FRR — but Red Hat-supported, integrated with the console, and the supported path for exposing LoadBalancer services on bare metal.
- Typical pattern: MetalLB VIP in front of the OpenShift Router (HAProxy) — MetalLB is L4 (the IP), the Router is L7 (host/path routing). Full picture on the L4/L7 page.
- Combines with FRR-K8s in newer releases (FRR-K8s can run standalone as the BGP/NFD implementation MetalLB uses).
- Node maintenance: drain a node → speaker withdraws its routes before the node leaves. BGP mode makes worker patching non-disruptive — that's a nice day-2 talking point.
1) VIP pool overlapping your DHCP/subnet = ARP chaos. 2) L2 mode silently funnels all traffic through one node. 3) BGP mode needs the switches to actually accept the session — default-deny policies block peering. 4) externalTrafficPolicy: Cluster hides client source IPs (second hop SNAT); Local preserves them but breaks even pod distribution. 5) Withdrawal ≠ instant — BFD profiles are how you get sub-second failover.
Questions to answer out loud
Why can't you just use kube-proxy / a NodePort for everything?
NodePort gives you a port on every node but no stable IP and no control of the node-level path — clients must hit specific node IPs and you eat double SNAT. MetalLB gives you a real, routable, cluster-abstracted VIP that survives node failure, plus integration with your BGP fabric for ECMP and fast failover.
Your Service gets an IP but traffic doesn't arrive. Where do you look?
1) Is the VIP actually in the pool and shown on the Service? 2) Are speaker pods running on eligible nodes (nodeSelector)? 3) Is the BGP session Established on the switch side — prefixes received? 4) Does the upstream router have a route to the VIP (ECMP next-hops)? 5) Node firewall blocking the Service port? Work the path hop by hop.
What does FRR do inside MetalLB?
Each speaker runs an embedded FRR instance: it forms the BGP session with your peers, sends the announcements MetalLB computes (VIP /32s with your communities and localpref), handles MD5 authentication and BFD, and withdraws on shutdown. That's why knowing FRR CLI/config dialect pays off twice: on routers and inside the cluster.