Andrew Mercer
on this page
  • https://metallb.universe.tf/installation/
  • https://github.com/kubernetes/ingress-nginx/blob/main/docs/deploy/baremetal.md#a-pure-software-solution-metallb

Concepts

IP address allocation

  • https://metallb.universe.tf/concepts/#address-allocation

MetalLB needs a pool of LAN addresses to hand out to LoadBalancer services. It doesn't create IPs. It claims and announces the ones you give it, so the range must be outside your router's DHCP scope and unused by anything else. This guide uses 10.0.0.100-10.0.0.200.

Layer 2 mode

  • https://metallb.universe.tf/concepts/layer2/

In Layer 2 mode, one node at a time owns each service IP and answers ARP requests for it. All traffic for that IP enters through that node, and another node takes over if it goes down. It's simpler than BGP mode, but there's no network-level load balancing across nodes for a single IP.

Requirements

  • https://metallb.universe.tf/#requirements

Check network addon compatibility before installing. Calico in particular needs some care alongside MetalLB; see Troubleshooting MetalLB.

1. Enable strictARP (IPVS mode only)

Skip this step if kube-proxy runs in the default iptables mode. In IPVS mode, kube-proxy must have strict ARP enabled, or MetalLB's speaker can't claim ARP responses. kube-router enables it by default.

Check the mode:

kubectl get configmap kube-proxy -n kube-system -o yaml | grep -E 'mode:|strictARP'

Flip strictARP to true, previewing the change first. The sed only changes false to true, so it's safe to re-run:

# preview
kubectl get configmap kube-proxy -n kube-system -o yaml | \
  sed -e "s/strictARP: false/strictARP: true/" | \
  kubectl diff -f - -n kube-system

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

# restart kube-proxy so it picks up the change
kubectl -n kube-system rollout restart daemonset kube-proxy

2. Create the namespace

The MetalLB speaker manipulates ARP and network interfaces directly, so its namespace needs the privileged Pod Security Admission level. The default baseline/restricted levels would block it.

cat <<EOF > metallb-namespace.yaml
apiVersion: v1
kind: Namespace
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-namespace.yaml

Upstream manifests use a namespace called metallb-system. This guide uses metallb, so adjust if you cross-reference upstream examples.

If the namespace already exists without the labels, add them, testing with --dry-run=server first:

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

# then the same command without --dry-run=server
  • https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/

3. Install with Helm

helm repo add metallb https://metallb.github.io/metallb
helm repo update
helm install metallb metallb/metallb -n metallb

Run helm repo update even if the repo was added a while ago. Otherwise Helm installs whatever chart version it cached back then.

kubectl get pods -n metallb

You should see one controller pod and one speaker pod per node (a DaemonSet). Depending on the chart version and values, the FRR-K8s components may also be deployed. They don't need BGP to be configured for Layer 2 use.

4. Configure the address pool and L2 advertisement

  • https://metallb.universe.tf/configuration/#layer-2-configuration
  • https://github.com/metallb/metallb/tree/main/configsamples

An IPAddressPool only defines which addresses can be claimed. An L2Advertisement tells MetalLB to announce them over ARP. Without one, services receive IPs that nothing on the LAN can reach.

cat <<EOF > metallb-config.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default-pool
  namespace: metallb
spec:
  addresses:
    - 10.0.0.100-10.0.0.200
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default-l2
  namespace: metallb
spec:
  ipAddressPools:
    - default-pool
EOF

kubectl apply -f metallb-config.yaml

Leaving ipAddressPools out of the L2Advertisement makes it announce every pool.

5. Verify

Create a test LoadBalancer service and confirm it gets an EXTERNAL-IP from the pool:

kubectl create deployment lb-test --image=nginx
kubectl expose deployment lb-test --type=LoadBalancer --port=80
kubectl get svc lb-test
curl http://<external-ip>/

Clean up:

kubectl delete svc,deployment lb-test

If the service stays <pending>, check the controller logs (kubectl logs -n metallb deploy/metallb-controller). The usual causes are a missing L2Advertisement or an exhausted pool. See Troubleshooting MetalLB.