Learn MetalLB - Multi-Pool & Traffic Policies
Episode 9 of 23

Learn MetalLB - Multi-Pool & Traffic Policies

Production clusters usually need more than one IP pool. This episode covers separating pools per environment, choosing a pool per Service via annotation, and the impact of externalTrafficPolicy Local versus Cluster on source IPs, connectivity, and failover.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

One pool for every Service is simple, but rarely meets real-world needs. On a cluster handling many environments at once — dev, staging, production — or many departments, IPs must be managed with clear separation. Episode 9 covers multi-pool and traffic policies: separating pools per environment, choosing a pool per Service, and understanding the impact of externalTrafficPolicy on source IPs and failover behavior.

Traffic policy is one of the most misunderstood Kubernetes concepts, and its combination with MetalLB makes a very real difference — from preserving the client source IP to deciding whether traffic must be re-routed between nodes.

Multi-Pool: Separating IPs per Environment

Two Pools for Two Environments

The most common separation is a pool per environment. Consider these two pools:

Dev pool and prod pool
apiVersion: metallb.io/v1beta2
kind: IPAddressPool
metadata:
  name: dev-pool
  namespace: metallb-system
spec:
  addresses:
    - 192.168.10.200-192.168.10.240
---
apiVersion: metallb.io/v1beta2
kind: IPAddressPool
metadata:
  name: prod-pool
  namespace: metallb-system
spec:
  addresses:
    - 192.168.20.200-192.168.20.240

Now there are two separate IP blocks: dev-pool for the development environment and prod-pool for production. Development traffic will never enter the production block — and vice versa — as long as pool selection is controlled correctly.

Choosing a Pool per Service: Annotation

Without control, the controller is free to pick any pool. To make a specific Service use a specific pool, use an annotation on the Service:

Service forced to use prod-pool
apiVersion: v1
kind: Service
metadata:
  name: api-production
  annotations:
    metallb.universe.tf/address-pool: prod-pool
spec:
  type: LoadBalancer
  selector:
    app: api
  ports:
    - port: 8080

The metallb.universe.tf/address-pool: prod-pool annotation makes the controller allocate a specific IP from prod-pool. This annotation works with pools that have autoAssign: true, and is the only way to use pools with autoAssign: false.

Checking Multi-Pool Allocation

Viewing Services and Used Pools

To make sure allocations match the intended pools:

Verifying allocation per pool
kubectl get svc -A -o wide
kubectl get ipaddresspool

By comparing the EXTERNAL-IP in kubectl get svc -A -o wide with the ranges in each pool, you can confirm production Services really use IPs from the production block. This is a quick audit that helps a lot in daily operations.

Traffic Policy: Local vs Cluster

Two Kube-Proxy Behaviors

externalTrafficPolicy on a Service determines how external traffic is forwarded to pods:

  • Cluster (default): traffic enters any node, then kube-proxy forwards it to pods on other nodes if needed.
  • Local: traffic is only forwarded to pods on the same node where the traffic entered.

This difference has a big impact on source IPs and failover behavior.

Service with externalTrafficPolicy Local
apiVersion: v1
kind: Service
metadata:
  name: api-local
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app: api
  ports:
    - port: 8080

externalTrafficPolicy: Local keeps the client source IP visible in the pods, because traffic isn't re-SNATed when forwarded to a pod on the same node. The consequence: nodes without an application pod won't capture traffic for that Service.

Impact on Source IP

With Cluster, the source IP that reaches the pod is the node IP (because of SNAT in kube-proxy). With Local, the original client source IP is preserved. If your application needs the real source IP for logging or access policies, Local is the answer.

Impact on Failover and Distribution

The main trade-off of Local is more refined failover: MetalLB only directs traffic to nodes that actually have the pod. This keeps the Service healthy when a node dies — traffic automatically avoids nodes without pods. However, if pod replicas only exist on a few nodes, some nodes sit idle as entry points.

Conversely, Cluster distributes traffic evenly across all nodes, but adds one extra hop when the pod is on another node, and the client source IP is lost.

Choosing the Right Policy

Decision Guidance

Practical considerations for choosing a policy:

  • Use Local if the real source IP matters and pod replicas are spread across all nodes.
  • Use Cluster if you prioritize even load distribution and don't care about the source IP.
  • In Layer 2 mode, Local makes the leader node only accept traffic if the pod is there — preventing the leader node from becoming a dead end.
  • In BGP mode, combining Local with ECMP produces far more even traffic distribution to nodes that actually have pods.
Checking a Service's policy
kubectl get svc api-local -o yaml
kubectl describe svc api-local

kubectl describe svc api-local shows the External Traffic Policy column in the LoadBalancer section. Checking this column is a good first step when diagnosing source IP behavior in the cluster.

Conclusion

Episode 9 completes multi-pool and traffic policy management: pools per environment, annotations to pick a pool per Service, and the real differences between externalTrafficPolicy: Local and Cluster on source IPs, distribution, and failover.

Key takeaways:

  • Multi-pool separates IPs per environment, department, or need.
  • The metallb.universe.tf/address-pool annotation chooses a pool for a specific Service.
  • externalTrafficPolicy: Local preserves the real source IP and avoids SNAT.
  • externalTrafficPolicy: Cluster spreads load evenly at the cost of the source IP.
  • With Local, nodes without pods don't capture Service traffic.
  • Your policy choice must consider your pod replica distribution.

In the next episode, episode 10, we'll discuss validating admission & CRD — how the metallb-controller webhook validates configurations before they're applied, how to read configuration errors, migrating CRDs from v1beta1 to v1beta2, and best practices when upgrading MetalLB.