- https://metallb.universe.tf
- https://github.com/kubernetes/ingress-nginx/blob/main/docs/deploy/baremetal.md#a-pure-software-solution-metallb
Concepts¶
MetalLB gives bare-metal Kubernetes clusters a working LoadBalancer service type — something cloud providers give you for free via their own load balancers, but that a self-hosted cluster has no equivalent for out of the box. Without MetalLB (or something like it), a LoadBalancer service on bare metal just sits in <pending> forever.
IP address allocation¶
- https://metallb.universe.tf/concepts/#address-allocation
MetalLB needs a pool of IP addresses on your local network to hand out to LoadBalancer services. These addresses come from your own network range — MetalLB doesn't create IPs, it just claims and advertises ones you tell it about, so they need to be addresses your router/network isn't already handing out via DHCP to other devices.
Layer 2 Mode¶
- https://metallb.universe.tf/concepts/layer2
In Layer 2 mode, one node at a time takes ownership of a given service IP and answers ARP requests for it directly — there's no real load-balancing across nodes at the network level (all traffic for that IP goes to whichever node currently owns it); failover to another node happens if the owning node goes down. This is simpler to set up than BGP mode but doesn't spread traffic across multiple nodes for a single IP the way BGP mode can.
Requirements¶
- https://metallb.universe.tf/#requirements
Verify requirements before installing — in particular, check network addon compatibility: some CNI plugins (Calico in particular — see the Troubleshooting doc) need extra configuration alongside MetalLB to avoid interface conflicts.
- I'll use 10.0.0.100-200 for IPs to start
Installation¶
- https://metallb.universe.tf/installation
Update the kube-proxy configmap¶
This step is only required if kube-proxy is running in IPVS mode. Since Kubernetes v1.14.2, IPVS mode needs strict ARP enabled, or MetalLB's Layer 2 speaker won't be able to properly claim ARP responses for its addresses. (If you're using kube-router as the service proxy, it enables strict ARP by default and this step isn't needed.)
kubectl edit configmap -n kube-system kube-proxy
Set the following under the ipvs section:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
strictARP: true
To automate this instead of hand-editing (safe to run — the sed pattern only flips false to true and does nothing if it's already true):
# preview the change first
kubectl get configmap kube-proxy -n kube-system -o yaml | \
sed -e "s/strictARP: false/strictARP: true/" | \
kubectl diff -f - -n kube-system
# apply for real
kubectl get configmap kube-proxy -n kube-system -o yaml | \
sed -e "s/strictARP: false/strictARP: true/" | \
kubectl apply -f - -n kube-system
Create the metallb namespace¶
kubectl create namespace metallb
(Upstream MetalLB's own manifests default to a metallb-system namespace — using plain metallb here is a fine custom choice, just worth remembering if you ever cross-reference upstream docs/manifests that assume metallb-system.)
or use a manifest file:
cat << EOF > metallb_ns.yaml
kind: Namespace
apiVersion: v1
metadata:
name: metallb
labels:
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/warn: privileged
EOF
kubectl apply -f metallb_ns.yaml
Label¶
The privileged Pod Security Admission labels are required because the MetalLB speaker pod needs elevated network permissions (it manipulates ARP/interfaces directly) that the default restricted or baseline PSA levels would block.
If you didn't add labels to the metallb namespace at creation:
Test:
kubectl label --dry-run=server --overwrite ns metallb \
pod-security.kubernetes.io/enforce=privileged \
pod-security.kubernetes.io/audit=privileged \
pod-security.kubernetes.io/warn=privileged
Run for real:
kubectl label --overwrite ns metallb \
pod-security.kubernetes.io/enforce=privileged \
pod-security.kubernetes.io/audit=privileged \
pod-security.kubernetes.io/warn=privileged
See also: https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels
Install metallb¶
helm repo add metallb https://metallb.github.io/metallb
helm repo update
helm -n metallb install metallb metallb/metallb
(Added helm repo update — without it, a repo added previously but not refreshed can silently install an older chart version than what's actually available.)
By default, the current Helm chart deploys MetalLB's FRR-K8s BGP backend alongside the speaker/controller even for pure Layer 2 use — this is normal and doesn't require BGP to actually be configured or used.
Confirm everything came up:
kubectl get pods -n metallb
You should see one controller deployment pod and one speaker pod per node (it's deployed as a DaemonSet).
MetalLB Configuration¶
Configure address pool¶
ipaddresspool_simple.yml¶
- https://github.com/metallb/metallb/blob/main/configsamples/ipaddresspool_simple.yml
cat << EOF > metallb_config.yml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: example
namespace: metallb
spec:
addresses:
- 10.0.0.100-10.0.0.200
EOF
Layer 2 configuration¶
- https://metallb.universe.tf/configuration/#layer-2-configuration
An IPAddressPool alone doesn't do anything by itself — it just defines a pool of claimable addresses. You also need an L2Advertisement resource to tell MetalLB how to announce those addresses (via Layer 2/ARP, in this case) and which pool(s) it applies to.
Append to the metallb_config.yml file:
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb
Full file:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: example
namespace: metallb
spec:
addresses:
- 10.0.0.100-10.0.0.200
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb
cat << EOF > metallb.yml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: example
namespace: metallb
spec:
addresses:
- 10.0.0.100-10.0.0.200
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb
EOF
kubectl apply -f metallb.yml
Verify¶
Create a test LoadBalancer service and confirm it gets an EXTERNAL-IP from the pool instead of staying <pending>:
kubectl expose deployment <some-deployment> --type=LoadBalancer --name=test-lb --port=80
kubectl get svc test-lb
If it stays <pending>, check the controller's logs (kubectl logs -n metallb deploy/metallb-controller) — a common cause is the L2Advertisement not referencing the right pool, or the pool's range already being fully allocated.
See Troubleshooting for the Calico interface conflict and other common post-install issues.