Kubernetes Administration — Level by Level›11 · Bare-metal load balancing with MetalLB

Lesson 11 of 32 · Level 2 — Operator

Bare-metal load balancing with MetalLB

Give LoadBalancer Services real external IPs on bare metal and VMs with MetalLB: how L2 (ARP) and BGP modes work, installing it, address pools and advertisements, a kind lab, and the trade-offs against kube-vip, Cilium and hardware load balancers.

Practitioner → Advanced
Key wordsMetalLBbare metalLoadBalancer ServiceIPAddressPoolL2AdvertisementARPBGPBGPPeerECMPstrictARPkube-vipCilium LB IPAM
L2 mode (ARP) Client → 10.0.0.240 Switch / L2 segment who has .240? Node A speaker answers ARP owns 10.0.0.240 Node B speaker on standby takes over on failure kube-proxy → Service → pods pods may run on any node BGP mode Router (ToR) learns 10.0.0.240/32 from every announcing node → ECMP Node A speaker BGP session Node B speaker BGP session Node C speaker BGP session traffic spread across nodes true load balancing; needs router config controller assigns IPs from an IPAddressPool
L2 mode: one node answers for the IP (failover). BGP mode: routers spread traffic across nodes.

The missing piece on bare metal

On a cloud, type: LoadBalancer makes the cloud controller create a load balancer with a public IP. On bare metal or plain VMs, nothing does that: the Service stays EXTERNAL-IP <pending>, and your ingress controller (lesson 05) has no address.

MetalLB fills the gap. It watches LoadBalancer Services, assigns an IP from pools you define, and announces that IP on your network so traffic reaches the cluster.

A shop in a new street has no address yet, so no one can find it. MetalLB is the town office that gives each shop an address from a list (the IP pool), and then either shouts "that's me!" when someone asks the street for that address (L2/ARP), or tells the postal service's route map where to deliver (BGP).

Two components, two modes

  • controller (Deployment): assigns IPs from IPAddressPools to Services.
  • speaker (DaemonSet): announces assigned IPs from the nodes.
L2 mode (ARP/NDP) BGP mode
How One node answers ARP for each IP Nodes advertise /32 routes to routers
Load spreading No: one node per IP receives traffic (then kube-proxy spreads to pods) Yes: routers use ECMP across announcing nodes
Failover Another node takes over after the leader is lost (seconds) Router withdraws routes from failed node
Network needs IPs in the nodes' L2 subnet BGP-capable routers + configuration
Best for Labs, small clusters, simple networks Production data centres

Install

$ helm repo add metallb https://metallb.github.io/metallb
$ helm install metallb metallb/metallb -n metallb-system --create-namespace
$ kubectl -n metallb-system get pods

If kube-proxy runs in IPVS mode, enable strictARP: true in its configuration first (MetalLB's docs show how); iptables mode needs no change.

L2 configuration

Pick addresses in the nodes' subnet that your DHCP server won't hand out:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: lan-pool
  namespace: metallb-system
spec:
  addresses:
    - 10.0.0.240-10.0.0.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: lan
  namespace: metallb-system
spec:
  ipAddressPools: [ lan-pool ]

BGP configuration (outline)

apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
  name: tor-a
  namespace: metallb-system
spec:
  myASN: 64512
  peerASN: 64513
  peerAddress: 10.0.0.1
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
  name: dc
  namespace: metallb-system
spec:
  ipAddressPools: [ dc-pool ]

Agree ASNs, peer addresses, route filters and pools with the network team. (API versions of MetalLB resources change occasionally; check the docs for your release.)

Lab: MetalLB on kind (Linux host)

kind nodes are containers on the Docker network kind; carve a few IPs from its subnet:

$ docker network inspect kind -f '{{(index .IPAM.Config 0).Subnet}}'
172.18.0.0/16

Use e.g. 172.18.255.200-172.18.255.250 as the pool (IPv4 subnet may be the second entry on some setups; use the IPv4 one). Apply the L2 config above with that range, then:

$ kubectl create deployment web --image=nginx --replicas=2
$ kubectl expose deployment web --port 80 --type LoadBalancer
$ kubectl get svc web
NAME   TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)
web    LoadBalancer   10.96.88.12    172.18.255.200   80:31234/TCP
$ curl -s http://172.18.255.200 | head -4

On macOS and Windows, Docker runs in a VM, so these IPs aren't reachable from the host directly; run the curl from another container on the kind network, or use a Linux VM.

Now switch your ingress controller Service (lesson 05) to LoadBalancer: it gets a stable IP from the pool, and all HTTP apps share it.

Alternatives

Option Notes
kube-vip Control-plane VIP and Service LoadBalancer IPs (ARP or BGP); common with kubeadm, EKS Anywhere and RKE2
Cilium LB IPAM + L2 announcements / BGP control plane If you already run Cilium
Hardware/virtual LB (F5, HAProxy, cloud LB) In front of NodePorts; existing enterprise standard
OpenStack Octavia On OpenStack, via the cloud controller (see OpenStack Private Cloud, lesson 09)

Try it: pools, failover and ingress

  1. Install MetalLB on a kind (Linux) or kubeadm cluster and create the L2 pool.
  2. Expose a Deployment as LoadBalancer and reach its EXTERNAL-IP.
  3. Find which node announces the IP (speaker logs), stop that node's container/VM, and measure how long until traffic flows again.
  4. Request a specific address with metallb.io/loadBalancerIPs: 172.18.255.210 as a Service annotation (check the annotation name for your version).
  5. Switch the ingress-nginx Service to LoadBalancer and point a DNS/hosts entry at its new IP.

Going deeper: MetalLB in production

  • Split pools by purpose (public ingress, internal services) and restrict which namespaces/Services may use each (pool selectors, autoAssign: false).
  • With externalTrafficPolicy: Local, only nodes running ready pods announce (BGP) or can be elected (L2), preserving client IPs; check the behaviour for your mode.
  • Monitor speaker/controller metrics and BGP session state; a dropped BGP session silently removes routes.

Recap

  • Bare metal has no built-in LoadBalancer; MetalLB assigns IPs from IPAddressPools and announces them.
  • L2 mode: one node answers ARP per IP (failover, no spreading); BGP mode: routers spread traffic with ECMP.
  • Configure pools + advertisements; enable strictARP for IPVS kube-proxy.
  • Give your ingress controller a MetalLB IP so many apps share one stable address.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.