Skip to main content

MetalLB Load Balancer

Production Purpose: In cloud environments (AWS, GCP), a LoadBalancer Service automatically provisions an external load balancer. On bare-metal, this doesn't exist. MetalLB fills that gap — it gives bare-metal Kubernetes clusters a real external IP for Services, exactly how cloud providers do it.


Why MetalLB?

Without MetalLB, LoadBalancer Services on bare-metal stay in <pending> state forever.


MetalLB Modes

ModeHow It WorksUse Case
L2 (ARP/NDP)Announces IPs via ARP broadcastsSimple LANs, Proxmox labs
BGPAnnounces IPs via BGP to routersProduction data centers

For this lab we use L2 mode — simpler and works without a BGP-capable router.


IP Address Pool Planning

You need to dedicate a range of IPs on your LAN that:

  • Are not in your DHCP range
  • Are on the same subnet as your nodes

Since your nodes are on 192.168.90.0/24, we'll use:

PurposeIP Range
Node IPs192.168.90.26192.168.90.28
MetalLB Pool192.168.90.100192.168.90.120
caution

Reserve this range in your router/DHCP server to avoid IP conflicts.


Install MetalLB

Input:

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml

Output:

namespace/metallb-system created
customresourcedefinition.apiextensions.k8s.io/...
deployment.apps/controller created
daemonset.apps/speaker created

Wait for MetalLB pods to be ready:

kubectl wait --namespace metallb-system \
--for=condition=ready pod \
--selector=app=metallb \
--timeout=90s

Output:

pod/controller-xxx condition met
pod/speaker-xxx condition met

Configure IP Address Pool

Create: metallb-pool.yaml

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production-pool
namespace: metallb-system
spec:
addresses:
- 192.168.90.100-192.168.90.120
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: production-l2
namespace: metallb-system
spec:
ipAddressPools:
- production-pool

Apply:

kubectl apply -f metallb-pool.yaml

Output:

ipaddresspool.metallb.io/production-pool created
l2advertisement.metallb.io/production-l2 created

Verify MetalLB is Working

Create a Test LoadBalancer Service

Create: test-lb.yaml

apiVersion: v1
kind: Pod
metadata:
name: nginx-test
labels:
app: nginx-test
spec:
containers:
- name: nginx
image: nginx:alpine
---
apiVersion: v1
kind: Service
metadata:
name: nginx-test-lb
spec:
type: LoadBalancer
selector:
app: nginx-test
ports:
- port: 80
targetPort: 80

Apply:

kubectl apply -f test-lb.yaml

Check External IP Assignment

kubectl get svc nginx-test-lb

Output:

NAME            TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)        AGE
nginx-test-lb LoadBalancer 10.96.45.123 192.168.90.100 80:32xxx/TCP 30s

MetalLB assigned 192.168.90.100 from the pool.

Test Access from Your Laptop

curl http://192.168.90.100

Output:

<!DOCTYPE html>
<html>
<head><title>Welcome to nginx!</title></head>
...

nginx is accessible from outside the cluster via a real IP.


How L2 Mode Works Internally

MetalLB's speaker pods watch for LoadBalancer Services and announce IPs via ARP.


Cleanup Test Resources

kubectl delete pod nginx-test
kubectl delete svc nginx-test-lb

Troubleshooting

SymptomCauseFix
EXTERNAL-IP stays <pending>MetalLB not installed or pool not configuredVerify kubectl get ipaddresspool -n metallb-system
IP assigned but unreachableIP conflicts with DHCPReserve the range in your router
speaker pods in CrashLoopBackOffstrict ARP not configuredSee below
IP assigned but wrong node respondsARP tie — multiple speakersExpected in L2 mode, only one wins

Enable Strict ARP (Required for Some Setups)

kubectl edit configmap kube-proxy -n kube-system

Set:

mode: "ipvs"
ipvs:
strictARP: true

Or patch directly:

kubectl get configmap kube-proxy -n kube-system -o yaml | \
sed -e 's/strictARP: false/strictARP: true/' | \
kubectl apply -f -

Production Best Practices

PracticeReason
Use BGP mode in real data centersMore scalable, no ARP broadcast storm
Reserve MetalLB IPs in DHCP serverPrevent IP conflicts
Create multiple IP poolsSeparate dev/staging/prod load balancer IPs
Monitor speaker podsIf all speakers die, external IPs stop working
Use spec.loadBalancerIP to pin IPsEnsures stable IP for DNS records

Pin a Specific IP to a Service

spec:
type: LoadBalancer
loadBalancerIP: 192.168.90.100 # Always use this IP