Load balancing & Services¶
To expose a workload outside the cluster, you create a Service type=LoadBalancer. CAPVCO turns
that into a real whitesky.cloud L4 load balancer with a public frontend, pooling your nodes behind
it. This guide covers the two ways to run it and the Service features that are (and aren't)
supported.
Two modes — pick one¶
There are two add-ons that reconcile Service type=LoadBalancer. Enable exactly one — they both
run the same Service-LB controller and will fight if both are on.
| Add-on | Label | Also does node init? | Use when |
|---|---|---|---|
| Service-LB (standalone) | …/service-lb: vco |
No | You just want LoadBalancer Services. Start here. |
| Cloud-controller-manager | …/ccm: enabled |
Yes | You want --cloud-provider=external node initialisation and LoadBalancer Services. |
Never enable both
The CCM already runs the Service-LB controller internally. Enabling service-lb alongside ccm
gives you two controllers reconciling the same Services against each other. Pick one.
Standalone Service-LB (recommended default)¶
Two steps — a label on the Cluster, and the add-on's ClusterResourceSet applied once to the
management cluster (exactly like the CNI add-on).
1. Label the Cluster with capvco.infrastructure.cluster.x-k8s.io/service-lb: vco. The base
template ships it commented out, ready to uncomment before you clusterctl generate — or add it to
an existing cluster:
2. Apply the add-on to the management cluster (once — it delivers the vcolb controller to every
cluster carrying that label, via ClusterResourceSet):
kubectl apply -f https://gitlab.whitesky.cloud/api/v4/projects/730/packages/generic/capvco/v0.6.1/service-lb.yaml
The manager then injects the per-cluster vco-lb-credential Secret into the workload cluster
automatically (the vcolb controller reads it to reach the whitesky.cloud API) — nothing else to
create. vcolb runs inside the workload cluster and does not touch node initialisation or set
--cloud-provider=external, so there's no uninitialized taint to worry about — it's the
low-friction option.
Cloud-controller-manager¶
Use the cluster-template-ccm.yaml template, or add the label:
…and apply the CCM add-on to the management cluster once (same pattern as above; it also reads the
auto-injected vco-lb-credential):
kubectl apply -f https://gitlab.whitesky.cloud/api/v4/projects/730/packages/generic/capvco/v0.6.1/ccm.yaml
The CCM (vco-ccm) does two jobs:
- Node initialisation — sets node addresses and
topology.kubernetes.io/region+zonelabels, and removes thenode.cloudprovider.kubernetes.io/uninitializedtaint. - Service load balancing — same controller as the standalone add-on.
--cloud-provider=external and the CCM go together — or not at all
Node init only happens when kubelets run with --cloud-provider=external (the ccm template
sets this). That flag makes kubelet register nodes tainted uninitialized until a CCM clears
them. So: set --cloud-provider=external only if the CCM add-on is enabled — otherwise your
nodes stay tainted forever and never schedule. The non-CCM path (base template) deliberately
leaves it off and sets the provider-id via a kubelet drop-in instead.
Using it¶
Nothing whitesky.cloud-specific in your manifests — it's a stock Service:
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: LoadBalancer
selector: {app: web}
ports:
- port: 80
targetPort: 8080
The controller allocates a public frontend IP, builds a server pool of your Ready nodes, creates one
L4 LB per service port (frontend IP:port → node :nodePort), and writes the address back to
status.loadBalancer.ingress. Deleting the Service tears the LB and pool down (a finalizer handles
it).
Supported Service features¶
| Feature | Supported | How |
|---|---|---|
externalTrafficPolicy: Local |
✅ | Pools only Ready nodes that host a Ready endpoint (via EndpointSlices). Cluster (default) pools all Ready nodes. |
loadBalancerSourceRanges |
✅ | Enforced with a per-Service Traefik ipAllowList middleware. The loadBalancerSourceRanges field and the equivalent annotation both work. |
| Multiple ports | ✅ | One backend LB per port. |
| LB-level health checks | ❌ | The platform's L4 ingress (Traefik TCP) has no health checks. The controller compensates by pooling only Ready nodes — which is why externalTrafficPolicy: Local is meaningful here. |
Session affinity (sessionAffinity: ClientIP) |
⚠️ | Not an LB feature — it's handled by kube-proxy on the node. No LB-level lever. |
About the frontend IP
The frontend uses the cloudspace's external-network IP. Per-Service dedicated public IPs need an attached external network / public-IP quota and are not yet wired — see known limitations.
Geo clusters¶
The Service-LB controller currently uses the cloudspace of the first node it finds. A geo cluster ideally wants an LB per cloudspace; that per-site behaviour isn't done yet. For geo today, LoadBalancer Services work but aren't site-aware — see known limitations.
Next: publish DNS names for those Services automatically → ExternalDNS.