, ,

First VKS Cluster on VCF 9, Namespaces, Cluster Classes and Node Pools (TKGI to VKS Series, Part 14)

Standing up the first VKS cluster the dev wave lands on: create the vSphere Namespace, bind resources, pick a Cluster Class and Tanzu Kubernetes release, apply the manifest, size node pools, and verify with kubectl.

TKGI to VKS Series · Part 14 of 26

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.
Who this is for: platform engineers and Kubernetes operators who have already enabled the vSphere Supervisor on VCF 9 and wired Supervisor networking on NSX VPCs, and now need the first real VKS cluster for the dev migration wave. You have kubectl and the kubectl vSphere plugin installed, and Supervisor admin rights.

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.

# Tested against VCF 9.1, VKS on vSphere Supervisor, # builtin-generic Cluster Class v3.x, kubectl vSphere plugin, kubectl v1.32 # log in to the Supervisor control plane endpoint kubectl vsphere login –server=sup-api.corp.local –vsphere-username platform-admin@corp.local –insecure-skip-tls-verify # in a pipeline, never hardcode the password: # export KUBECTL_VSPHERE_PASSWORD from a secret store and the # plugin reads it from the environment instead of prompting kubectl config use-context sup-api.corp.local kubectl get namespaces kubectl get tanzukubernetesreleases

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.

NAME STATUS AGE dev-apps Active 4m NAME VERSION COMPATIBLE CREATED v1.31.4—vmware.1-fips 1.31.4+vmware.1 True 6d v1.30.8—vmware.1-fips 1.30.8+vmware.1 True 21d
Version note: the exact release string on your Supervisor will differ, so always read it from kubectl get tanzukubernetesreleases rather than copying mine. Treat the strings printed above as illustrative [VERIFY]; the command is what matters.

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.

kubectl get virtualmachineclassbindings -n dev-apps kubectl get storageclasses -n dev-apps
NAME VIRTUALMACHINECLASS AGE best-effort-medium best-effort-medium 2m best-effort-large best-effort-large 2m NAME PROVISIONER AGE vcf-workload-storage csi.vsphere.vmware.com 2m

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]
Figure 1. The path from an empty namespace to a reachable cluster. Binding resources is step two for a reason, everything after it depends on it.

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.

Contrarian call: newest is not safest during a migration. Match the release to the workload you are moving, not to the release notes. You upgrade once the app is proven on VKS, not before.

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.

WaveTKGI source (plan, nodes)VKS VM classWorker replicasControl plane
DevSmall plan, 5 nodes, 2 vCPU 8 GBbest-effort-medium31
StagingMedium plan, 3 nodes, 4 vCPU 16 GBbest-effort-large33
ProdLarge plan, 6 nodes, 8 vCPU 32 GBguaranteed-large63
Registry tierHarbor VMs, 2 nodesbest-effort-medium21

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.

apiVersion: cluster.x-k8s.io/v1beta1 kind: Cluster metadata: name: dev-tkg-01 namespace: dev-apps spec: clusterNetwork: services: cidrBlocks: ["10.96.0.0/12"] pods: cidrBlocks: ["192.168.0.0/16"] topology: class: builtin-generic-v3.2.0 version: v1.31.4—vmware.1-fips-vkr.1 # read yours from: kubectl get tkr controlPlane: replicas: 1 workers: machineDeployments: – class: node-pool name: dev-workers replicas: 3 variables: – name: vmClass value: best-effort-medium – name: storageClass value: vcf-workload-storage

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.

$ kubectl apply -f dev-cluster.yaml Error from server (Failure): error when creating "dev-cluster.yaml": admission webhook "default.validating.cluster.x-k8s.io" denied the request: spec.topology.variables: Invalid value: "best-effort-medium": VirtualMachineClass "best-effort-medium" is not associated with namespace "dev-apps"

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.

$ kubectl apply -f dev-cluster.yaml cluster.cluster.x-k8s.io/dev-tkg-01 created

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.

# correct: bump replicas in the manifest, then kubectl apply -f dev-cluster.yaml # wrong: this is reverted by the topology reconciler # kubectl scale machinedeployment dev-tkg-01-dev-workers -n dev-apps –replicas=5

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.

$ kubectl get cluster dev-tkg-01 -n dev-apps NAME PHASE AGE VERSION dev-tkg-01 Provisioned 12m v1.31.4 $ kubectl get machinedeployment -n dev-apps NAME CLUSTER REPLICAS READY UPDATED PHASE dev-tkg-01-dev-workers dev-tkg-01 3 3 3 Running
Provisioning time by worker countMinutes to reach Provisioned on the reference estatemin061218243 workers125 workers168 workers22
Figure 2. Measured provisioning times. The fixed control plane and image pull cost is why doubling workers does not double the wait.

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.

$ kubectl vsphere login –server=sup-api.corp.local –tanzu-kubernetes-cluster-name dev-tkg-01 –tanzu-kubernetes-cluster-namespace dev-apps –vsphere-username platform-admin@corp.local Error: Post "https://sup-api.corp.local/wcp/login": x509: certificate signed by unknown authority

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.

$ kubectl config use-context dev-tkg-01 $ kubectl get nodes NAME STATUS ROLES AGE VERSION dev-tkg-01-jx4k2-abcde Ready control-plane 11m v1.31.4 dev-tkg-01-dev-workers-9f2c1-x1 Ready <none> 9m v1.31.4 dev-tkg-01-dev-workers-9f2c1-x2 Ready <none> 9m v1.31.4 dev-tkg-01-dev-workers-9f2c1-x3 Ready <none> 9m v1.31.4

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.

$ kubectl delete cluster dev-tkg-01 -n dev-apps cluster.cluster.x-k8s.io "dev-tkg-01" deleted

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 seeLikely causeFix
VirtualMachineClass not associated with namespaceVM class never bound to the namespaceAdd the class under the namespace VM Service, re-apply
Cluster stuck in Provisioning, no machinesNo StorageClass bound, or TKr not compatibleBind the storage policy, pick a COMPATIBLE release, re-apply
x509 certificate signed by unknown authoritySupervisor CA not trusted on the clientImport the CA, or use –insecure-skip-tls-verify in lab
Worker scale reverts after a minutekubectl scale on the MachineDeploymentEdit spec.topology.workers replicas, re-apply the Cluster
Nodes Pending, insufficient resourcesNamespace CPU or memory limits too lowRaise 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

What I got wrong first: on the very first dev landing cluster I let the LCI wizard pick the newest release, several minors ahead of our TKGI 1.18 source. The dev team restored their manifests and hit removed API versions and a Pod Security default that their pods did not satisfy. We spent close to two days re-templating charts before I gave up, deleted the cluster, and re-provisioned it pinned to a release one minor above source. That re-provision took 12 minutes, but the lesson cost two days.
My verdict: use the builtin generic Cluster Class, pin the release to the workload rather than the release notes, and bind namespace resources before you apply. Avoid two habits, chasing the newest TKr on a landing cluster, and scaling MachineDeployments directly. Both feel productive and both cost you time.

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.

TKGI to VKS Series · Part 14 of 26
« Previous: Part 13  |  Guide  |  Next: Part 15 »

References

About The Author


Discover more from Journal of Intelligent Infrastructure

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

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

Continue reading