What is MetalLB?
MetalLB is a network load balancer implementation for bare-metal Kubernetes clusters. In cloud environments like AWS, GCP, or Azure, when you create a Kubernetes Service of type LoadBalancer, the cloud provider automatically provisions an external load balancer and assigns a public IP address. On bare-metal (on-premises) clusters, there is no such integration out of the box. MetalLB fills this gap by providing a network load balancer that runs entirely within your cluster, making services of type LoadBalancer fully functional without any external cloud provider dependency.
MetalLB operates purely at the network level. It does not create pods, does not proxy traffic at Layer 7, and does not perform TLS termination. Instead, it works by announcing IP addresses to your local network through standard protocols, then responding to ARP requests (Layer 2 mode) or BGP advertisements (BGP mode) to direct traffic to the correct nodes in your cluster.
Why MetalLB Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →For teams running Kubernetes on-premises, in data centers, or on edge devices, the inability to use LoadBalancer services has traditionally been a significant limitation. Without MetalLB, you are forced to work around the problem using NodePort services, ingress controllers with manual DNS configuration, or complex external load balancer setups. MetalLB solves this elegantly:
- Native Kubernetes Experience: You can define a Service as
type: LoadBalancerand get an external IP assigned automatically, just like in the cloud. - No External Dependencies: MetalLB runs entirely inside your cluster. No need for a separate hardware load balancer or cloud account.
- Simple or Advanced Routing: Layer 2 mode works with any standard network (just a few IPs and a switch), while BGP mode integrates with existing router infrastructure for advanced traffic distribution.
- High Availability: MetalLB supports failover between nodes, ensuring your services remain reachable even if individual nodes go down.
- IP Address Pool Management: You define ranges of IP addresses that MetalLB can assign, keeping your network administration clean and predictable.
How MetalLB Works
MetalLB has two main components that work together:
- Controller: Runs as a Deployment, watches for Services of type LoadBalancer, and assigns them IP addresses from configured pools.
- Speaker: Runs as a DaemonSet on every node, announces the assigned IP addresses using either ARP/NDP (Layer 2) or BGP, and handles traffic routing.
The controller is responsible for IP allocation. When you create a LoadBalancer service, the controller picks an available IP from the pool, assigns it to the service, and updates the service's status.loadBalancer.ingress field. The speaker component then takes over, advertising that IP from the node(s) where the service's endpoints are running.
Layer 2 Mode
In Layer 2 mode, the speaker on the leader node responds to ARP requests (IPv4) or NDP requests (IPv6) for the LoadBalancer IP. All traffic for that IP goes to the leader node, which then forwards it to the appropriate pod. If the leader node fails, another node takes over within seconds through a failover mechanism. This mode is extremely simple to set up—it requires zero configuration on your network hardware—but it does concentrate all traffic for a given service on a single node, which can become a bottleneck for high-throughput services.
BGP Mode
In BGP mode, every speaker node establishes BGP peering sessions with your network routers. When a LoadBalancer service is created, all nodes that host its endpoints advertise the service IP to the routers via BGP. The routers then perform ECMP (Equal-Cost Multi-Path) load balancing across all those nodes, distributing traffic evenly. This mode scales much better than Layer 2 and integrates with existing BGP infrastructure, but requires BGP-capable routers and more complex configuration.
Installation Guide
MetalLB is installed directly into your cluster using standard Kubernetes manifests or Helm. The recommended approach is via the official manifests. Here is the complete installation process:
Step 1: Prepare Your Cluster
Before installing MetalLB, ensure that kube-proxy is operating in IPVS mode (recommended for performance) or iptables mode. If you use kube-proxy in IPVS mode, you must enable strict ARP mode. For kubeproxy in iptables mode, no special preparation is needed.
# Check current kube-proxy mode
kubectl get configmap kube-proxy -n kube-system -o yaml | grep mode
# If using IPVS, set strictARP to true
kubectl get configmap kube-proxy -n kube-system -o yaml | \
sed -e "s/strictARP: false/strictARP: true/" | \
kubectl apply -f - -n kube-system
Step 2: Install MetalLB via Native Manifest
MetalLB provides a single-file manifest that installs everything you need: namespace, RBAC rules, controller deployment, and speaker daemonset.
# Install the latest stable version (adjust the version tag as needed)
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml
Verify that all components are running:
kubectl get pods -n metallb-system
# Expected output:
# NAME READY STATUS RESTARTS AGE
# controller-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
# speaker-xxxxx 1/1 Running 0 30s
# speaker-yyyyy 1/1 Running 0 30s
# ... (one speaker per node)
Step 3: Define IP Address Pools
MetalLB needs at least one IP address pool to allocate addresses from. You define pools using the IPAddressPool custom resource. Here is a complete example that reserves a range of IPs for LoadBalancer services:
# ipaddresspool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: main-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.240-192.168.1.250
- 192.168.2.0/28
autoAssign: true
avoidBuggyIPs: true
Apply the pool configuration:
kubectl apply -f ipaddresspool.yaml
The addresses field accepts CIDR notation, IP ranges (start-end), or individual IPs. The autoAssign field (default true) allows MetalLB to automatically pick IPs for new services. Setting it to false means services must explicitly request IPs from this pool via annotations. The avoidBuggyIPs option prevents allocation of addresses ending in .0 or .255, which can cause issues with some network equipment.
Step 4: Configure Advertisement (L2Advertisement or BGPAdvertisement)
You must tell MetalLB how to announce the allocated IPs. For Layer 2 mode (simplest setup), create an L2Advertisement resource:
# l2advertisement.yaml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-adv
namespace: metallb-system
spec:
ipAddressPools:
- main-pool
# Optional: restrict to specific interfaces
interfaces:
- eth0
- bond0
Apply it:
kubectl apply -f l2advertisement.yaml
For BGP mode, you would create a BGPAdvertisement and a BGPPeer resource instead. We will cover BGP mode in detail later in this guide.
Testing Your Setup
Once MetalLB is installed and configured, you can test it by creating a simple LoadBalancer service:
# test-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-test
spec:
replicas: 3
selector:
matchLabels:
app: nginx-test
template:
metadata:
labels:
app: nginx-test
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: LoadBalancer
selector:
app: nginx-test
ports:
- port: 80
targetPort: 80
Apply and check the assigned IP:
kubectl apply -f test-deployment.yaml
kubectl get service nginx-service
# Expected output:
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# nginx-service LoadBalancer 10.96.x.x 192.168.1.240 80:30080/TCP 5s
If you see an IP in the EXTERNAL-IP column from your configured pool, MetalLB is working correctly. You can now curl that IP from any machine on the same network:
curl http://192.168.1.240
# Should return the default Nginx welcome page
BGP Mode: Complete Configuration
BGP mode offers superior load distribution by advertising service IPs to your routers. Here is a full BGP setup example assuming you have BGP-capable routers (like FRRouting, VyOS, or hardware routers from Cisco/Juniper) with AS number 64512.
Step 1: Create BGPPeer Resource
# bgppeer.yaml
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: router-peer
namespace: metallb-system
spec:
myASN: 64500
peerASN: 64512
peerAddress: 10.0.0.1
# Optional: source address for peering
sourceAddress: 10.0.0.2
# Optional: MD5 password for BGP session authentication
routerID: 10.0.0.2
holdTime: 90s
keepaliveTime: 30s
bfdProfile: fast-bfd
Step 2: Create BFDProfile (Optional but Recommended)
BFD (Bidirectional Forwarding Detection) enables sub-second failure detection, dramatically improving failover speed compared to standard BGP timers.
# bfdprofile.yaml
apiVersion: metallb.io/v1beta1
kind: BFDProfile
metadata:
name: fast-bfd
namespace: metallb-system
spec:
receiveInterval: 300ms
transmitInterval: 300ms
detectMultiplier: 3
echoInterval: 50ms
echoMode: true
passiveMode: false
minimumTtl: 254
Step 3: Create BGPAdvertisement
# bgpadvertisement.yaml
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: bgp-adv
namespace: metallb-system
spec:
ipAddressPools:
- main-pool
# Optional: community strings for traffic engineering
communities:
- "64500:100"
# Optional: local preference
localPref: 100
# Optional: aggregate length for route summarization
aggregationLength: 32
# Optional: enable graceful restart capability
nodeSelectors:
- matchLabels:
metallb.universe.tf/node: active
Apply all BGP resources:
kubectl apply -f bgppeer.yaml
kubectl apply -f bfdprofile.yaml
kubectl apply -f bgpadvertisement.yaml
Verify BGP Peering
Check that BGP sessions are established:
kubectl logs -n metallb-system deployment/controller | grep -i bgp
# Look for "BGP session established" messages
# Or inspect speaker logs directly
kubectl logs -n metallb-system daemonset/speaker | grep -i "peer.*established"
On your router, verify the BGP session and routes:
# Example on a FRRouting router
vtysh -c "show bgp summary"
vtysh -c "show bgp ipv4 unicast"
Advanced Features and Configuration
IP Sharing Across Services
By default, each LoadBalancer service gets a unique IP. However, you can share an IP across multiple services in the same namespace using the metallb.universe.tf/allow-shared-ip annotation. This is useful for services that need to coexist on the same address (e.g., multiple ports for the same application).
# service-a.yaml
apiVersion: v1
kind: Service
metadata:
name: app-http
annotations:
metallb.universe.tf/allow-shared-ip: "my-shared-key"
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
---
# service-b.yaml
apiVersion: v1
kind: Service
metadata:
name: app-grpc
annotations:
metallb.universe.tf/allow-shared-ip: "my-shared-key"
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 50051
targetPort: 50051
Both services will receive the same external IP, and traffic will be routed based on the port number. The annotation value acts as a sharing key—services with the same key share an IP.
Static IP Assignment via Annotations
Instead of relying on automatic IP allocation, you can request a specific IP from a pool using service annotations:
apiVersion: v1
kind: Service
metadata:
name: database-external
annotations:
metallb.universe.tf/address-pool: main-pool
metallb.universe.tf/ip-allocated-from-pool: "192.168.1.245"
spec:
type: LoadBalancer
selector:
app: postgres-primary
ports:
- port: 5432
targetPort: 5432
Note: The IP must fall within one of your configured address pools, or MetalLB will not assign it.
Node Selectors for Speaker
In large clusters, you may want to restrict which nodes participate in BGP peering. Use node selectors on the BGPAdvertisement or L2Advertisement resources:
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: edge-only
namespace: metallb-system
spec:
ipAddressPools:
- main-pool
nodeSelectors:
- matchLabels:
node-role: edge-router
- matchExpressions:
- key: topology.kubernetes.io/region
operator: In
values:
- us-east
- us-west
Community of Communities
In BGP mode, you can attach BGP communities to advertisements for advanced traffic engineering. The communities field in BGPAdvertisement accepts community strings. You can also use the communityAliases feature to define human-readable names for community values:
# communityaliases.yaml
apiVersion: metallb.io/v1beta1
kind: CommunityAliases
metadata:
name: my-communities
namespace: metallb-system
spec:
aliases:
- name: production
value: "64500:100"
- name: staging
value: "64500:200"
- name: high-priority
value: "64500:50"
Complete Working Example: Production-Ready Layer 2 Setup
Here is a complete, end-to-end configuration that sets up MetalLB in Layer 2 mode with multiple pools and proper labeling—a configuration suitable for production environments:
# metallb-complete.yaml
---
# Namespace (typically created by the main manifest, but explicit here)
apiVersion: v1
kind: Namespace
metadata:
name: metallb-system
---
# Primary IP Address Pool for general workloads
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production-pool
namespace: metallb-system
spec:
addresses:
- 10.100.0.100-10.100.0.150
autoAssign: true
avoidBuggyIPs: true
---
# Secondary pool for infrastructure services, with autoAssign disabled
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: infra-pool
namespace: metallb-system
spec:
addresses:
- 10.100.0.50-10.100.0.59
autoAssign: false
avoidBuggyIPs: true
---
# Layer 2 advertisement for the production pool
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: production-l2
namespace: metallb-system
spec:
ipAddressPools:
- production-pool
interfaces:
- eth0
---
# Layer 2 advertisement for the infra pool
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: infra-l2
namespace: metallb-system
spec:
ipAddressPools:
- infra-pool
interfaces:
- eth0
nodeSelectors:
- matchLabels:
node-role: infrastructure
Apply the complete configuration:
kubectl apply -f metallb-complete.yaml
Now create services. General services will automatically get IPs from production-pool. For infrastructure services, you explicitly request from infra-pool:
apiVersion: v1
kind: Service
metadata:
name: vault-internal
annotations:
metallb.universe.tf/address-pool: infra-pool
spec:
type: LoadBalancer
selector:
app: vault
ports:
- port: 8200
targetPort: 8200
Best Practices
- Plan Your IP Space Carefully: Allocate IP ranges that are outside your DHCP scope to avoid conflicts. Reserve a block specifically for LoadBalancer services and document it. Consider future growth—a /24 subnet gives you 254 usable addresses.
- Use Multiple Pools for Organization: Separate pools for production, staging, and infrastructure services help you track IP usage and apply different policies. Use
autoAssign: falsefor pools that require explicit assignment. - Enable
avoidBuggyIPs: Always set this totrueto prevent allocation of addresses ending in .0 or .255, which are treated as network/broadcast addresses by some older network gear. - Monitor IP Pool Exhaustion: MetalLB exposes Prometheus metrics including
metallb_allocator_allocated_ipsandmetallb_allocator_available_ips. Set up alerts when pool utilization exceeds 80%. - For Production BGP, Use BFD: Standard BGP hold timers (typically 90 seconds) mean slow failover. BFD reduces failure detection to sub-second levels, which is critical for high-availability services.
- Use Node Selectors Strategically: In BGP mode, limit which nodes peer with routers. Typically, only nodes with direct uplink connectivity should participate. In Layer 2 mode, you might restrict advertisements to edge/ingress nodes.
- Version Compatibility: Always match MetalLB version with your Kubernetes version. Check the official compatibility matrix. As of writing, MetalLB v0.14.x supports Kubernetes 1.25+.
- Test Failover Regularly: In Layer 2 mode, simulate node failure to verify that another node takes over the IP within the expected time (typically 10-30 seconds). In BGP mode, withdraw routes by cordoning a node and verify traffic redistribution.
- Secure BGP Sessions: Use MD5 passwords for BGP peering authentication in production. While MD5 is cryptographically weak by modern standards, it remains the standard for BGP session protection against spoofed TCP resets.
- Label Your Services: Use the
metallb.universe.tf/service-nameannotation convention to document which service owns which IP, making troubleshooting easier.
Troubleshooting Common Issues
Service Stuck in Pending State
If your LoadBalancer service never gets an EXTERNAL-IP and remains in <pending> state, check the controller logs:
kubectl logs -n metallb-system deployment/controller
Common causes include: no IPAddressPool configured, pool exhaustion (all IPs are assigned), or autoAssign: false on all pools without explicit service annotations.
IP Not Reachable in Layer 2 Mode
Verify the speaker is running on all nodes and check which node is the leader for your service IP:
kubectl exec -n metallb-system daemonset/speaker -- sh -c "arp -a | grep 192.168.1.240"
If ARP resolution works but TCP connections fail, ensure kube-proxy is correctly forwarding traffic. Check that the service's endpoints are healthy:
kubectl get endpoints nginx-service
BGP Session Not Establishing
Check network connectivity between speaker nodes and the router on port 179:
kubectl exec -n metallb-system daemonset/speaker -- nc -zv 10.0.0.1 179
Verify AS numbers match on both sides, and check for MD5 password mismatches. Speaker logs will indicate the reason for session failure:
kubectl logs -n metallb-system daemonset/speaker | grep -i "bgp\|peer\|error"
Conclusion
MetalLB transforms bare-metal Kubernetes clusters by providing a production-ready LoadBalancer implementation that integrates seamlessly with standard networking protocols. Whether you choose the simplicity of Layer 2 mode for smaller deployments or the scalability of BGP mode for larger, router-integrated environments, MetalLB gives you the same native Kubernetes experience that cloud users enjoy. By carefully planning your IP pools, monitoring allocation, and following the best practices outlined in this guide, you can build a robust, highly available service exposure layer entirely within your on-premises infrastructure. The project continues to evolve with active community support, making it the de facto standard for bare-metal Kubernetes load balancing.