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

  1. Controller: Watches for Service changes and assigns IPs
  2. Speaker: Runs on each node, handles BGP peering and IP advertisement
  3. 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.