MetalLB - Load Balancer for Bare Metal
Overview
MetalLB is a load-balancer implementation for bare-metal Kubernetes clusters. It provides LoadBalancer-type Services, which are typically only available in cloud environments. For this homelab cluster, MetalLB runs in Layer 2 mode to assign external IP addresses to Services.
Key Features
- LoadBalancer Services: Enables LoadBalancer type on bare-metal
- Layer 2 Mode: Uses ARP to announce IPs on the local network
- IP Address Management: Assigns IPs from configured pools
- Automatic Failover: Moves IPs between nodes if pods are rescheduled
Deployment Details
ArgoCD Application
- Name: metal-lb
- Namespace: metallb-system
- Project: infrastructure
- Sync Wave: -35
- Helm Chart: metallb/metallb v0.15.3
- Auto-Sync: Enabled (prune, selfHeal)
Components
- Controller: Watches Services and assigns IPs
- Speaker DaemonSet: Announces IPs using ARP (Layer 2)
Resources
# Controller resources
controller:
requests:
cpu: 100m
memory: 100Mi
# Speaker (per node - 5 total)
speaker:
requests:
cpu: 100m
memory: 100Mi
Total Resource Usage:
- CPU Requests: 600m (3% of 20 cores)
- Memory Requests: 600Mi (0.7% of 80GB)
Configuration
IP Address Pool
MetalLB manages a pool of IP addresses on the Kubernetes VLAN:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 10.0.10.10-10.0.10.99 # 90 available IPs
autoAssign: true
avoidBuggyIPs: false
Pool Status:
- Total IPs: 90 (10.0.10.10 - 10.0.10.99)
- Assigned IPv4: 1
- Available IPv4: 89
- Subnet: 10.0.10.0/24 (Kubernetes VLAN)
Layer 2 Advertisement
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: first-pool
namespace: metallb-system
spec:
ipAddressPools:
- first-pool
How it Works:
- Speaker pods use ARP to advertise IPs on the local network
- UniFi switch learns MAC addresses and routes traffic
- If a node fails, another speaker takes over the IP
Allocated IP Addresses
| Service | Namespace | IP Address | Ports | Purpose |
|---|---|---|---|---|
| ingress-nginx-controller | ingress-nginx | 10.0.10.10 | 80, 443 | Main ingress controller |
| pi-hole (planned) | pihole | 10.0.0.200 | 53, 80, 443 | DNS/DHCP server |
Operations
Verify MetalLB Status
# Check MetalLB pods
kubectl get pods -n metallb-system
# Expected output:
# NAME READY STATUS
# controller-xxxxxxxxxx-xxxxx 1/1 Running
# speaker-xxxxx 1/1 Running (x5 nodes)
Check IP Pools
# List IP address pools
kubectl get ipaddresspool -n metallb-system
# View pool status
kubectl get ipaddresspool first-pool -n metallb-system -o yaml
# Check available IPs
kubectl get ipaddresspool first-pool -n metallb-system -o jsonpath='{.status}'
List LoadBalancer Services
# All LoadBalancer services across cluster
kubectl get svc -A --field-selector spec.type=LoadBalancer
# Check specific service
kubectl describe svc ingress-nginx-controller -n ingress-nginx
Check Layer 2 Advertisements
# List L2 advertisements
kubectl get l2advertisement -n metallb-system
# View advertisement details
kubectl describe l2advertisement first-pool -n metallb-system
Creating LoadBalancer Services
Example Service
apiVersion: v1
kind: Service
metadata:
name: my-app-loadbalancer
namespace: my-app
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: my-app
Process:
- Service created with
type: LoadBalancer - MetalLB controller assigns next available IP from pool
- Speaker pods announce IP via ARP
- Service accessible at assigned external IP
Request Specific IP
spec:
type: LoadBalancer
loadBalancerIP: 10.0.10.50 # Request specific IP
Note: IP must be within configured pool range.
Troubleshooting
Service Stuck in "Pending"
Symptoms:
kubectl get svc -n my-namespace
# EXTERNAL-IP shows <pending>
Causes & Solutions:
-
No available IPs in pool:
kubectl get ipaddresspool first-pool -n metallb-system -o yaml
# Check: status.availableIPv4Solution: Expand IP pool range or release unused IPs
-
MetalLB controller not running:
kubectl get pods -n metallb-systemSolution: Check controller logs, ensure ArgoCD sync is healthy
-
Service in different namespace than pool:
- MetalLB pools are cluster-wide, this shouldn't be an issue
- Check for namespace selectors in IPAddressPool if configured
IP Not Reachable from Outside Cluster
Symptoms: LoadBalancer IP assigned but not pingable
Causes & Solutions:
-
Speaker pods not running:
kubectl get pods -n metallb-system -l component=speaker
# Should show 5/5 running (one per node) -
ARP not propagating:
# From a client on same network:
arp -a | grep 10.0.10.10Solution: Check network switch configuration, VLAN settings
-
Firewall blocking traffic:
- Verify no iptables rules blocking the IP
- Check UniFi firewall rules for VLAN 10
Multiple Services Getting Same IP
Cause: Services with spec.loadBalancerIP set to same IP
Solution: Remove loadBalancerIP and let MetalLB auto-assign
Layer 2 Mode Limitations
Known Constraints
-
Single Node Handling Traffic:
- Only one node announces the IP at a time
- All traffic flows through that node
- Not true load balancing across nodes
-
Failover Time:
- 10-15 seconds for IP to fail over to another node
- Requires pod rescheduling and ARP cache updates
-
Network Requirements:
- All nodes must be on same Layer 2 network
- Switch must support ARP
- Broadcast domain required
When This Works Well
- Homelab/bare-metal environments ✅
- Small to medium clusters ✅
- Services with multiple pods on single node ✅
When to Consider BGP Mode
- Large clusters with > 10 nodes
- Multi-subnet environments
- True multi-path load balancing requirements
- Faster failover requirements (sub-second)
Monitoring
Metrics
MetalLB exposes Prometheus metrics:
# IP allocation status
metallb_allocator_addresses_in_use_total
metallb_allocator_addresses_total
# Speaker announcements
metallb_speaker_announced
metallb_speaker_layer2_requests_received
# Controller assignments
metallb_k8s_client_update_errors_total
Recommended Alerts
- IP Pool Exhaustion: Alert when available IPs < 10
- Speaker Pod Down: Alert if speaker count < 5
- Controller Errors: Alert on assignment errors
Expanding IP Pool
To add more IPs to the pool:
-
Edit IP Pool in homelab repo:
# manifests/base/metal-lb/ipaddresspool.yaml
spec:
addresses:
- 10.0.10.10-10.0.10.150 # Expand from .99 to .150 -
Commit and create PR
-
ArgoCD syncs automatically after merge
-
Verify:
kubectl get ipaddresspool first-pool -n metallb-system -o yaml
# Check: spec.addresses and status.availableIPv4
Best Practices
- Reserve Static IPs: Don't overlap with DHCP range
- Monitor IP Usage: Set alerts before pool exhaustion
- Consistent Naming: Use descriptive service names
- Avoid Manual IP Assignment: Let MetalLB auto-assign when possible
- Plan for Growth: Leave room in IP pool for future services
Security Considerations
- Network Isolation: LoadBalancer IPs are on Kubernetes VLAN (10.0.10.0/24)
- Firewall Rules: Control access via UniFi firewall (VLAN-level)
- Speaker Permissions: Requires hostNetwork for ARP announcements
- IP Spoofing: Layer 2 mode has no authentication (inherent limitation)
Upgrade Procedure
MetalLB is managed by ArgoCD using Helm:
- Update Chart Version: Edit
manifests/applications/metal-lb.yaml - Check Release Notes: Review breaking changes at MetalLB Release Notes
- Create PR and merge
- ArgoCD Syncs automatically
- Verify: Check pods and service IPs
Note: IP assignments persist across upgrades.
Useful Commands
# MetalLB status
kubectl get pods -n metallb-system
kubectl get ipaddresspool -n metallb-system
kubectl get l2advertisement -n metallb-system
# Services using LoadBalancer
kubectl get svc -A --field-selector spec.type=LoadBalancer
# Controller logs
kubectl logs -n metallb-system deployment/controller -f
# Speaker logs (specific node)
kubectl logs -n metallb-system daemonset/speaker -f
# Pool status
kubectl get ipaddresspool first-pool -n metallb-system -o jsonpath='{.status}' | jq
Resources
- Official Documentation: MetalLB Documentation
- Configuration: Configuration Guide
- Concepts: MetalLB Concepts
- Troubleshooting: Troubleshooting Guide
Related Documentation
- ingress-nginx - Primary consumer of MetalLB LoadBalancer
- ArgoCD - GitOps deployment of MetalLB
Note: For comprehensive network configuration details, see network-info.md in the homelab repository.
Last Updated: 2025-12-27
Status: Production, Healthy
Managed By: ArgoCD (manifests/applications/metal-lb.yaml)