← Back to DevBytes

MetalLB Load Balancer: Complete Setup and Configuration Guide

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:

How MetalLB Works

MetalLB has two main components that work together:

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

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles