Skip to content

LoadBalancer Configuration

LoadBalancer services expose applications to external traffic by automatically provisioning and configuring load balancers. KSail provides comprehensive LoadBalancer support across all distributions with distribution-specific implementations optimized for each platform.

LoadBalancer support varies by Kubernetes distribution and infrastructure provider:

DistributionProviderImplementationDefault BehaviorConfiguration Required
Vanilla (Kind)DockerCloud Provider KINDDisabledYes
K3s (K3d)DockerServiceLB (Klipper)EnabledNo
TalosDockerMetalLBDisabledYes
TalosHetznerhcloud-cloud-controller-managerEnabledNo
VCluster (Vind)DockerDelegated to host clusterN/AN/A

Key Points:

  • K3s and Talos × Hetzner include LoadBalancer support by default—no configuration needed
  • Vanilla and Talos × Docker require explicit enablement during cluster initialization
  • LoadBalancer type is configured via --load-balancer flag or ksail.yaml
  • VCluster delegates LoadBalancer to the host cluster. The spec.cluster.loadBalancer setting has no effect on VCluster clusters—KSail does not install or uninstall any LoadBalancer controller. Changing this setting will not trigger a cluster update.

LoadBalancer support is controlled by the --load-balancer flag or spec.cluster.loadBalancer in ksail.yaml:

ValueBehavior
DefaultUse distribution × provider default (enabled for K3s and Talos × Hetzner, disabled otherwise)
EnabledForce LoadBalancer support on (installs controller if not provided by default)
DisabledForce LoadBalancer support off (prevents LoadBalancer services from getting external IPs)

Implementation: Cloud Provider KIND

Vanilla clusters use the Cloud Provider KIND controller, which runs as an external Docker container and allocates LoadBalancer IPs from the Docker bridge network.

CLI:

Terminal window
ksail cluster init \
--name my-cluster \
--distribution Vanilla \
--load-balancer Enabled

ksail.yaml:

apiVersion: ksail.io/v1alpha1
kind: Cluster
spec:
cluster:
distribution: Vanilla
loadBalancer: Enabled
  1. KSail starts the Cloud Provider KIND controller as a Docker container named ksail-cloud-provider-kind (Cloud Provider KIND then creates per-service LoadBalancer containers with a cpk- prefix)
  2. The controller watches for LoadBalancer services in the cluster
  3. When a LoadBalancer service is created, the controller allocates an IP from the Docker network
  4. The service becomes accessible on that IP from your host machine
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: my-app

After applying this service, check the external IP:

Terminal window
kubectl get svc my-app
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# my-app LoadBalancer 10.96.1.10 172.18.0.100 80:30123/TCP 10s

Access your application:

Terminal window
curl http://172.18.0.100

Implementation: ServiceLB (Klipper LoadBalancer)

K3s includes ServiceLB (formerly known as Klipper LB) by default. It uses host ports to expose LoadBalancer services.

LoadBalancer support is enabled by default—no configuration needed:

Terminal window
ksail cluster init --name my-cluster --distribution K3s
ksail cluster create
  1. When a LoadBalancer service is created, K3s automatically assigns an external IP
  2. The IP is typically the IP of one of the cluster nodes
  3. Traffic is forwarded to the service using iptables rules
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: my-app

Check the service:

Terminal window
kubectl get svc my-app
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# my-app LoadBalancer 10.43.123.45 192.168.1.100 80:30456/TCP 5s

If you want to disable LoadBalancer support (for example, to use NodePort services instead):

CLI:

Terminal window
ksail cluster init \
--name my-cluster \
--distribution K3s \
--load-balancer Disabled

ksail.yaml:

spec:
cluster:
distribution: K3s
loadBalancer: Disabled

Implementation: MetalLB

Talos on Docker uses MetalLB to provide LoadBalancer services. MetalLB operates in Layer 2 mode and allocates IPs from a pre-configured pool.

CLI:

Terminal window
ksail cluster init \
--name my-cluster \
--distribution Talos \
--load-balancer Enabled

ksail.yaml:

apiVersion: ksail.io/v1alpha1
kind: Cluster
spec:
cluster:
distribution: Talos
provider: Docker
loadBalancer: Enabled

KSail configures MetalLB with a default IP pool:

  • IP Range: 172.18.255.200 - 172.18.255.250
  • Mode: Layer 2 (ARP/NDP)
  • Network: Docker bridge network

This range is chosen to avoid conflicts with typical Docker network allocations.

  1. KSail installs MetalLB via Helm during cluster creation
  2. An IPAddressPool and L2Advertisement are created automatically
  3. When a LoadBalancer service is created, MetalLB assigns an IP from the pool
  4. MetalLB announces the IP on the Docker network using ARP
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: my-app

Check the service:

Terminal window
kubectl get svc my-app
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# my-app LoadBalancer 10.96.123.200 172.18.255.200 80:31234/TCP 8s

Access your application:

Terminal window
curl http://172.18.255.200

To use a custom IP range, you’ll need to create custom MetalLB resources after cluster creation:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: custom-pool
namespace: metallb-system
spec:
addresses:
- 172.18.100.1-172.18.100.254
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: custom-l2
namespace: metallb-system
spec:
ipAddressPools:
- custom-pool

Implementation: hcloud-cloud-controller-manager

Talos on Hetzner Cloud uses the Hetzner Cloud Controller Manager to provision real cloud load balancers.

LoadBalancer is enabled by default for Talos × Hetzner clusters. KSail automatically installs hcloud-ccm when loadBalancer is Default or Enabled.

CLI:

Terminal window
export HCLOUD_TOKEN=your-token-here
ksail cluster init \
--name my-cluster \
--distribution Talos \
--provider Hetzner
ksail cluster create

ksail.yaml:

apiVersion: ksail.io/v1alpha1
kind: Cluster
spec:
cluster:
distribution: Talos
provider: Hetzner

Prerequisites:

  • HCLOUD_TOKEN environment variable must be set with a valid Hetzner Cloud API token
  • The token requires read/write permissions for Load Balancers
  1. The Hetzner CCM runs automatically in the cluster
  2. When a LoadBalancer service is created, the CCM provisions a real Hetzner Cloud Load Balancer
  3. The load balancer gets a public IP and forwards traffic to your cluster nodes
  4. You are billed for the Hetzner Load Balancer resource
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: my-app

Check the service (may take 30-60 seconds to provision):

Terminal window
kubectl get svc my-app
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# my-app LoadBalancer 10.32.45.100 <pending> 80:32100/TCP 5s
# Wait for provisioning...
kubectl get svc my-app
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# my-app LoadBalancer 10.32.45.100 135.181.10.50 80:32100/TCP 45s

The EXTERNAL-IP is a real public IP accessible from the internet.

You can customize Hetzner Load Balancer behavior using annotations:

apiVersion: v1
kind: Service
metadata:
name: my-app
annotations:
load-balancer.hetzner.cloud/location: nbg1
load-balancer.hetzner.cloud/use-private-ip: "true"
load-balancer.hetzner.cloud/health-check-interval: "15s"
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: my-app

See the Hetzner CCM documentation for all available annotations.

If you have existing services using type: NodePort, migrating to LoadBalancer is straightforward:

Before (NodePort):

apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # Static node port
selector:
app: my-app

Access via: http://<node-ip>:30080

After (LoadBalancer):

apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: my-app

Access via: http://<external-ip>:80

Migration Steps:

  1. Enable LoadBalancer support if not already enabled (see platform-specific sections)
  2. Update your service manifests to change type: NodePort to type: LoadBalancer
  3. Remove nodePort specifications (they’re unnecessary with LoadBalancer)
  4. Apply the updated manifests: kubectl apply -f service.yaml
  5. Wait for the external IP to be assigned: kubectl get svc my-app -w
  6. Update any clients or DNS records to use the new external IP

Here’s a complete example to test LoadBalancer functionality:

  1. Create a test deployment:

    nginx-deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: nginx-test
    spec:
    replicas: 2
    selector:
    matchLabels:
    app: nginx
    template:
    metadata:
    labels:
    app: nginx
    spec:
    containers:
    - name: nginx
    image: nginx:1.25
    ports:
    - containerPort: 80
  2. Create a LoadBalancer service:

    nginx-service.yaml
    apiVersion: v1
    kind: Service
    metadata:
    name: nginx-lb
    spec:
    type: LoadBalancer
    ports:
    - port: 80
    targetPort: 80
    selector:
    app: nginx
  3. Apply the manifests:

    Terminal window
    kubectl apply -f nginx-deployment.yaml
    kubectl apply -f nginx-service.yaml
  4. Check the service:

    Terminal window
    kubectl get svc nginx-lb -w
    # Wait for EXTERNAL-IP to be assigned
  5. Test connectivity:

    Terminal window
    EXTERNAL_IP=$(kubectl get svc nginx-lb -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    curl http://$EXTERNAL_IP
    # Should show nginx welcome page

Symptom: Service shows <pending> for EXTERNAL-IP:

Terminal window
kubectl get svc
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# my-app LoadBalancer 10.96.1.50 <pending> 80:30123/TCP 5m

Diagnosis:

  1. Check if LoadBalancer is enabled:

    Terminal window
    # Check ksail.yaml
    cat ksail.yaml | grep -A 5 "loadBalancer"
  2. Verify LoadBalancer controller is running:

    Vanilla (Cloud Provider KIND):

    Terminal window
    docker ps | grep ksail-cloud-provider-kind
    # Should show a container named ksail-cloud-provider-kind

    Talos (MetalLB):

    Terminal window
    kubectl get pods -n metallb-system
    # Should show controller and speaker pods in Running state

    Hetzner:

    Terminal window
    kubectl get pods -n kube-system | grep hcloud
    # Should show hcloud-cloud-controller-manager pod
  3. Check controller logs:

    Cloud Provider KIND:

    Terminal window
    docker logs ksail-cloud-provider-kind

    MetalLB:

    Terminal window
    kubectl logs -n metallb-system -l app.kubernetes.io/component=controller

    Hetzner:

    Terminal window
    kubectl logs -n kube-system -l app=hcloud-cloud-controller-manager

Common Fixes:

  • LoadBalancer disabled: Re-initialize cluster with --load-balancer Enabled
  • Cloud Provider KIND not running: Delete and recreate cluster
  • MetalLB IP pool exhausted: Check available IPs in the pool (default: 51 IPs)
  • Hetzner token missing: Ensure HCLOUD_TOKEN is set during cluster creation

Symptom: Service has external IP but connection fails:

Terminal window
curl http://172.18.255.200
# curl: (7) Failed to connect to 172.18.255.200 port 80: Connection refused

Diagnosis:

  1. Verify pods are running:

    Terminal window
    kubectl get pods -l app=my-app
  2. Check service endpoints:

    Terminal window
    kubectl get endpoints my-app
    # Should show pod IPs
  3. Test from within cluster:

    Terminal window
    kubectl run test --rm -it --image=curlimages/curl -- sh
    curl http://my-app.default.svc.cluster.local
  4. Check pod logs:

    Terminal window
    kubectl logs -l app=my-app

Common Fixes:

  • Pods not ready: Wait for pods to reach Running state
  • Wrong target port: Verify targetPort matches container port
  • Network policy blocking traffic: Check for restrictive NetworkPolicies
  • Application not listening: Verify app listens on correct port and 0.0.0.0

Symptom: New LoadBalancer services remain pending after several successful allocations:

Terminal window
kubectl get svc
# NAME TYPE EXTERNAL-IP PORT(S)
# app-1 LoadBalancer 172.18.255.200 80:30001/TCP
# app-2 LoadBalancer 172.18.255.201 80:30002/TCP
# ...
# app-52 LoadBalancer <pending> 80:30052/TCP

Diagnosis:

Check IPAddressPool status:

Terminal window
kubectl get ipaddresspool -n metallb-system default-pool -o yaml

Fix:

Expand the IP range by creating a new pool with additional addresses:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: expanded-pool
namespace: metallb-system
spec:
addresses:
- 172.18.255.200-172.18.255.254 # Expanded from .250 to .254

Or create multiple pools:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: additional-pool
namespace: metallb-system
spec:
addresses:
- 172.18.254.200-172.18.254.254