Skip to content

LoadBalancer Configuration

LoadBalancer services expose applications to external traffic. KSail supports LoadBalancer across all distributions with distribution-specific implementations 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

VCluster delegates LoadBalancer to the host cluster — spec.cluster.loadBalancer has no effect and never triggers a cluster update. Use the --load-balancer flag or spec.cluster.loadBalancer in ksail.yaml with values Default, Enabled, or Disabled.

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:

spec:
cluster:
distribution: Vanilla
loadBalancer: Enabled

Cloud Provider KIND runs as a single Docker container named ksail-cloud-provider-kind on the kind network, mounting the Docker socket to watch for type: LoadBalancer services. For each service it creates a dedicated cpk-<namespace>-<name> container that routes traffic from an IP on the kind network bridge subnet (typically 172.18.0.0/16) to the service’s pods. ksail cluster delete removes the controller and all cpk-* containers.

K3s includes ServiceLB by default, assigning the cluster node’s IP as the external IP and forwarding traffic via iptables. No configuration needed:

Terminal window
ksail cluster init --name my-cluster --distribution K3s
ksail cluster create

To disable: use --load-balancer Disabled (CLI) or set loadBalancer: Disabled in ksail.yaml.

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:

spec:
cluster:
distribution: Talos
provider: Docker
loadBalancer: Enabled

KSail configures MetalLB with a default IP pool of 172.18.255.200–172.18.255.250 in Layer 2 (ARP/NDP) mode on the Docker bridge network, chosen to avoid conflicts with typical Docker allocations.

KSail installs MetalLB via Helm, configures an IPAddressPool and L2Advertisement automatically, and MetalLB assigns IPs from the pool via ARP on the Docker network.

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

Talos on Hetzner Cloud uses the Hetzner Cloud Controller Manager to provision real cloud load balancers. LoadBalancer is enabled by default — KSail automatically installs hcloud-ccm when loadBalancer is Default or Enabled.

Prerequisites: Set HCLOUD_TOKEN to a Hetzner API token with read/write permissions for Load Balancers.

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

The Hetzner CCM provisions a real cloud load balancer with a public IP in 30–60 seconds (subject to Hetzner billing).

Terminal window
kubectl get svc my-app -w # EXTERNAL-IP appears after 30–60 seconds

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
selector:
app: my-app
ports:
- port: 80
targetPort: 8080

See the Hetzner CCM documentation for all available annotations.

Symptom: EXTERNAL-IP shows <pending>.

Diagnosis:

Terminal window
# Check loadBalancer setting (absent field = Default; Default auto-enables on K3s and Talos×Hetzner):
grep 'loadBalancer:' ksail.yaml || echo "(not set — effective value: Default)"
# Vanilla: docker logs ksail-cloud-provider-kind
# Talos: kubectl logs -n metallb-system -l app.kubernetes.io/component=controller
# Hetzner: kubectl logs -n kube-system -l app=hcloud-cloud-controller-manager

Common fixes: LoadBalancer disabled → re-init with --load-balancer Enabled. Cloud Provider KIND not running → delete and recreate cluster. MetalLB IP pool exhausted → expand the pool (see below). Hetzner → ensure HCLOUD_TOKEN is set during cluster creation.

Symptom: Service has an external IP but connections fail.

Diagnosis:

Terminal window
kubectl get pods -l app=my-app # verify pods are Running
kubectl get endpoints my-app # check endpoint IPs are populated
kubectl logs -l app=my-app # review pod logs
# Test from within the cluster:
kubectl run test --rm -it --restart=Never --image=curlimages/curl -- curl http://my-app.default.svc.cluster.local

Common fixes: Wrong targetPort → match the container port. Network policy blocking traffic → inspect NetworkPolicy resources. Application not listening on 0.0.0.0 → fix app bind address.

Symptom: New LoadBalancer services remain <pending> after many allocations (default pool has 51 IPs: 172.18.255.200–172.18.255.250).

Fix: Create an additional IPAddressPool in metallb-system:

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