MetalLB Load Balancer for Bare Metal Kubernetes
Deploy and configure MetalLB for load balancing in bare metal Kubernetes clusters with L2 and BGP modes.
When I first configured MetalLB on my k3s bare-metal cluster, I spent an embarrassing amount of time wondering why my LoadBalancer services were stuck in Pending. It turned out that k3s ships with its own built-in ServiceLB (formerly Klipper), and the two were fighting over IP assignments. After disabling the k3s ServiceLB with --disable servicelb and carving out a dedicated IP range on my home network that my router would not hand out via DHCP, everything clicked into place. That initial struggle taught me more about Layer 2 networking and ARP than any textbook ever did, and it fundamentally changed how I think about homelab networking.
Introduction
In cloud environments, creating a LoadBalancer Service is trivial—AWS, GCP, and Azure automatically provision external IPs. But in bare-metal Kubernetes? That same Service sits in Pending forever, waiting for a load balancer that doesn’t exist.
Why MetalLB Matters:
- Real External IPs: Your Services get actual routable IP addresses
- No Cloud Lock-in: Run production-grade networking on your own hardware
- Simple Setup: L2 mode works with any network—no BGP expertise required
- High Availability: Services failover automatically when nodes go down
MetalLB fills this gap by implementing the LoadBalancer Service type for bare-metal clusters. It’s the missing piece that makes homelab Kubernetes feel like a real cloud.
[MetalLB - Concepts] — MetalLB Project , 2024-06-15Architecture Overview
MetalLB assigns IPs from a configured pool and announces them to your network:
flowchart TB
subgraph Network["🌐 Local Network"]
Router["📡 Router\n192.168.1.1"]
Client["💻 Client"]
end
subgraph MetalLB["⚖️ MetalLB"]
Controller["🎛️ Controller\nIP Assignment"]
subgraph Speakers["📢 Speakers (DaemonSet)"]
S1["🔊 Speaker\nNode 1"]
S2["🔊 Speaker\nNode 2"]
S3["🔊 Speaker\nNode 3"]
end
Pool["🏊 IP Pool\n192.168.1.200-250"]
end
subgraph Services["☸️ Kubernetes Services"]
Svc1["🔌 Traefik\n192.168.1.200"]
Svc2["🔌 API\n192.168.1.201"]
end
Client -->|"Request"| Router
Router -->|"ARP"| S1
S1 -->|"Traffic"| Svc1
Controller --> Pool
Pool --> Svc1
Pool --> Svc2
classDef primary fill:#7c3aed,color:#fff
classDef secondary fill:#06b6d4,color:#fff
classDef db fill:#f43f5e,color:#fff
classDef warning fill:#fbbf24,color:#000
class Services primary
class MetalLB secondary
class Network warning
How L2 Mode Works:
- Controller assigns an IP from the pool to your Service
- Speaker pods respond to ARP requests for that IP
- Traffic flows to the elected speaker node, then to your Service
- Failover happens automatically if the speaker node fails
MetalLB provides network load balancer implementations for bare metal Kubernetes clusters. This guide covers deploying MetalLB with L2 mode for homelab and small deployments.
[MetalLB - Layer 2 Mode] — MetalLB Project , 2024-06-15Installation
Helm Deployment
# infrastructure/metallb/helm-release.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: metallb
namespace: metallb-system
spec:
interval: 30m
chart:
spec:
chart: metallb
version: "0.14.x"
sourceRef:
kind: HelmRepository
name: metallb
namespace: flux-system
install:
crds: CreateReplace
remediation:
retries: 3
upgrade:
crds: CreateReplace
values:
controller:
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
speaker:
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
tolerations:
- effect: NoSchedule
key: node-role.kubernetes.io/control-plane
L2 Mode Configuration
IP Address Pool
# infrastructure/metallb/ip-pool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: main-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.250
autoAssign: true
avoidBuggyIPs: true
L2 Advertisement
# infrastructure/metallb/l2-advertisement.yaml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: main-l2
namespace: metallb-system
spec:
ipAddressPools:
- main-pool
interfaces:
- eth0
Multiple Address Pools
Segmented Pools
# infrastructure/metallb/pools.yaml
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: ingress-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.209
autoAssign: false
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: apps-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.210-192.168.1.230
autoAssign: true
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: db-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.240-192.168.1.250
autoAssign: false
Pool Selection by Service
# Service requesting specific pool
apiVersion: v1
kind: Service
metadata:
name: traefik
namespace: traefik-system
annotations:
metallb.universe.tf/address-pool: ingress-pool
spec:
type: LoadBalancer
ports:
- name: web
port: 80
targetPort: 8000
- name: websecure
port: 443
targetPort: 8443
Sharing IPs
Multiple Services on One IP
# Service 1
apiVersion: v1
kind: Service
metadata:
name: traefik-web
annotations:
metallb.universe.tf/allow-shared-ip: "traefik-shared"
metallb.universe.tf/loadBalancerIPs: "192.168.1.200"
spec:
type: LoadBalancer
ports:
- name: http
port: 80
targetPort: 8000
- name: https
port: 443
targetPort: 8443
---
# Service 2 sharing same IP
apiVersion: v1
kind: Service
metadata:
name: traefik-dashboard
annotations:
metallb.universe.tf/allow-shared-ip: "traefik-shared"
metallb.universe.tf/loadBalancerIPs: "192.168.1.200"
spec:
type: LoadBalancer
ports:
- name: dashboard
port: 9000
targetPort: 9000
Requesting Specific IPs
Static IP Assignment
apiVersion: v1
kind: Service
metadata:
name: postgres-external
namespace: data-layer
annotations:
metallb.universe.tf/address-pool: db-pool
spec:
type: LoadBalancer
loadBalancerIP: 192.168.1.240
ports:
- port: 5432
targetPort: 5432
BGP Mode Configuration
[MetalLB - BGP Mode] — MetalLB Project , 2024-06-15BGP Peer Configuration
# infrastructure/metallb/bgp-peer.yaml
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: router-peer
namespace: metallb-system
spec:
myASN: 64500
peerASN: 64501
peerAddress: 192.168.1.1
peerPort: 179
password: "bgp-secret"
holdTime: 90s
keepaliveTime: 30s
BGP Advertisement
# infrastructure/metallb/bgp-advertisement.yaml
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: main-bgp
namespace: metallb-system
spec:
ipAddressPools:
- main-pool
aggregationLength: 32
localPref: 100
communities:
- 64500:100
Service Configuration
Traefik LoadBalancer Service
# apps/traefik/service.yaml
apiVersion: v1
kind: Service
metadata:
name: traefik
namespace: traefik-system
annotations:
metallb.universe.tf/address-pool: ingress-pool
metallb.universe.tf/loadBalancerIPs: "192.168.1.200"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
ports:
- name: web
port: 80
targetPort: 8000
protocol: TCP
- name: websecure
port: 443
targetPort: 8443
protocol: TCP
selector:
app.kubernetes.io/name: traefik
Monitoring
Prometheus ServiceMonitor
# infrastructure/metallb/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: metallb
namespace: metallb-system
spec:
selector:
matchLabels:
app.kubernetes.io/name: metallb
endpoints:
- port: metrics
interval: 30s
Key Metrics
| Metric | Description |
|---|---|
metallb_bgp_session_up | BGP session status |
metallb_bgp_announced_prefixes_total | Announced route count |
metallb_allocator_addresses_total | Pool address capacity |
metallb_allocator_addresses_in_use_total | Addresses currently assigned |
Troubleshooting
Common Issues
# Check controller logs
kubectl logs -n metallb-system -l app.kubernetes.io/component=controller
# Check speaker logs (L2 mode ARP)
kubectl logs -n metallb-system -l app.kubernetes.io/component=speaker
# Verify IP pool assignments
kubectl get ipaddresspools -n metallb-system -o yaml
# Check service IP allocation
kubectl get svc -A -o wide | grep LoadBalancer
ARP Debugging (L2 Mode)
[Bare-metal considerations for Kubernetes] — Kubernetes , 2024-09-01# On a network machine, check ARP for service IP
arp -n | grep 192.168.1.200
# Check which node is advertising
kubectl logs -n metallb-system -l app.kubernetes.io/component=speaker | grep -i "192.168.1.200"
Summary
MetalLB configuration components:
| Resource | Purpose |
|---|---|
| IPAddressPool | Define available IP ranges |
| L2Advertisement | Enable L2/ARP mode |
| BGPPeer | Configure BGP routing |
| BGPAdvertisement | Control BGP announcements |
| Service Annotations | Pool selection, IP sharing |
MetalLB enables production-grade load balancing for bare metal Kubernetes without cloud provider dependencies.
Looking back, MetalLB was one of the first components I deployed that made my homelab cluster feel like a real production environment. The ability to point a browser at a stable IP address and reach my services — without NodePort hacks or manual iptables rules — was a turning point. If you are running bare-metal Kubernetes of any kind, MetalLB is essential infrastructure.
[MetalLB Installation Guide] — MetalLB Project , 2024-06-15 [A Guide to MetalLB in Layer 2 Mode] — Adaltas , 2022-09-08Next Steps
- Configure Traefik Ingress to use MetalLB-assigned IPs for HTTP routing
- Set up Prometheus monitoring for MetalLB metrics to catch pool exhaustion early
- Explore BGP mode if your router supports it for true load distribution across nodes
Further Reading
[MetalLB Official Documentation] — MetalLB Authors , 2024 [Kubernetes Service Types] — Kubernetes Authors , 2024 [k3s Networking] — Rancher / SUSE , 2024 []title=“MetalLB Documentation” url=“https://metallb.io/” author=“MetalLB Project” date=“2024-06-15”