MetalLB BGP Configuration for Bare Metal Kubernetes: Enterprise Load Balancer Guide
MetalLB brings cloud-provider LoadBalancer functionality to bare metal Kubernetes clusters, enabling the use of LoadBalancer-type Services without relying on external cloud infrastructure. Operating in either Layer 2 (ARP/NDP) or BGP mode, MetalLB provides enterprise-grade load balancing capabilities essential for on-premises and edge Kubernetes deployments.
This comprehensive guide focuses on BGP mode configuration, which provides superior scalability, true load balancing across multiple nodes, and integration with existing network infrastructure. We’ll cover installation, BGP peering configuration, advanced routing scenarios, high availability patterns, and production-tested practices for enterprise bare metal Kubernetes environments.
MetalLB BGP Configuration for Bare Metal Kubernetes
Executive Summary
MetalLB solves a critical gap in bare metal Kubernetes deployments by providing LoadBalancer service type support typically available only in cloud environments. While Layer 2 mode offers simplicity, BGP mode provides enterprise-grade capabilities including true multi-path load balancing, integration with existing network infrastructure, and scalability to thousands of services.
In this guide, we’ll explore MetalLB’s BGP architecture, advanced configuration patterns, integration with network routers (Cisco, Juniper, Arista), high availability strategies, and monitoring approaches that enable production-grade load balancing in bare metal environments.
MetalLB Architecture Overview
Operating Modes
Layer 2 Mode:
- Uses ARP/NDP for service IP advertisement
- Single node handles all traffic (no true load balancing)
- Simple configuration, no router integration required
- Limited scalability
BGP Mode (Focus of this guide):
- Uses BGP to advertise service IPs to network routers
- True load balancing with ECMP (Equal-Cost Multi-Path)
- Scalable to thousands of services
- Integrates with existing network infrastructure
- Supports advanced routing policies
Core Components
- Controller: Watches for Service changes and assigns IPs
- Speaker: Runs on each node, handles BGP peering and IP advertisement
- ConfigMap/CRDs: Configuration for IP address pools and BGP peers
Prerequisites
Network Requirements
- BGP-capable routers or switches in your infrastructure
- IP address pool(s) for LoadBalancer services
- ASN (Autonomous System Number) for your Kubernetes cluster
- Routing capability between router and Kubernetes nodes
Kubernetes Requirements
# Kubernetes 1.22+
kubectl version
# Network plugin that supports BGP
# Calico, Cilium, or standard CNI plugins work with MetalLB
Installing MetalLB
Installation via Manifest
# Apply MetalLB namespace and controller
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.3/config/manifests/metallb-native.yaml
# Verify installation
kubectl get pods -n metallb-system
kubectl get daemonset -n metallb-system
Installation via Helm
# Add MetalLB Helm repository
helm repo add metallb https://metallb.github.io/metallb
helm repo update
# Create namespace
kubectl create namespace metallb-system
# Install MetalLB
helm install metallb metallb/metallb \
--namespace metallb-system \
--values metallb-values.yaml
Create metallb-values.yaml:
# MetalLB Helm Values
controller:
# Resource configuration
resources:
limits:
cpu: "200m"
memory: "256Mi"
requests:
cpu: "100m"
memory: "128Mi"
# Node selection
nodeSelector:
node-role.kubernetes.io/control-plane: ""
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
# High availability
replicas: 2
speaker:
# Resource configuration
resources:
limits:
cpu: "200m"
memory: "256Mi"
requests:
cpu: "100m"
memory: "128Mi"
# Speaker must run on all nodes
tolerations:
- effect: NoSchedule
operator: Exists
# FRR (Free Range Routing) mode for BGP
frr:
enabled: true
# Logging
logLevel: info
# Enable Prometheus metrics
prometheus:
serviceAccount: metallb-system
namespace: metallb-system
podMonitor:
enabled: true
prometheusRule:
enabled: true
Basic BGP Configuration
IP Address Pool Configuration
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production-pool
namespace: metallb-system
spec:
addresses:
# Single IP range
- 192.168.1.240/28 # 192.168.1.240 - 192.168.1.255
# Multiple IP ranges
- 10.0.100.0/24
- 10.0.101.0/24
# Individual IPs
- 192.168.2.100/32
- 192.168.2.101/32
- 192.168.2.102/32
# Auto-assign IPs from this pool by default
autoAssign: true
# Avoid buggy IP ranges if needed
avoidBuggyIPs: false
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: development-pool
namespace: metallb-system
spec:
addresses:
- 10.0.200.0/24
autoAssign: false # Require explicit pool selection
Basic BGP Peering Configuration
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: router1
namespace: metallb-system
spec:
# Router IP address
peerAddress: 192.168.1.1
peerASN: 65000
myASN: 65001
# Optional: Specify which nodes should peer
# nodeSelectors:
# - matchLabels:
# kubernetes.io/hostname: node1
# BGP router ID (optional, defaults to node IP)
# routerID: 192.168.1.10
# Hold time
holdTime: 90s
# Keepalive time
keepaliveTime: 30s
---
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: router2
namespace: metallb-system
spec:
peerAddress: 192.168.1.2
peerASN: 65000
myASN: 65001
holdTime: 90s
keepaliveTime: 30s
BGP Advertisement Configuration
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: default-advertisement
namespace: metallb-system
spec:
# Advertise IPs from these pools
ipAddressPools:
- production-pool
- development-pool
# Optional: Communities
communities:
- 65000:100 # Production traffic
- no-advertise # Don't advertise to external peers
# Optional: Aggregation length for route summarization
aggregationLength: 32 # /32 for individual IPs
# Optional: Local preference
localPref: 100
# Optional: Peer selection
# peers:
# - router1
Advanced BGP Configuration
Multi-Homing with Route Redundancy
# Multiple routers for redundancy
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: core-router-1
namespace: metallb-system
spec:
peerAddress: 10.0.0.1
peerASN: 65000
myASN: 65001
holdTime: 90s
keepaliveTime: 30s
nodeSelectors:
- matchLabels:
metallb.universe.tf/peer-group: core-routers
---
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: core-router-2
namespace: metallb-system
spec:
peerAddress: 10.0.0.2
peerASN: 65000
myASN: 65001
holdTime: 90s
keepaliveTime: 30s
nodeSelectors:
- matchLabels:
metallb.universe.tf/peer-group: core-routers
---
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: edge-router-1
namespace: metallb-system
spec:
peerAddress: 10.0.1.1
peerASN: 65000
myASN: 65001
holdTime: 90s
keepaliveTime: 30s
nodeSelectors:
- matchLabels:
metallb.universe.tf/peer-group: edge-routers
BGP Communities for Traffic Engineering
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: production-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- production-pool
communities:
# Standard communities
- 65000:100 # Production traffic
- 65000:1000 # High priority
# Well-known communities
- no-export # Don't advertise to EBGP peers
# - no-advertise # Don't advertise to any peer
# - no-export-subconfed # Don't advertise outside confederation
localPref: 200 # Higher local preference (preferred path)
aggregationLength: 24 # Advertise /24 instead of /32
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: development-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- development-pool
communities:
- 65000:200 # Development traffic
- 65000:500 # Lower priority
localPref: 100 # Standard local preference
Per-Node BGP Configuration
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: node1-router
namespace: metallb-system
spec:
peerAddress: 192.168.1.1
peerASN: 65000
myASN: 65001
nodeSelectors:
- matchLabels:
kubernetes.io/hostname: node1
---
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: node2-router
namespace: metallb-system
spec:
peerAddress: 192.168.2.1
peerASN: 65000
myASN: 65001
nodeSelectors:
- matchLabels:
kubernetes.io/hostname: node2
---
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: node3-router
namespace: metallb-system
spec:
peerAddress: 192.168.3.1
peerASN: 65000
myASN: 65001
nodeSelectors:
- matchLabels:
kubernetes.io/hostname: node3
Router Integration Examples
Cisco IOS/IOS-XE Configuration
! Configure BGP
router bgp 65000
bgp log-neighbor-changes
bgp graceful-restart
! Peer with Kubernetes nodes
neighbor 10.0.10.1 remote-as 65001
neighbor 10.0.10.1 description k8s-node1
neighbor 10.0.10.1 timers 30 90
neighbor 10.0.10.2 remote-as 65001
neighbor 10.0.10.2 description k8s-node2
neighbor 10.0.10.2 timers 30 90
neighbor 10.0.10.3 remote-as 65001
neighbor 10.0.10.3 description k8s-node3
neighbor 10.0.10.3 timers 30 90
! Address family configuration
address-family ipv4
neighbor 10.0.10.1 activate
neighbor 10.0.10.1 soft-reconfiguration inbound
neighbor 10.0.10.2 activate
neighbor 10.0.10.2 soft-reconfiguration inbound
neighbor 10.0.10.3 activate
neighbor 10.0.10.3 soft-reconfiguration inbound
! Enable ECMP
maximum-paths 4
exit-address-family
!
! Enable ECMP load balancing
ip cef load-sharing algorithm universal
Juniper JunOS Configuration
# Configure BGP
set protocols bgp group kubernetes type external
set protocols bgp group kubernetes peer-as 65001
set protocols bgp group kubernetes local-as 65000
# Peer configuration
set protocols bgp group kubernetes neighbor 10.0.10.1 description k8s-node1
set protocols bgp group kubernetes neighbor 10.0.10.2 description k8s-node2
set protocols bgp group kubernetes neighbor 10.0.10.3 description k8s-node3
# Timers
set protocols bgp group kubernetes hold-time 90
set protocols bgp group kubernetes keepalive 30
# Enable ECMP
set routing-options forwarding-table export load-balance-policy
set policy-options policy-statement load-balance-policy then load-balance per-packet
# BGP policy
set policy-options policy-statement accept-k8s term 1 from protocol bgp
set policy-options policy-statement accept-k8s term 1 from neighbor 10.0.10.0/24
set policy-options policy-statement accept-k8s term 1 then accept
Arista EOS Configuration
! Configure BGP
router bgp 65000
router-id 10.0.0.1
bgp listen range 10.0.10.0/24 peer-group kubernetes remote-as 65001
! Peer group configuration
neighbor kubernetes peer group
neighbor kubernetes maximum-routes 1000
neighbor kubernetes send-community extended
neighbor kubernetes timers 30 90
! Enable ECMP
maximum-paths 4 ecmp 4
! Address family
address-family ipv4
neighbor kubernetes activate
neighbor kubernetes next-hop-self
!
!
! ECMP load balancing
ip routing
hardware tcam profile vxlan-routing
service routing protocols model multi-agent
Free Range Routing (FRR) Configuration
! Configure BGP on Linux router
router bgp 65000
bgp router-id 10.0.0.1
bgp log-neighbor-changes
bgp graceful-restart
! Kubernetes peers
neighbor 10.0.10.1 remote-as 65001
neighbor 10.0.10.1 description k8s-node1
neighbor 10.0.10.1 timers 30 90
neighbor 10.0.10.2 remote-as 65001
neighbor 10.0.10.2 description k8s-node2
neighbor 10.0.10.2 timers 30 90
neighbor 10.0.10.3 remote-as 65001
neighbor 10.0.10.3 description k8s-node3
neighbor 10.0.10.3 timers 30 90
! Address family configuration
address-family ipv4 unicast
neighbor 10.0.10.1 activate
neighbor 10.0.10.1 soft-reconfiguration inbound
neighbor 10.0.10.2 activate
neighbor 10.0.10.2 soft-reconfiguration inbound
neighbor 10.0.10.3 activate
neighbor 10.0.10.3 soft-reconfiguration inbound
! Enable ECMP
maximum-paths 4
exit-address-family
!
Service Configuration Examples
Basic LoadBalancer Service
apiVersion: v1
kind: Service
metadata:
name: web-service
namespace: default
spec:
type: LoadBalancer
selector:
app: web
ports:
- protocol: TCP
port: 80
targetPort: 8080
# MetalLB will auto-assign an IP from default pool
LoadBalancer with Specific IP
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: default
spec:
type: LoadBalancer
loadBalancerIP: 192.168.1.240 # Request specific IP
selector:
app: api
ports:
- protocol: TCP
port: 443
targetPort: 8443
LoadBalancer with Pool Selection
apiVersion: v1
kind: Service
metadata:
name: database-service
namespace: production
annotations:
metallb.universe.tf/address-pool: production-pool
spec:
type: LoadBalancer
selector:
app: postgresql
ports:
- protocol: TCP
port: 5432
targetPort: 5432
LoadBalancer with Shared IP
# HTTP Service
apiVersion: v1
kind: Service
metadata:
name: http-service
namespace: default
annotations:
metallb.universe.tf/allow-shared-ip: "shared-web"
spec:
type: LoadBalancer
selector:
app: web
ports:
- name: http
protocol: TCP
port: 80
targetPort: 8080
---
# HTTPS Service (shares same IP)
apiVersion: v1
kind: Service
metadata:
name: https-service
namespace: default
annotations:
metallb.universe.tf/allow-shared-ip: "shared-web"
spec:
type: LoadBalancer
selector:
app: web
ports:
- name: https
protocol: TCP
port: 443
targetPort: 8443
LoadBalancer with Traffic Policy
apiVersion: v1
kind: Service
metadata:
name: external-service
namespace: default
spec:
type: LoadBalancer
externalTrafficPolicy: Local # Preserve source IP
selector:
app: external-api
ports:
- protocol: TCP
port: 80
targetPort: 8080
High Availability Configuration
Multi-Router Setup
# Primary router peering
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: primary-router
namespace: metallb-system
spec:
peerAddress: 10.0.0.1
peerASN: 65000
myASN: 65001
holdTime: 90s
keepaliveTime: 30s
---
# Secondary router peering
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: secondary-router
namespace: metallb-system
spec:
peerAddress: 10.0.0.2
peerASN: 65000
myASN: 65001
holdTime: 90s
keepaliveTime: 30s
---
# Advertisement with both routers
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: ha-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- production-pool
peers:
- primary-router
- secondary-router
Active-Standby Configuration
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: active-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- production-pool
localPref: 200 # Higher preference
peers:
- primary-router
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: standby-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- production-pool
localPref: 100 # Lower preference (standby)
peers:
- secondary-router
Monitoring and Troubleshooting
Prometheus Metrics
apiVersion: v1
kind: Service
metadata:
name: metallb-metrics
namespace: metallb-system
labels:
app: metallb
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "7472"
spec:
type: ClusterIP
ports:
- name: metrics
port: 7472
targetPort: 7472
selector:
app: metallb
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: metallb
namespace: metallb-system
spec:
jobLabel: metallb
selector:
matchLabels:
app: metallb
endpoints:
- port: metrics
interval: 30s
Grafana Dashboard
apiVersion: v1
kind: ConfigMap
metadata:
name: metallb-dashboard
namespace: monitoring
labels:
grafana_dashboard: "1"
data:
metallb-dashboard.json: |
{
"dashboard": {
"title": "MetalLB BGP Metrics",
"panels": [
{
"title": "BGP Session Status",
"targets": [
{
"expr": "metallb_bgp_session_up"
}
]
},
{
"title": "BGP Updates Sent",
"targets": [
{
"expr": "rate(metallb_bgp_updates_total[5m])"
}
]
},
{
"title": "IP Addresses Allocated",
"targets": [
{
"expr": "metallb_allocator_addresses_in_use_total"
}
]
}
]
}
}
Troubleshooting Commands
#!/bin/bash
# metallb-troubleshoot.sh
echo "=== MetalLB BGP Troubleshooting ==="
# Check MetalLB pods
echo -e "\n1. Pod Status:"
kubectl get pods -n metallb-system
# Check BGP configuration
echo -e "\n2. BGP Peers:"
kubectl get bgppeers -n metallb-system
echo -e "\n3. IP Address Pools:"
kubectl get ipaddresspool -n metallb-system
echo -e "\n4. BGP Advertisements:"
kubectl get bgpadvertisement -n metallb-system
# Check speaker logs
echo -e "\n5. Speaker Logs (BGP):"
kubectl logs -n metallb-system -l app=metallb,component=speaker --tail=50
# Check controller logs
echo -e "\n6. Controller Logs:"
kubectl logs -n metallb-system -l app=metallb,component=controller --tail=50
# Check LoadBalancer services
echo -e "\n7. LoadBalancer Services:"
kubectl get svc --all-namespaces -o wide | grep LoadBalancer
# BGP session status (requires access to speaker pod)
echo -e "\n8. BGP Session Status:"
SPEAKER_POD=$(kubectl get pods -n metallb-system -l component=speaker -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n metallb-system $SPEAKER_POD -- vtysh -c "show bgp summary"
# BGP routes
echo -e "\n9. BGP Routes:"
kubectl exec -n metallb-system $SPEAKER_POD -- vtysh -c "show bgp ipv4"
BGP Session Verification
#!/bin/bash
# verify-bgp-sessions.sh
NAMESPACE="metallb-system"
# Get all speaker pods
SPEAKER_PODS=$(kubectl get pods -n $NAMESPACE -l component=speaker -o jsonpath='{.items[*].metadata.name}')
for POD in $SPEAKER_PODS; do
echo "=== BGP Status on $POD ==="
# BGP summary
echo "BGP Summary:"
kubectl exec -n $NAMESPACE $POD -- vtysh -c "show bgp summary"
# BGP neighbors
echo -e "\nBGP Neighbors:"
kubectl exec -n $NAMESPACE $POD -- vtysh -c "show bgp neighbors"
# Advertised routes
echo -e "\nAdvertised Routes:"
kubectl exec -n $NAMESPACE $POD -- vtysh -c "show bgp ipv4 unicast"
echo -e "\n---\n"
done
Production Best Practices
Resource Allocation
# Controller resources
controller:
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"
# Speaker resources (per node)
speaker:
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"
Network Segmentation
# Separate pools for different environments
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production
namespace: metallb-system
spec:
addresses:
- 10.0.100.0/24
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: staging
namespace: metallb-system
spec:
addresses:
- 10.0.101.0/24
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: development
namespace: metallb-system
spec:
addresses:
- 10.0.102.0/24
Security Considerations
# Limit BGP peering to specific nodes
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: secure-router
namespace: metallb-system
spec:
peerAddress: 10.0.0.1
peerASN: 65000
myASN: 65001
# Only peer from designated nodes
nodeSelectors:
- matchLabels:
metallb.universe.tf/bgp-peer: "allowed"
node-role.kubernetes.io/bgp: "true"
# MD5 authentication (requires router support)
password: "$(kubectl get secret bgp-auth -o jsonpath='{.data.password}' | base64 -d)"
Operational Procedures
#!/bin/bash
# metallb-operations.sh
# Gracefully drain a node before maintenance
drain_node() {
local NODE=$1
echo "Draining node $NODE..."
# Cordon the node
kubectl cordon $NODE
# Wait for BGP sessions to reconverge
sleep 30
# Drain the node
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data
echo "Node $NODE drained. BGP sessions should be redistributed."
}
# Add a new BGP peer
add_bgp_peer() {
local PEER_IP=$1
local PEER_ASN=$2
cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: router-${PEER_IP//./-}
namespace: metallb-system
spec:
peerAddress: $PEER_IP
peerASN: $PEER_ASN
myASN: 65001
holdTime: 90s
keepaliveTime: 30s
EOF
echo "BGP peer $PEER_IP added."
}
# Show usage
case "$1" in
drain) drain_node "$2" ;;
add-peer) add_bgp_peer "$2" "$3" ;;
*) echo "Usage: $0 {drain NODE|add-peer IP ASN}" ;;
esac
Conclusion
MetalLB with BGP provides enterprise-grade load balancing capabilities for bare metal Kubernetes clusters, enabling the same LoadBalancer service type available in cloud environments. BGP mode offers superior scalability, true multi-path load balancing with ECMP, and seamless integration with existing network infrastructure.
Key advantages of MetalLB BGP mode:
- True load balancing across multiple nodes via ECMP
- Scalability to thousands of services
- Integration with existing network routers and infrastructure
- Support for advanced routing policies and traffic engineering
- High availability through redundant BGP peering
- Production-grade performance and reliability
By following the configurations and best practices in this guide, organizations can deploy robust load balancing solutions for on-premises and edge Kubernetes environments that match or exceed cloud provider capabilities while maintaining full control over their network infrastructure.