Skip to content

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.

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:

kubectl label cluster <name> capvco.infrastructure.cluster.x-k8s.io/service-lb=vco

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:

metadata:
  labels:
    capvco.infrastructure.cluster.x-k8s.io/ccm: enabled

…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:

  1. Node initialisation — sets node addresses and topology.kubernetes.io/region + zone labels, and removes the node.cloudprovider.kubernetes.io/uninitialized taint.
  2. 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
kubectl get svc web -w      # EXTERNAL-IP populates when the LB is up

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.