, ,

Provisioning VKS Clusters: ClusterClass and the Cluster API Workflow (VKS Series, Part 4)

Provisioning a VKS cluster is a Cluster API workflow, not a wizard. Here is the manifest, the deprecated API to avoid, and how to read the cluster lifecycle honestly.

Provisioning VKS Clusters with ClusterClass
VKS Series · Part 4 of 17

TL;DR · Key Takeaways

  • You provision a VKS cluster the Cluster API way: write a Cluster manifest that references a built-in ClusterClass, target a vSphere Namespace, and apply it with kubectl.
  • The TanzuKubernetesCluster API is deprecated. Start on the Cluster v1beta1/v1beta2 API with a versioned builtin-generic class, not on copied legacy YAML.
  • Pin nothing by hand that the catalog can tell you: the ClusterClass version, the Kubernetes version and the VM classes must come from what your VKS build and content library actually offer.
  • Watch the Cluster and Machine objects, not the kubectl exit code. The apply records intent; the cluster is ready only when the control plane and nodes report healthy.
Who this is for: platform engineers and DevOps users who will actually create clusters.  Prerequisites: a healthy Supervisor with a synced content library (Part 3), and access to a vSphere Namespace with VM classes and a storage policy assigned.

This is where VKS stops being theory. We provision an actual workload cluster, and the mechanics are deliberately Kubernetes-native: you describe the cluster you want as a manifest, apply it, and let the service reconcile reality to match. If you have used Cluster API anywhere else, the shape will feel familiar. If you have not, it is simpler than it looks once you see the moving parts, and most of the friction comes from two avoidable mistakes, the deprecated API and copied version strings, that this part heads off.

The API you use, and the one you should leave alone

Current VKS provisions clusters through the upstream Cluster API using the Cluster v1beta1/v1beta2 resource. Your Cluster object references a ClusterClass, and VKS ships built-in, versioned classes, builtin-generic-v3.3.0, builtin-generic-v3.4.0 and so on, that encode a tested cluster topology. You set variables, the Kubernetes version, node counts, VM classes, storage, and the class assembles the cluster. The older TanzuKubernetesCluster (TKC) API was deprecated with the move to ClusterClass. It still exists for backward compatibility through a legacy class, but new clusters should use the Cluster API. This is the trap when you copy YAML from older blog posts: a TKC manifest may still apply, yet you have started on a deprecated path you will have to migrate off. Begin with the versioned built-in class instead.

The workflow, end to end

From manifest to running cluster 1Log in toSupervisor 2Target thenamespace 3Apply Clustermanifest 4VKS buildsnode VMs 5Ready: pullkubeconfig
Log in, target the namespace, apply, let VKS reconcile, then consume. Steps 1-3 are yours; step 4 is the service.

You authenticate to the Supervisor, which gives kubectl a context for each vSphere Namespace you can reach. You switch context to the target namespace, then apply a Cluster manifest. A minimal manifest names the cluster, picks a ClusterClass and Kubernetes version, and declares the control plane and worker topology with the VM classes and storage class to use:

apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
  name: prod
  namespace: team-a
spec:
  topology:
    class: builtin-generic-v3.3.0      # from your VKS build
    version: v1.31.x---vmware.x        # from your content library
    controlPlane:
      replicas: 3                      # 3 for HA, 1 for throwaway
    workers:
      machineDeployments:
      - class: node-pool
        name: np-1
        replicas: 3
    variables:
    - name: vmClass
      value: guaranteed-large          # must exist in the namespace
    - name: storageClass
      value: vks-storage-policy         # maps to an SPBM policy

Apply that, and VKS takes over: it provisions the control plane VMs, then the workers in the node pool, installs the CNI, and brings the cluster to Ready. The exact class and version strings come from what your content library and VKS version offer, so list the available releases rather than copying a version verbatim, that one habit prevents a surprising share of provisioning failures.


Lifecycle states, read honestly

A cluster does not go from absent to ready instantly. It moves through phases as the control plane provisions, the workers join, and add-ons settle. The mistake is treating a clean kubectl apply as success, the apply only records intent. Watch the objects:

What you checkHealthy signIf it stalls
Cluster phaseProvisioning → ProvisionedRead the Ready condition message
Control plane MachinesAll Running, API reachableCapacity or storage policy (Part 15)
Worker MachinesJoined and ReadyPending usually means no room to place

Once ready, you pull the cluster’s kubeconfig and hand it to developers or wire it into tooling. From that point it is a normal Kubernetes cluster, kubectl, Helm, GitOps, whatever you run elsewhere. The VKS-specific part was getting it built; consuming it is standard Kubernetes, which is exactly the point.

Version strings are not portable: the ClusterClass and Kubernetes version available to you depend on your VKS build and your content library. Always query what is offered before you write the manifest; a version copied from another environment is the single most common reason a first apply does nothing.

What the ClusterClass variables actually control

The variables block is where most of the real configuration lives, and it is worth knowing what is on offer before you accept the defaults. Beyond the VM class and storage class, the built-in ClusterClass exposes the pod and service CIDRs, the CNI choice (Antrea or Calico), proxy settings for clusters that reach the internet through a corporate proxy, trust bundles for private registries and internal certificate authorities, and node labels and taints. These are not advanced extras. The CIDR fields are the ones that cause the routing failures from the networking part if you leave them on a default that overlaps a corporate subnet, and the trust and proxy fields are what make image pulls work in a locked-down enterprise rather than failing with TLS errors nobody can explain.

Treat the variable list as a checklist you walk once per cluster template, not a thing you discover field by field during an incident. Read the ClusterClass your VKS build ships and note which variables it actually supports, because the set grows between versions and a field that exists in builtin-generic-v3.4.0 may not exist in an older class. Copying a manifest that sets a variable your class does not recognise is a quiet way to have the value silently ignored.

Customisations worth setting on day one

A handful of choices are far cheaper to make at creation than to retrofit. Set the pod and service CIDRs from a planned, non-overlapping block rather than the default. Add your private registry and any internal certificate authority to the trust configuration so workloads pull images cleanly from the first deploy. If your nodes reach the internet through a proxy, set it in the cluster variables, not as a hand-edited daemonset later. Decide the CNI now, because it is baked in at provisioning. And label your node pools meaningfully (workload type, environment, cost centre) so scheduling, quotas and chargeback have something to key on.

None of these is exotic, but each one quietly determines whether the cluster is usable in production or merely demo-ready. The pattern I push on clients: maintain one reviewed Cluster manifest per environment in Git, with these fields already correct, and let people change only the cluster name, size and version. That turns cluster creation into filling in three blanks instead of rediscovering the same five settings every time, and it stops the slow drift where every team’s clusters are configured slightly differently and nobody can say why.

Deleting and recreating clusters cleanly

Because clusters are declarative, deletion is also declarative, and that is mostly a strength. Delete the Cluster object and VKS reconciles the worker and control plane VMs out of existence. The one thing that catches people is persistent volumes: depending on the reclaim policy and how the storage was provisioned, deleting the cluster does not always delete the underlying disks, and you can leave orphaned First Class Disks consuming datastore capacity. Before you tear a stateful cluster down, account for its volumes deliberately, back up anything that matters with Velero, then delete, then confirm in vCenter that the disks actually went away.

This declarative lifecycle is why ephemeral, disposable clusters are a realistic pattern on VKS rather than an aspiration. A dev team can stand up a cluster for a sprint, run their work, and delete it, and the only residue is whatever storage they explicitly chose to keep. Lean into that. Long-lived pet clusters accumulate drift and risk; cattle clusters defined in Git and recreated on demand do not.

Anatomy of the manifest you will reuse

The Cluster manifest looks small, but every field in it maps to a real decision and a real piece of infrastructure, and seeing those mappings is what turns copy-paste into understanding. The topology section names the ClusterClass and Kubernetes version; the control plane and worker blocks set replica counts that become VMs; and the variables resolve to VM classes, storage classes and CIDRs that the platform must already offer. Nothing in the manifest is free-form, each value is validated against what your namespace and content library actually provide.

Each manifest field maps to something real In the manifesttopology.class + versioncontrolPlane.replicasworkers.replicasvariables: vmClass, storage, CIDRs Resolves toa VKr image in your content library (must exist)1 or 3 control plane VMs (HA decision)N worker VMs in a node poolnamespace VM classes, SPBM policy, pod/svc ranges
A small manifest, but every line is a validated reference to something the platform must already provide.

Why a copied manifest fails on a clean platform

Most first-apply failures trace to a value that does not resolve: a ClusterClass version your build does not ship, a VM class the namespace was never given, a storage class with no matching policy. The manifest is valid YAML, so it applies; it just references things that are not there, and the cluster never builds. This is why the habit of listing what the platform offers, the available VKr, the namespace VM classes and storage classes, before you write the manifest, prevents the single most common provisioning dead end.

In practice

Keep one reviewed manifest per environment in Git with the class, version, VM class and CIDRs already correct for that platform, and let people edit only the cluster name and size. The first time a new joiner spins up a cluster from that template in thirty seconds instead of spending an afternoon discovering which VM class their namespace actually has, the value of the paved road is obvious.

From apply to ready: the provisioning timeline

Provisioning is not instant and it is not uniform; the control plane comes up first, then the workers, then the cluster settles. Knowing the order is what lets you read a half-built cluster correctly instead of panicking that it is stuck when it is simply mid-sequence.

What happens between apply and Ready applyintent recorded control planeVMs build, API up workers joinnode pool VMs ReadyCNI + add-ons settled
A cluster with its control plane up but workers still joining is mid-sequence, not stuck.

Reading the half-built state

If the control plane machines are Running and the workers are still Provisioning, that is normal progress, not a fault. The genuine stalls look different: a control plane machine stuck in Provisioning past its usual time points at storage or capacity, and a worker stuck Pending points at no room to place it. Read the machine objects rather than the wall clock, and you can tell expected sequencing from a real block within seconds.

Worked example

A developer reported a cluster “stuck” two minutes after apply because kubectl get cluster still said Provisioning. The machine objects told the real story: all three control plane VMs were Running and two of three workers had already joined; the third was seconds from Ready. Nothing was stuck. The lesson stuck better than any doc: read the machines, give the sequence its normal window, and only treat a phase as blocked when its condition message says so.

Label every cluster from the first apply

A field worth setting on day one and almost always skipped is metadata: labels and annotations on the cluster that say which team owns it, which environment it serves, and which cost centre it belongs to. It feels like bureaucracy when you have one cluster and becomes priceless when you have fifty, because that is what lets you answer “whose cluster is this, can we delete it, and who pays for it” without archaeology. The same labels drive scheduling, quota reporting and chargeback later, so a consistent labelling scheme baked into the cluster template costs nothing at creation and saves a genuinely painful clean-up once the estate grows and nobody remembers which of the thirty clusters named after Greek gods is still in use. Decide the labels once, put them in the template, and make them non-negotiable.

What I’d Do

Standardise on the versioned built-in ClusterClass and never let a TKC manifest into a new environment, it is a debt you take on for nothing. Keep a tiny, known-good Cluster manifest in Git as your template, with the class, version and VM class as the only fields anyone edits, and have people query the available releases before they change them. Treat provisioning as asynchronous: apply, then watch the Cluster and Machine objects until they are genuinely Provisioned, and resist re-applying out of impatience. Do that and cluster creation becomes a thirty-second action plus a wait, which is exactly what you want from a platform. When you create your next cluster, are you reading the cluster conditions, or just trusting that a clean apply meant it worked?

References

VKS Series · Part 4 of 17
« Prev: Part 3  |  VKS Complete Guide  |  Next: Part 5 »

About The Author


Discover more from Journal of Intelligent Infrastructure – By Dr Pranay Jha

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure - By Dr Pranay Jha

Subscribe now to keep reading and get access to the full archive.

Continue reading