- 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.