TL;DR · Key Takeaways
- A VKS cluster (vSphere Kubernetes Service cluster) is a declarative object you apply with kubectl, not a BOSH deployment you push. Create the vSphere Namespace first, then apply one Cluster manifest.
- Bind the VM classes and the storage policy to the namespace before you apply, or an admission webhook rejects the cluster and nothing provisions.
- Pin the Tanzu Kubernetes release (TKr, the versioned node image) close to your source TKGI cluster Kubernetes minor. Chasing the newest build breaks app manifests and webhooks on restore.
- Headline flow: kubectl apply -f dev-cluster.yaml, then kubectl vsphere login to pull the new cluster kubeconfig.
- A three worker cluster provisions in about 12 minutes on this estate. Size the control plane to 1 for non-prod, 3 for prod.
Where the migration stands, and what lands first
Two parts back we prepared VCF 9 and enabled the vSphere Supervisor, the control plane that turns a vSphere cluster into a Kubernetes platform. Last part we wired Supervisor networking on NSX VPCs and picked a load balancer. So the platform can now host Kubernetes clusters. Nothing runs on it yet.
This part builds the first VKS cluster, the landing zone for the dev wave from our reference estate. That source estate is TKGI 1.18 (Tanzu Kubernetes Grid Integrated, formerly Enterprise PKS) on NSX-T with three clusters named dev, staging and prod, UAA and LDAP auth, a Harbor registry, and one stateful app behind a couple of stateless services. We move workloads later. Right now we need somewhere for them to go, and getting the first cluster right sets the pattern for the other two.
A quick reset on mechanics, because this is where TKGI muscle memory misfires. On TKGI you sized a plan, then ran tkgi create-cluster and BOSH (the release engineering and VM lifecycle system under TKGI) built the nodes. On VKS you write a Cluster object and apply it with kubectl. The Supervisor reconciles that object into control plane and worker VMs. There is no Ops Manager tile, no BOSH manifest, and no imperative create command. If you want to know how the Supervisor itself carves subnets out of an NSX VPC, that belongs to the previous part and I will not re-teach it here.
Preflight before the first cluster
Three preconditions have to be true before a Cluster object will provision, and each one has a check that proves it. First, you can authenticate to the Supervisor. Second, at least one vSphere Namespace exists with a VM class and a storage policy bound to it. Third, a compatible Tanzu Kubernetes release is available to the Supervisor. Miss any of these and the cluster either never appears or sticks in a pending phase with a webhook error.
What green looks like: the login returns a context, the namespace you plan to use shows Active, and the release list returns at least one COMPATIBLE entry. A truncated example follows.
Create the vSphere Namespace and bind resources
A vSphere Namespace is the tenancy and quota boundary that a VKS cluster lives inside. It is the VCF answer to what a TKGI plan plus a BOSH deployment used to bound. You create it in the vCenter Supervisor management view, or through the Local Consumption Interface (LCI) if you installed that Supervisor service. Give it a DNS compliant name, so lower case, no spaces, no special characters. I named the dev landing namespace dev-apps.
Binding is the step people skip, and it is the one that bites. A namespace does not automatically grant every VM class or storage policy in the vCenter. You add them explicitly. Under the namespace, assign a storage policy (this becomes a Kubernetes StorageClass inside the cluster) and add the VM classes you intend to reference. If your Cluster manifest names a VM class the namespace does not carry, provisioning fails at admission, not at runtime, which is a small mercy because it fails fast. I add the storage policy and both VM classes I plan to use, then confirm from the command line before writing any YAML.
Both classes appear as bindings, and the storage policy shows up as a StorageClass backed by the vSphere CSI (Container Storage Interface) driver. Now the namespace can actually satisfy a cluster request.
flowchart TD A[vSphere Namespace dev-apps] --> B[Bind VM classes and storage policy] B --> C[Choose Cluster Class and TKr] C --> D[Apply Cluster manifest] D --> E[Control plane VM provisions] E --> F[Worker node pool joins] F --> G[Verify and pull kubeconfig]
Choose a Cluster Class and a Tanzu Kubernetes release
Two choices shape the cluster: which Cluster Class you build from, and which Tanzu Kubernetes release you pin. A Cluster Class is a versioned template, shipped by VMware as builtin-generic-v3.x, that defines the shape and patches of a conformant cluster so you declare topology instead of wiring machine templates by hand. Use the builtin generic class unless you have a concrete reason to fork it, and you rarely do on a first migration.
The release choice is where the tutorial default is wrong for a migration. Every quickstart, and the LCI wizard, nudges you toward the newest release. For a greenfield cluster that is fine. For a landing zone that has to receive restored workloads from TKGI 1.18, it is a trap. Our source clusters run an older Kubernetes minor, and jumping several minors at once means deprecated API versions, removed beta APIs, and stricter Pod Security admission all hit on the first restore. Pin the first VKS cluster to a release within about one minor of the source, get the apps running, then upgrade the cluster in place afterwards where you control the blast radius.
Sizing is the other half of the design, and here a mapping from the old plans to the new VM classes saves a lot of guesswork. This table is the artifact I keep open while I template each of the three clusters. It reads TKGI plan sizes across to VKS VM classes, node pool replicas and control plane count.
| Wave | TKGI source (plan, nodes) | VKS VM class | Worker replicas | Control plane |
|---|---|---|---|---|
| Dev | Small plan, 5 nodes, 2 vCPU 8 GB | best-effort-medium | 3 | 1 |
| Staging | Medium plan, 3 nodes, 4 vCPU 16 GB | best-effort-large | 3 | 3 |
| Prod | Large plan, 6 nodes, 8 vCPU 32 GB | guaranteed-large | 6 | 3 |
| Registry tier | Harbor VMs, 2 nodes | best-effort-medium | 2 | 1 |
Table 1. Node pool sizing map from TKGI plans to VKS VM classes. Note dev drops from 5 nodes to 3, since a landing cluster does not need to mirror source node count, only source capacity headroom.
Apply the manifest and size the node pools
Here is the dev cluster manifest, built from the table above. It declares a single control plane node, one worker pool of three, the medium VM class, and the workload storage policy. Note the guaranteed absence of any machine template, that is what the Cluster Class buys you.
Apply it. If you did the binding step, this succeeds. If you did not, you meet the most common first cluster failure, an admission webhook rejecting a VM class the namespace does not carry.
The fix is to add that VM class to the namespace (VM Service, Add VM Class, or the same in LCI), then re-apply. This time it takes.
To resize a pool later, resist the obvious move. Running kubectl scale on the MachineDeployment appears to work, then the topology reconciler quietly reverts it within a minute, because the Cluster object is the source of truth, not the MachineDeployment. Edit the replicas in spec.topology.workers and re-apply the Cluster instead.
Verify, then roll back if you must
Watch the Cluster phase move to Provisioned and the MachineDeployment reach full ready replicas. On this estate a three worker dev cluster reaches Provisioned in about 12 minutes, a five worker cluster in about 16, and an eight worker cluster in about 22. Provisioning time grows with worker count but not linearly, since the control plane and image pulls are a fixed cost.
Once Provisioned, log in to the workload cluster itself to pull its kubeconfig. A first login often trips on certificate trust, which is worth seeing so you recognise it.
Trust the Supervisor certificate authority on your workstation, or in a lab pass –insecure-skip-tls-verify, then the context appears and the nodes report Ready.
Rollback on a landing cluster is refreshingly cheap, because nothing is running on it yet. If the topology is wrong or you pinned the wrong release, delete the Cluster and start over. Deleting the object tears down the control plane and worker VMs cleanly.
This is a real advantage over the TKGI world, where re-rolling a plan meant a BOSH redeploy. Because the cluster is a declarative object, your rollback is a delete and a re-apply, and it is worth enjoying that while the cluster is still empty. Once workloads land, rollback becomes a Velero restore question, which is later in the series.
These are the failures I actually hit standing up first clusters, and how each one clears.
| Error you see | Likely cause | Fix |
|---|---|---|
| VirtualMachineClass not associated with namespace | VM class never bound to the namespace | Add the class under the namespace VM Service, re-apply |
| Cluster stuck in Provisioning, no machines | No StorageClass bound, or TKr not compatible | Bind the storage policy, pick a COMPATIBLE release, re-apply |
| x509 certificate signed by unknown authority | Supervisor CA not trusted on the client | Import the CA, or use –insecure-skip-tls-verify in lab |
| Worker scale reverts after a minute | kubectl scale on the MachineDeployment | Edit spec.topology.workers replicas, re-apply the Cluster |
| Nodes Pending, insufficient resources | Namespace CPU or memory limits too low | Raise the namespace resource limits, or shrink the VM class |
Table 2. First cluster failures and their remediation. Four of these five are configuration on the namespace, not the cluster.
Field note and verdict
Start with the dev landing cluster on Monday
A clean first cluster looks like this: the namespace shows Active with a StorageClass and the VM classes bound, kubectl get cluster reports Provisioned, the MachineDeployment shows all replicas Ready, kubectl get nodes returns a control plane node and every worker as Ready on the release you pinned, and you can delete and re-apply the Cluster without touching vCenter. That is the pattern you will now repeat for staging and prod, changing only the sizes from Table 1.
On your own estate on Monday, do one thing: check your source cluster Kubernetes version, then run kubectl get tanzukubernetesreleases on the Supervisor and choose the release within one minor of it. That single decision, made before you write any manifest, prevents the most expensive mistake in this whole part. For the mechanics of VKS clusters beyond this first build, the vSphere Kubernetes Service complete guide goes deeper, and the broader platform context sits in the VCF 9 complete guide. Next part wires identity, RBAC and Harbor onto this cluster so the dev team can actually log in and pull images.
Questions worth answering
Do I need three control plane nodes for a dev cluster?
No. A single control plane node is fine for non-prod and saves resources. Use three for staging and prod where control plane availability matters. Never run a single control plane node in production.
Can I add a second node pool later?
Yes. Add another entry under spec.topology.workers.machineDeployments with its own name and VM class, then re-apply the Cluster. This is how you separate, say, general workers from a memory heavy pool without rebuilding.
Should I fork the Cluster Class?
Not on a first migration. The builtin generic class covers the vast majority of needs. Fork it only when you have a concrete, repeatable patch you cannot express through variables, and treat that fork as its own maintained artifact.
How do I pick between best-effort and guaranteed VM classes?
Best-effort classes do not reserve the full resource on the host and pack more densely, which suits dev and staging. Guaranteed classes reserve CPU and memory, which you want for prod where noisy neighbours are unacceptable. Table 1 reflects that split.
This series covers a production migration. Any step that touches a real cluster should run in a change window against your own environment, with the source estate untouched until the target is verified.
References
- VKS Components, Broadcom TechDocs
- vSphere Kubernetes Service on VCF 9.1, VCF Blog
- Configuring vSphere Kubernetes Service, William Lam


DrJha