My Load Balancer and Ingress Setup: MetalLB, Traefik, and Cloudflare

When I first set up Kubernetes on my bare-metal cluster, one of the first things I needed was a way to handle load balancing. Since I’m not using a cloud provider, I needed something that could assign external IPs directly on my local network — and MetalLB fit that role perfectly.


Why I Chose MetalLB Over Klipper-LB

K3s includes a lightweight load balancer called Klipper-LB. It works fine for simple, single-node setups, but it has some limitations that made me look elsewhere.

I wanted more flexibility — mainly the ability to use IP addresses that aren’t part of the node’s network.
That doesn’t just mean Internet-exposed addresses; it also includes local but separate subnets, allowing me to completely isolate ingress traffic from node traffic. On top of that, I wanted true multi-node support, better control of IP allocation, and behavior closer to what you’d expect from a full cloud load balancer.

MetalLB checks all those boxes. It supports ARP and BGP-based announcements, handles custom IP pools, and integrates directly with Kubernetes services of type LoadBalancer. It’s purpose-built for bare-metal clusters and has the flexibility to grow with more complex setups.

TL;DR:
Klipper-LB is great for single-node dev or test environments, but MetalLB provides proper IP management, supports multi-node clusters, and lets you assign IPs outside your node network — whether those are on the Internet or just a different LAN segment. This makes it possible to completely isolate ingress traffic while keeping everything local and under your control.


Disabling the Built-in K3s ServiceLB

K3s also includes another basic load balancer called ServiceLB. If you plan to use MetalLB, you’ll need to disable ServiceLB during installation to prevent conflicts.

You can do that by adding the following flag when installing K3s:

curl -sfL https://get.k3s.io | sh -s - server --disable servicelb

If you’re using Ansible, make sure your K3s install role includes the --disable servicelb argument.
Once disabled, MetalLB takes over as the cluster’s external IP manager.


Installing MetalLB with Ansible

I manage my entire cluster with Ansible, so adding MetalLB was straightforward.
Here’s the playbook I use to install and configure it:

- name: Install MetalLB 
  hosts: master
  become: true
  gather_facts: false
  tasks:
    - name: Add MetalLB Helm repo
      kubernetes.core.helm_repository:
        validate_certs: false
        kubeconfig: "{{ kubeconfig_path }}"
        repo_name: metallb
        repo_url: "https://metallb.github.io/metallb"
        state: present

    - name: Deploy MetalLB
      kubernetes.core.helm:
        validate_certs: false
        kubeconfig: "{{ kubeconfig_path }}"
        name: metallb
        chart_ref: metallb/metallb
        release_namespace: metallb-system
        create_namespace: true
        release_state: present
        wait: true

    - name: Configure MetalLB IP Pool
      kubernetes.core.k8s:
        validate_certs: false
        kubeconfig: "{{ kubeconfig_path }}"
        state: present
        definition:
          apiVersion: metallb.io/v1beta1
          kind: IPAddressPool
          metadata:
            name: first-pool
            namespace: metallb-system
          spec:
            addresses:
              - 10.0.10.100-10.0.10.110

    - name: Configure L2 Advertisement
      kubernetes.core.k8s:
        validate_certs: false
        kubeconfig: "{{ kubeconfig_path }}"
        state: present
        definition:
          apiVersion: metallb.io/v1beta1
          kind: L2Advertisement
          metadata:
            name: first-pool
            namespace: metallb-system
          spec:
            ipAddressPools:
              - first-pool

This playbook:

  • Adds the MetalLB Helm repo
  • Installs MetalLB into the metallb-system namespace
  • Defines a small address pool (10.0.10.100–10.0.10.110) for LoadBalancer-type services
  • Configures a Layer-2 advertisement so those IPs are announced on your LAN

💡 Tip: Update the IP range to match your local network and make sure it doesn’t overlap with DHCP.

🔒 Note: Anything in {{ }} should go in your Ansible vars file, for example:

kubeconfig_path: /etc/rancher/k3s/k3s.yaml

Protect this file with Ansible Vault, especially if it contains sensitive information.


Using Traefik as the Ingress Controller

Since I’m running K3s, it comes with Traefik pre-installed as the default ingress controller.
Rather than swapping it out for something like NGINX, I decided to keep it. Traefik is lightweight, flexible, and integrates nicely with Let’s Encrypt and Cloudflare later on.


Cloudflare Integration

All traffic to my cluster passes through Cloudflare, which gives me security, DDoS protection, and caching at the edge. Recently, I added Cloudflared tunnels into the setup. That let me remove inbound firewall holes I previously had open for Cloudflare’s network — simplifying my configuration while keeping performance steady.

11/1/2025 Update:
I decided to keep Traefik and run it alongside Cloudflared. Resource usage has stayed about the same, and I haven’t noticed any change in response times.
See my post here: Cloudflared with Traefik


Verifying the MetalLB Deployment

After deploying MetalLB, use these kubectl commands to verify everything is working properly.

Check the namespace and pods:

kubectl get ns metallb-system
kubectl get pods -n metallb-system

You should see the MetalLB controller and speaker pods in a Running state.

Check your IPAddressPool and L2Advertisement:

kubectl get ipaddresspools.metallb.io -n metallb-system
kubectl describe ipaddresspool first-pool -n metallb-system

kubectl get l2advertisements.metallb.io -n metallb-system
kubectl describe l2advertisement first-pool -n metallb-system

You should see the configured address range (10.0.10.100–10.0.10.110).

Confirm that services are receiving external IPs:

kubectl get svc -A | grep LoadBalancer

When a LoadBalancer-type service gets one of the IPs from your pool, MetalLB is functioning correctly.


Next up: Cert-Manager, where I’ll cover how I handle certificate automation for all my services.

Leave a Comment