K3s’s ServiceLB is a clever way to get your services accessible from outside the cluster without needing a full-blown cloud load balancer.

Here’s how it looks in action. Imagine you’ve got a simple web server running in K3s:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-web-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-web-app
  template:
    metadata:
      labels:
        app: my-web-app
    spec:
      containers:
      - name: web
        image: nginx:latest
        ports:
        - containerPort: 80

You want to expose this. Normally, you’d create a LoadBalancer type Service, and if you were on AWS or GCP, the cloud provider would spin up a real load balancer. With K3s ServiceLB, it works differently. You still create a LoadBalancer service:

apiVersion: v1
kind: Service
metadata:
  name: my-web-service
spec:
  selector:
    app: my-web-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: LoadBalancer

When K3s sees this LoadBalancer service and ServiceLB is enabled (which it is by default in recent K3s versions), it doesn’t call out to a cloud API. Instead, it configures a local component to handle this.

ServiceLB is essentially a controller that watches for LoadBalancer service types. When it finds one, it picks a node in your K3s cluster and assigns an external IP address to that node. It then configures iptables (or nftables on newer systems) on that node to forward traffic arriving at the assigned external IP and the service’s port to the actual pods running your application.

The "external IP" ServiceLB assigns isn’t a magically provisioned IP from a cloud provider. It’s usually an IP address that’s already routable to your K3s node. This could be:

  • The node’s primary IP address.
  • An IP address from a predefined pool you configure.
  • An IP address that K3s can discover on the network.

The key here is that ServiceLB simulates the behavior of a cloud load balancer by using the existing network infrastructure and iptables rules on your K3s nodes. It makes your services appear accessible from outside the cluster as if a real load balancer were in front of them, but it does so with minimal overhead and no external dependencies.

The problem this solves is providing a standard Kubernetes way to expose services that works even in environments where you don’t have cloud provider integration or a pre-existing load balancer infrastructure. It democratizes the LoadBalancer service type for bare-metal, on-premises, or even edge deployments.

Internally, ServiceLB is a Go program that runs as a Deployment within K3s itself. It watches the Kubernetes API for Service objects with type: LoadBalancer. For each such service, it needs to:

  1. Allocate an IP: It determines which IP address will be used as the "external" IP for the service. This is typically the IP of one of the worker nodes.
  2. Configure Node: It makes sure the chosen node has the necessary iptables rules to route traffic from the external IP/port to the service’s pods. This involves creating rules that NAT incoming traffic.
  3. Update Service Status: It updates the status.loadBalancer.ingress field of the Kubernetes Service object with the assigned external IP. This is how kubectl get svc shows you the IP you can connect to.

The exact levers you control are primarily around how ServiceLB selects an IP and which node it uses. You can configure a list of IP addresses for ServiceLB to choose from using the --service-lb-address flag when starting K3s or in its configuration file. If you don’t provide any, it defaults to using the IP addresses of the nodes themselves. You can also influence which node is chosen by K3s’s scheduler, although ServiceLB itself doesn’t have sophisticated node selection logic beyond picking an available, ready node.

The most surprising thing, and mechanically the most important, is that ServiceLB doesn’t actually balance load in the traditional sense. It picks one node and forwards all traffic for that service to that node. That node then uses iptables to forward the traffic to one of the available pods for the service. If that chosen node goes down, the service becomes inaccessible until ServiceLB can reassign the IP to a healthy node. It’s more of a "service exposure" mechanism than a true load balancer.

The next thing you’ll run into is handling the ingress traffic for more complex routing needs, like TLS termination or path-based routing, which is where K3s’s built-in Traefik ingress controller or other ingress solutions come into play.

Want structured learning?

Take the full K3s course →