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:
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:
Point kubectl at it for the rest of this page:
2. Confirm it’s healthy¶
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:
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.