This guide walks through the process of installing a lightweight Kubernetes distribution (K3s), configuring MetalLB for LoadBalancer services, and deploying a simple echo server application. You’ll also learn how to configure MetalLB with a single IP address and set up anti-affinity rules for better pod distribution.
┌────────────────────────────┐
│ External Client │
│ (requests echo-service LB) │
└────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ MetalLB LoadBalancer │
│ (IP: 192.168.1.10) │
└────────────┬───────────────┬─┘
│ │
┌────────────────▼───┐ ┌───▼─────────────────┐
│ Node 1 (Master) │ │ Node 2 │
│ Internal IP: .1 │ │ Internal IP: .2 │
└───┬─────────┬──────┘ └─────┬──────────┬────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ echo-server │ │ echo-server │ │ echo-server │ │ echo-server │
│ Pod │ │ Pod │ │ Pod │ │ Pod │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
- Two Linux nodes:
- Node 1:
192.168.1.1 - Node 2:
192.168.1.2 - IP Pool:
192.168.1.10
- Node 1:
- User with sudo privileges:
ubuntu - K3sup installed: K3sup GitHub repository
export KUBECONFIG=./config/kube_config.yaml
export Node1=192.168.1.1
export Node2=192.168.1.2
export USERNAME=ubuntu
export K3S_VERSION=v1.32.2+k3s1
k3sup install --ip $Node1 --user $USERNAME --k3s-version $K3S_VERSION --k3s-extra-args "--disable traefik --disable servicelb --disable local-storage"k3sup join --ip $Node2 --server-ip $Node1 --user $USERNAME --k3s-version $K3S_VERSIONApply the MetalLB manifests:
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yamlCreate a metallb-config.yaml file with the following content:
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: single-ip-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.10-192.168.1.10
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: single-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- single-ip-poolApply the configuration:
kubectl apply -f metallb-config.yamlCreate a file named echo-deployment.yaml with the following content:
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-server
spec:
replicas: 2
selector:
matchLabels:
app: echo
template:
metadata:
labels:
app: echo
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- echo
topologyKey: "kubernetes.io/hostname"
containers:
- name: echo
image: k8s.gcr.io/echoserver:1.10
ports:
- containerPort: 8080Apply the deployment:
kubectl apply -f echo-deployment.yamlCreate a file named echo-service.yaml with the following content:
apiVersion: v1
kind: Service
metadata:
name: echo-service
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: echo
ports:
- protocol: TCP
port: 80
targetPort: 8080Apply the service:
kubectl apply -f echo-service.yamlNote:
In this example, we set externalTrafficPolicy: Local because we assume all pods run on each node. This configuration preserves the client’s source IP by only sending traffic to nodes with local endpoints. If a node does not have any local pods for the service, it will not receive traffic.
-
Check Pods and Nodes:
kubectl get pods -A -o wide kubectl get nodes -o wide
-
Test LoadBalancer IP: Check the external IP assigned by MetalLB:
kubectl get services -A -o wide
Use
curlto test:curl http://<external-IP>
You should see a response from one of the echo server pods, indicating successful load balancing and traffic routing.
In Cluster mode, incoming traffic is routed to any node’s IP address,
and the Kubernetes network forwards the request to an appropriate backend pod, regardless of its location.
The client’s source IP is not preserved, as the traffic may traverse multiple network hops.
┌────────────────────────────┐
│ External Client │
│ (requests echo-service LB) │
└────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ MetalLB LoadBalancer │
│ (IP: 192.168.1.10) │
└────────────┬───────────────┬─┘
│ │
┌────────────────▼───┐ ┌───▼─────────────────┐
│ Node 1 (Master) │ │ Node 2 │
│ Internal IP: .1 │ │ Internal IP: .2 │
└───┬─────────┬──────┘ └─────┬──────────┬────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ echo-server │ │ echo-server │ │ echo-server │ │ echo-server │
│ Pod │ │ Pod │ │ Pod │ │ Pod │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
In Local mode, the LoadBalancer sends traffic only to nodes that have local backend pods.
This ensures that the client’s source IP is preserved, as the request goes directly to the node where a pod is running.
If a node has no local pods, it does not receive traffic from the LoadBalancer.
┌────────────────────────────┐
│ External Client │
│ (requests echo-service LB) │
└────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ MetalLB LoadBalancer │
│ (IP: 192.168.1.10) │
└────────────┬───────────────┬─┘
│ │
┌────────────────▼───┐ ┌───▼─────────────────┐
│ Node 1 (Master) │ │ Node 2 │
│ Internal IP: .1 │ │ Internal IP: .2 │
└───┬────────────────┘ └──────┬──────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ echo-server │ │ echo-server │ │ │
│ Pod │ │ Pod │ │ (No local │
│ │ │ │ │ endpoint) │
└─────────────┘ └─────────────┘ └─────────────┘
MetalLB handles the VIP reassignment automatically.
In the event of a node failure, it will shift the VIP to another healthy node to maintain service availability.
If you’re using externalTrafficPolicy: Local and the current node loses its local service endpoints,
MetalLB may also relocate the VIP to a node that still has endpoints.
This dynamic reassignment ensures continued traffic flow and minimal disruption for external clients.
By following these steps, you’ve set up a minimal Kubernetes environment using K3s, configured MetalLB as the load balancer, and deployed an echo service with anti-affinity rules to distribute replicas across nodes. This approach provides a simple yet effective way to manage Kubernetes LoadBalancer services in a lightweight cluster environment.