, ,

VKS Cluster Rolling Updates and Configuration Changes (VCAP-VKS Exam Series, Part 21)

A rolling update on a VKS cluster behaves differently depending on whether you changed the Kubernetes version or a configuration variable. Part 21 runs both against the reference estate, shows the output that lies to you, and covers objective 4.6.

VCAP-VKS Exam Series · Part 21 of 34
Key takeaways: Objective 4.6 is one sentence hiding three separate behaviours. Changing spec.topology.version replaces control plane nodes first, then worker nodes one at a time. Changing a configuration variable such as VM class or storage class replaces one worker per node pool at the same time, so a two pool cluster rolls two workers concurrently. kubectl get cluster echoes back the version you typed rather than the version your nodes run, which makes that column worthless as proof. Watch UPDATED against REPLICAS on the MachineDeployment instead. And there is no downgrade path, so once a roll starts your only options are forward or restore.
Who this is for: A candidate who can already provision and scale a workload cluster from a terminal and now has to change one in place without taking an application down. This Part covers Objective 4.6, published wording Update VKS clusters through rolling updates and configuration changes. Terms defined on first use: VKS is vSphere Kubernetes Service, the product formerly named TKG Service or TKGS, and that rename is unfinished, so tkg still appears in API groups, controller names and documentation URLs; a VKr is a vSphere Kubernetes release, the pairing of a node image with a Kubernetes version, formerly a TKr; a Supervisor is the Kubernetes control plane running on ESX hosts that owns vSphere Namespaces and provisions clusters; a ClusterClass is the reusable topology template a Cluster object references; a MachineDeployment is the Cluster API object behind a worker node pool; a PodDisruptionBudget, abbreviated PDB, is the Kubernetes object that caps how many pods of a workload may be unavailable at once.

Forty seconds after I saved a manifest edit on vks-lifecycle-01, kubectl reported the cluster VERSION as v1.32.0+vmware.6-fips. Not one node had restarted. No image had been pulled. That column is reading back the number I typed into spec.topology.version, not the Kubernetes version any node is actually running, and a candidate who verifies an update from it will be wrong in both directions, in the exam and in a change window.

Last part we drove vks-lifecycle-01 through provisioning, monitoring, scaling and deletion. This part we take that same cluster from Kubernetes 1.31.4 to 1.32.0, then change two configuration variables underneath it, and watch how differently the platform behaves in each case. Same three zone VCF 9.0 estate throughout, same namespace, same node shapes.

Preflight before editing a cluster manifest

Two facts decide whether an update will even begin. Whether a compatible newer VKr exists in the namespace, and whether the cluster carries a condition that blocks updates. Both are readable in about fifteen seconds, and skipping them is how people end up staring at a manifest edit that produced nothing at all.

# Versions everything below was run against. VCF 9.0, vCenter 9.0.0.0 build 24755230 Supervisor version v1.31.4+vmware.wcp.2 VKS 3.3.1, ClusterClass builtin-generic-v3.1.0 kubectl v1.32.2, kubectl-vsphere plugin 9.0.0 VCF CLI v9.0.0 Cluster under test: vks-lifecycle-01, namespace ns-platform-a 3 control plane nodes, 3 workers across 2 node pools, guaranteed-medium

Note the resource name in the next block. From VKS 3.2 onward the release object is kubernetesreleases. Below 3.2 it is tanzukubernetesreleases. Both exist in the wild, both appear in Broadcom documentation, and an item that shows you one of them is testing whether you know which platform generation you are looking at.

kubectl config use-context ns-platform-a # VKS 3.2 and later kubectl get kubernetesreleases NAME VERSION READY COMPATIBLE v1.31.4—vmware.1-fips-vkr.3 v1.31.4+vmware.1-fips True True v1.32.0—vmware.6-fips-vkr.2 v1.32.0+vmware.6-fips True True # Ask the cluster itself what it will accept, rather than guessing from the list. kubectl get cluster vks-lifecycle-01 -o json | jq ‘.status.conditions[] | select(.type=="UpdatesAvailable") | .message’ "[v1.31.7+vmware.1-fips-vkr.2 v1.32.0+vmware.6-fips-vkr.2]" kubectl get cluster vks-lifecycle-01 -o json | jq ‘.status.conditions[] | select(.type=="Ready") | .status’ "True"

UpdatesAvailable is the condition worth internalising. It is computed against compatibility, not against what happens to be sitting in your content library, so it will hide a release the library carries but the cluster cannot legally move to. If UpdatesAvailable is absent or empty, editing the version field will be silently ignored by the topology controller and you will lose twenty minutes wondering why.

Rolling update mechanics on VKS 3.3

Two controllers share the work, an add ons controller and a cluster controller, and between them a roll has three stages that always run in this order: add ons, control plane, worker nodes. Each stage is gated by prechecks so a later stage cannot start until the earlier one has progressed far enough, and a stage is skipped entirely when nothing in it changed. A change that only touches workers therefore never disturbs your control plane.

Per node, replacement is additive first and destructive second. A new node is created and joined at the target configuration, then the old node is cordoned and drained, and only once every pod has left is that node deleted. If a pod cannot be evicted, the old node sits cordoned forever and the roll does not advance. Cluster API respects PodDisruptionBudgets absolutely, which is a feature until somebody writes one badly.

flowchart TD
  A[Spec edit or system change] --> B[Add ons controller reconciles packages]
  B --> C[Control plane nodes replaced first]
  C --> D[New node joins at target config]
  D --> E[Old node cordoned and drained]
  E --> F{Every pod evictable}
  F -- yes --> G[Old node deleted, next node begins]
  F -- no --> H[Node stays cordoned, roll stalls here]
  G --> I[Worker node pools roll in order]
  I --> J[Cluster phase returns to Provisioned]
Rolling update stages and the drain gate where most stalls happen.

One consequence gets people fired. Pods not governed by a replication controller, meaning bare pods that are not part of a Deployment, ReplicaSet or StatefulSet, are deleted during the worker drain and never recreated. Somebody kubectl run a debug pod with a mounted volume, walk away, and an overnight update will remove it without a trace in the application logs.

Habit to unlearn: On vSphere 8 estates, upgrading the Supervisor commonly triggered a rolling update of every workload cluster underneath it, because the TKG controller shipped inside the Supervisor. From VKS 3.0 the controller is independent of vCenter and the Supervisor, and upgrading those components no longer rolls your clusters. Upgrading the VKS version itself still may. If you plan your change windows on the vSphere 8 assumption you will book four hours of risk you no longer carry, and if you plan a VKS component upgrade as a five minute job you will find out the hard way that this is where the roll moved to.

Version updates by editing the VKr

For a v1beta1 or v1beta2 cluster, a Kubernetes version change is a single field. Edit spec.topology.version to the name of the target VKr, save, and the topology controller does the rest. Two details trip candidates up. First, the value in the manifest uses the VKr object name with three hyphens before vmware, while every status field and printed column uses the plus sign form. Second, the TKR_DATA variable in the same manifest updates itself, and hand editing it is a good way to produce a cluster that will not reconcile.

kubectl edit cluster vks-lifecycle-01 # before apiVersion: cluster.x-k8s.io/v1beta1 kind: Cluster spec: topology: class: builtin-generic-v3.1.0 version: v1.31.4—vmware.1-fips-vkr.3 controlPlane: replicas: 3 # after: change one line only version: v1.32.0—vmware.6-fips-vkr.2 cluster.cluster.x-k8s.io/vks-lifecycle-01 edited # Within seconds, both of these flip to False. That is the roll starting. kubectl get cluster vks-lifecycle-01 -o json | jq ‘.status.conditions[] | select(.type=="TopologyReconciled") | .status’ "False"

On my estate that edit produced 57 minutes of work. Three control plane nodes replaced sequentially at roughly 11 minutes each, then three workers at roughly 8 minutes each, strictly one worker at a time beginning with the zone A node pool. Nothing overlapped. If you have two node pools and you assumed a version update would roll them in parallel to save time, it will not, and provisioning identical extra node pools to game that is a documented waste of capacity.

Configuration changes that roll nodes

Objective 4.6 pairs rolling updates with configuration changes for a reason: a configuration variable edit is a rolling update, it just has a different shape. VM class and storage class are the two the blueprint cares about, and both replace nodes rather than reconfiguring them in place. What changes is the concurrency. During a configuration variable update, control plane nodes go first as usual, and then one worker per node pool rolls simultaneously. Two node pools means two workers replaced at once, which is why the storage class change on the same cluster finished in 34 minutes where the version change took 57.

# Storage class change. The class must already be bound to the vSphere Namespace, # or the edit is accepted and the roll never starts. kubectl get storageclass NAME PROVISIONER AGE vsan-default-policy csi.vsphere.vmware.com 61d vsan-highperf-policy csi.vsphere.vmware.com 12d kubectl edit cluster vks-lifecycle-01 variables: – name: defaultStorageClass value: vsan-highperf-policy # VM class change on the worker pool only. Control plane untouched. – name: nodePoolVolumes … workers: machineDeployments: – class: node-pool name: node-pool-1 replicas: 2 variables: overrides: – name: vmClass value: guaranteed-large

Here is the artifact worth keeping, a change to blast radius lookup. Print it, tape it inside the change ticket template, and stop guessing how long a window needs to be.

What you changeNodes replacedWorker concurrencyMeasured on this estate
spec.topology.version, the VKrAll control plane, then all workersOne worker total, regardless of pool count57 min for 3 plus 3
Storage class variableControl plane, then all workersOne worker per node pool34 min, 2 pools
VM class override on a poolThat pool onlyOne worker per node pool38 min, 3 workers
replicas countNone, nodes are added or removedNot a roll9 min, plus three workers
Content library image added by subscriptionNoneNot a roll0 min
Content library swapped, images renamedEvery node of every cluster in scopePer cluster, as aboveHours, unplanned
Last two rows deserve a moment: Adding images to a subscribed library, or letting a sync bring new ones down, does not roll anything. Switching a namespace from a subscribed library to a local one and uploading the same OVAs under your own names does roll every cluster fed by it, because the image names no longer match. That is the air gapped migration everyone plans as a storage task and nobody plans as a downtime task. Part 9 covered building the local library; this is the bill it hands you later.

Verification signals worth trusting

Go back to the opener. Cluster VERSION reflects your spec, so it is a statement of intent. Three signals report reality: the UPDATED column against REPLICAS on KubeadmControlPlane and MachineDeployment, the TopologyReconciled condition, and Machine object ages. Machine age is the crude one I trust most, because a node that was not replaced has an age older than the moment you saved the edit, and no controller can lie about that.

kubectl get cluster,kubeadmcontrolplane,machinedeployment NAME CLUSTERCLASS PHASE AGE VERSION cluster.cluster.x-k8s.io/vks-lifecycle-01 builtin-generic-v3.1.0 Provisioned 9d v1.32.0+vmware.6-fips NAME CLUSTER REPLICAS READY UPDATED UNAVAILABLE VERSION kubeadmcontrolplane…/vks-lifecycle-01-9d5wz vks-lifecycle-01 3 3 3 0 v1.32.0+vmware.6-fips NAME CLUSTER REPLICAS READY UPDATED UNAVAILABLE PHASE VERSION machinedeployment…/vks-lifecycle-01-node-pool-1-psdcg vks-lifecycle-01 2 2 2 0 Running v1.32.0+vmware.6-fips machinedeployment…/vks-lifecycle-01-node-pool-2-k4qrb vks-lifecycle-01 1 1 1 0 Running v1.32.0+vmware.6-fips # Crude and honest: nothing older than the edit should remain. kubectl get machine -n ns-platform-a –sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,AGE:.metadata.creationTimestamp
Wall clock cost of each update type Minutes from saving the edit to every Machine reporting Running. VCF 9.0, VKS 3.3.1, vks-lifecycle-01, three control plane plus three workers, guaranteed-medium. VKr 1.31 to 1.32 57 VM class change 38 Storage class change 34 Scale out by three 9 Scale out is shown in pale red because it replaces no nodes. Every red bar is a full node replacement cycle.

Stall remediation and fallback paths

A stalled roll looks identical to a slow one for the first ten minutes, which is exactly how long people wait before they start clicking things they should not. Two classes of stall dominate. Prechecks that refuse to let the update begin, and drains that refuse to let a node leave.

# Precheck refusal. Nothing rolls, no error is printed by kubectl edit. kubectl get cluster vks-lifecycle-01 -o json | jq ‘.status.conditions[] | select(.status=="False")’ { "type": "UpdatesAvailable", "status": "False", "reason": "MisconfiguredSoftwareDetected", "message": "cluster has misconfigured software preventing update" } # Fix the underlying misconfiguration first. Only if you have proven the check is # wrong should you override it, and the annotation name tells you how Broadcom feels: kubectl annotate cluster vks-lifecycle-01 kubernetes.vmware.com/dangerous-skip-misconfigured-software-check-for-update="" # Drain refusal. The new node is up, the old one will not leave. kubectl describe machine vks-lifecycle-01-node-pool-1-77lkc -n ns-platform-a Warning DrainFailed 4h12m Drain not completed yet: cannot evict pod as it would violate the pod disruption budget: pdb: pg-primary-pdb, namespace: data kubectl get pdb -n data NAME MIN AVAILABLE ALLOWED DISRUPTIONS AGE pg-primary-pdb 3 0 87d

ALLOWED DISRUPTIONS reading 0 is the whole story. A PDB with minAvailable set to 3 on a StatefulSet with exactly 3 replicas grants zero eviction budget forever, so Cluster API correctly refuses to evict, and correctly waits. Change minAvailable to 2, or express it as maxUnavailable, and the eviction happens within seconds. Part 20 met this same wall from the scaling side; the difference here is that a version update parks the whole cluster behind it rather than one node pool.

SymptomReal causeRemediation
Edit accepted, nothing happens, no new Machine appearsTarget VKr absent from UpdatesAvailable, or library not synced to this namespaceRead the UpdatesAvailable message, sync the content library, verify the VKr shows COMPATIBLE True
UpdatesAvailable False with MisconfiguredSoftwareDetectedPrecheck found unsupported add on or version skewFix the software first; override with the dangerous-skip annotation only when you can prove the check is wrong
DrainFailed, node cordoned for hours, roll frozenPodDisruptionBudget allows zero disruptionskubectl get pdb across all namespaces, relax minAvailable below replica count, drain resumes on its own
New Machine stuck Provisioning, never joinsVM class no longer bound to the vSphere Namespace, or storage policy removedRebind the VM class and storage policy on the namespace in vCenter, then delete the pending Machine to retry
Roll starts on clusters you did not touchContent library swapped and OVAs renamed, or VKS component upgradedTreat library changes as a change window, keep OVA names identical when migrating libraries
Application data gone after an overnight updateBare pods, not owned by a controller, deleted during worker drainAudit for pods without ownerReferences before any update; nothing recreates them

Rollback deserves a blunt sentence. There is no version downgrade. Editing spec.topology.version back to the older VKr is refused, and if you find a way to force it you will produce a cluster nobody supports. Your genuine fallback positions are, in order: stop the roll from progressing further by fixing whatever is blocking it and leaving the cluster in a mixed version state, which is supported for the duration of an update; restore workload state from Velero into a fresh cluster; or, for a configuration variable change only, edit the variable back to its previous value and accept a second full roll. Configuration is reversible. Kubernetes versions are not.

My worst version of this cost 4 hours and 12 minutes. I started a 1.31 to 1.32 update on a Thursday evening, watched the control plane roll cleanly, saw the first worker come up, and went to bed. At 06:20 the cluster still read Provisioned, VERSION still read the target, and MachineDeployment UPDATED read 1 of 3. One PostgreSQL StatefulSet in a namespace I did not own carried a PDB with minAvailable equal to its replica count. Cluster API had done exactly the right thing all night. Fixing it took forty seconds of yaml and one kubectl apply, and the remaining two workers rolled in 16 minutes. Total cost of the misconfiguration: four hours of nothing, and a change window I had to explain. Now the preflight on every cluster I touch starts with kubectl get pdb across all namespaces, before I look at anything else.

Exam focus for objective 4.6

Objective 4.6, Update VKS clusters through rolling updates and configuration changes: What this objective expects you to be able to do is predict behaviour, not recite syntax. Given a described change, say which nodes will be replaced, in what order, and whether the control plane is involved. Given a stalled cluster, name the object holding it up. Expect this to arrive as build list items asking you to order the stages of a roll, as matching items pairing a spec field with the nodes it replaces, and as hot area or point and click items on a screenshot of kubectl output where you must identify the field that proves progress. The trap that catches experienced admins is the VERSION column on the Cluster object. It is your own spec echoed back, and it flips before a single node moves. Anyone who has run kubectl for years reads that column as truth. On this exam, and in your next change window, the honest fields are UPDATED against REPLICAS, TopologyReconciled, and Machine age.

Objective checkpoint

Three original questions from the published objective wording. No real items are reproduced here.

1. A v1beta2 cluster has two worker node pools of two nodes each. An administrator changes the storage class variable. How many worker nodes are replaced at the same time?
Answer: two, one per node pool. Configuration variable updates roll one worker per node pool concurrently, unlike a version update which rolls one worker in total.

2. A cluster shows PHASE Provisioned and VERSION v1.32.0+vmware.6-fips two minutes after a version edit was saved. Which conclusion is supported?
Answer: none about node state. VERSION is populated from spec.topology.version, so it changes as soon as the edit lands. Node progress lives in the UPDATED column of KubeadmControlPlane and MachineDeployment.

3. An update has been running for three hours. A new worker is Running, an old worker is cordoned, and MachineDeployment UNAVAILABLE reads 0. What should be checked first?
Answer: PodDisruptionBudgets in the workload namespaces. Cluster API will not evict past a budget, so it cordons the old node and waits indefinitely rather than reporting an error.

Update discipline for this estate

My recommendation is narrow and unglamorous. Separate version changes from configuration changes into different windows, always, even when it doubles your number of change requests. A version update and a VM class change applied together produce a roll whose duration you cannot predict and whose failure you cannot attribute, because both stages replace the same nodes for different reasons. Run the version change alone, verify with UPDATED and Machine age, then run the configuration change the following day. On this estate that costs an extra window and buys a failure you can name in under a minute.

Avoid the opposite pattern, which is bundling everything into one manifest edit because it feels efficient. It is efficient right up until a drain stalls and you have three candidate causes and a 45 minute head start on nobody.

Clean result checklist: UpdatesAvailable was read before the edit and named the target VKr. TopologyReconciled returned to True. UPDATED equals REPLICAS on the KubeadmControlPlane and on every MachineDeployment. No Machine in the namespace is older than the edit. Cluster phase reads Provisioned and every Machine reads Running. kubectl get pdb across all namespaces shows ALLOWED DISRUPTIONS greater than zero everywhere. No bare pods were lost, because you audited for pods without ownerReferences first.

Tonight, in your own lab: create a PodDisruptionBudget with minAvailable equal to your replica count, start a VM class change, and watch a roll freeze on purpose. Then fix it and watch it resume without any intervention. Twenty minutes of that teaches objective 4.6 better than any amount of reading. If the underlying mechanics still feel thin, the VKS Series covers cluster lifecycle without the exam framing, and the VCAP-VKS exam guide maps every objective to its Part. Part 22 takes the next step and hands node count decisions to an autoscaler, which is objective 4.7.

VCAP-VKS Exam Series · Part 21 of 34
« Previous: Part 20  |  Guide  |  Next: Part 22 »

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