Skip to content

Access it & deploy an app

Your control-plane node is Running, so the workload cluster’s API server is live. Now you’ll get its kubeconfig, confirm it’s healthy, and run a real application on it.

1. Get the kubeconfig

Cluster API stores each workload cluster’s admin kubeconfig in a Secret on the management cluster. Fetch it:

clusterctl get kubeconfig my-cluster > my-cluster.kubeconfig
If clusterctl get kubeconfig complains about the management cluster version

An older clusterctl may refuse a newer (v1beta2) management cluster. You can always read the Secret directly:

kubectl get secret my-cluster-kubeconfig \
  -o jsonpath='{.data.value}' | base64 -d > my-cluster.kubeconfig

Point kubectl at it for the rest of this page:

export KUBECONFIG=$PWD/my-cluster.kubeconfig

2. Confirm it’s healthy

kubectl get nodes -o wide
NAME                             STATUS   ROLES           AGE   VERSION
my-cluster-control-plane-abcde   Ready    control-plane   6m    v1.35.8
my-cluster-md-0-xxxxx-yyyyy      Ready    <none>          5m    v1.35.8

Nodes go Ready once the CNI add-on (Calico) rolls out — the ClusterResourceSet you applied when installing the provider pushes Calico into the cluster automatically, within a minute of the API server coming up. Check the system pods:

kubectl get pods -A

You should see calico-*, coredns-*, the control-plane static pods (etcd, kube-apiserver, kube-controller-manager, kube-scheduler) and kube-proxy — all Running. (If you also installed the CSI add-on, the whitesky.cloud CSI driver pods appear here too — it's a separate, optional add-on because its driver image is instance-specific.)

That’s a real cluster

Everything from here is standard Kubernetes — kubectl, Helm, your GitOps tooling all work exactly as they would anywhere.

3. Deploy a test app

Run a tiny web app and expose it inside the cluster:

kubectl create deployment hello --image=registry.k8s.io/e2e-test-images/agnhost:2.47 \
  -- /agnhost netexec --http-port=8080
kubectl scale deployment/hello --replicas=2
kubectl expose deployment/hello --port=80 --target-port=8080
kubectl get pods -l app=hello -w      # wait for both Running

Reach it from inside the cluster:

kubectl run curl --rm -it --image=curlimages/curl --restart=Never -- \
  curl -s hello.default.svc.cluster.local/hostname

You’ll get back a pod name — traffic is being load-balanced across your two replicas.

4. (Optional) A persistent volume

Persistent volumes need the CSI add-on — a one-time install on the management cluster, separate from the CNI because the driver image is instance-specific. Set it up first (and pick the right StorageClass for your instance) via the storage guide, then come back.

Create a claim. The default class uses WaitForFirstConsumer, so it stays Pending until a pod mounts it — that's expected:

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: demo-data}
spec:
  accessModes: ["ReadWriteOnce"]
  resources: {requests: {storage: 5Gi}}
EOF
kubectl get pvc demo-data      # Pending — until a pod consumes it

Now mount it into the hello app at /data. kubectl create deployment names the container after the image (agnhost), not the deployment, so this JSON patch targets the container by index — no need to know its name:

kubectl patch deployment hello --type json -p '[
  {"op":"add","path":"/spec/template/spec/volumes","value":[{"name":"data","persistentVolumeClaim":{"claimName":"demo-data"}}]},
  {"op":"add","path":"/spec/template/spec/containers/0/volumeMounts","value":[{"name":"data","mountPath":"/data"}]}
]'
kubectl rollout status deployment/hello

The claim binds (a real disk is created and attached), and you can write to it:

kubectl get pvc demo-data                                          # now Bound
kubectl exec deploy/hello -- sh -c 'echo "hello from $(hostname)" > /data/test.txt && cat /data/test.txt'

The data outlives the pod — delete it and the disk reattaches to the replacement with the file intact:

kubectl delete pod -l app=hello
kubectl rollout status deployment/hello
kubectl exec deploy/hello -- cat /data/test.txt                    # still there

PVC stuck Pending?

Two usual causes: (1) WaitForFirstConsumer — it only binds once a pod mounts it (the patch above); (2) the default StorageClass's provisioner must match your instance's CSI driver — if the disk is never created, see Persistent storage for finding your driver name and the matching class.

ReadWriteOnce = one node

The disk is ReadWriteOnce — mountable on a single node. Your two hello replicas share it only because they're on the same (single) worker. Don't spread a Deployment's replicas across nodes on one RWO claim; for a disk per replica, use a StatefulSet with volumeClaimTemplates. See storage.

More on standard/physical/encrypted classes and online expansion in the storage guide.

5. (Optional) Expose it to the internet

A Service type=LoadBalancer gets a real external IP from a whitesky.cloud load balancer — once the Service-LB add-on is enabled. Like the CNI/CSI add-ons, that's two things: a label on the cluster and the add-on applied once to the management cluster. (Full detail + the CCM alternative: load-balancing guide.)

On the management cluster (kind-capvco-mgmt):

kubectl label cluster my-cluster capvco.infrastructure.cluster.x-k8s.io/service-lb=vco   # opt in
kubectl apply -f https://gitlab.whitesky.cloud/api/v4/projects/730/packages/generic/capvco/v0.6.1/service-lb.yaml

The manager auto-injects the LB credential into the workload cluster. Now, against the workload cluster, expose the app:

kubectl expose deployment/hello --name=hello-lb --type=LoadBalancer --port=80 --target-port=8080
kubectl get svc hello-lb -w     # EXTERNAL-IP populates once the whitesky.cloud LB is created

Curl that external IP and you've served your app to the internet.


Done exploring? → Tear it down to release the whitesky.cloud resources cleanly.