, ,

Storage and Data Assessment, vSphere CSI and Persistent Volumes (TKGI to OpenShift Series, Part 9)

A working method for inventorying persistent volumes on a TKGI estate, mapping storage classes onto OpenShift 4 vSphere CSI, and sizing restore windows before wave one starts. Includes the RWX trap and the storage class rename that leaves PVCs Pending forever.

TKGI to OpenShift Series · Part 9 of 26

Our first volume inventory across the three TKGI clusters came back with 47 PersistentVolumes and 3.4 TB provisioned. Twenty two of those volumes were Released, or bound to a namespace somebody had deleted months earlier, so a third of what looked like migration scope was data nobody had claimed in over a year.

Who this is for: You run a TKGI (Tanzu Kubernetes Grid Integrated Edition, formerly Enterprise PKS) estate on vSphere, you have already sorted applications into migration waves, and you now need to know what the persistent data actually costs to move. You have not installed OpenShift yet. Everything below is discovery and design work you can do this week against the clusters you already have.
Key takeaways: PersistentVolume objects do not migrate. Data migrates, and the PV is rebuilt on the far side by the OpenShift vSphere CSI driver, which means every StorageClass name baked into a PVC or a StatefulSet template is a rename waiting to strand you. ReadWriteOnce claims are routine. ReadWriteMany is the one that stops a wave, because vSphere CSI block volumes cannot serve RWX and the driver only provisions it when vSAN file services are enabled underneath. Restore duration is a network number, not a storage number, so size your cutover windows from sustained throughput and raise the OADP DataMover timeout before you find out the default is 10 minutes. Count your orphans first, because on our estate they were 47 percent of the volume list and zero percent of the workload.

Discovery Commands for a TKGI Volume Inventory

Everything in this part was checked against a specific set of versions. Storage behaviour drifts between minor releases more than most people expect, so pin what you are testing before you trust any output.

# tested against TKGI 1.19 on vSphere 8.0 Update 3, vSAN and VMFS datastores kubectl v1.30.x against the TKGI clusters OpenShift Container Platform 4.18, oc client 4.18 OADP 1.5 on the OpenShift side, Velero CLI 1.18 on the TKGI side vSphere CSI driver as shipped by the OCP vSphere CSI Driver Operator

One note on tooling before the commands. On the TKGI side I use kubectl, because that is all TKGI ships. On the OpenShift side I use oc throughout the rest of this series, because oc carries the project, route and SCC subcommands that kubectl has no concept of, and mixing the two in a runbook is how people end up filing bugs against the wrong binary. I will say that once and then stop mentioning it.

Start with a single wide listing of every PersistentVolume, because the default kubectl output hides the two columns that matter most, reclaim policy and the namespace that owns the claim.

kubectl get pv -o custom-columns="NAME:.metadata.name,CAP:.spec.capacity.storage,MODE:.spec.accessModes[0],RECLAIM:.spec.persistentVolumeReclaimPolicy,PHASE:.status.phase,NS:.spec.claimRef.namespace,CLAIM:.spec.claimRef.name,SC:.spec.storageClassName"

A trimmed sample of what came back on the production cluster:

NAME CAP MODE RECLAIM PHASE NS CLAIM SC pvc-2b41c0d9-7e2a-4f11-9a80-6f2c3d1e77aa 200Gi ReadWriteOnce Delete Bound shop-prod pgdata-shop-0 fast-vsan pvc-8c07e5b2-1d44-4b93-8f0e-2a9b7c4e1130 50Gi ReadWriteOnce Retain Bound shop-prod uploads-cache fast-vsan pvc-f19a6c88-33ab-42de-9c71-5b8e0a2d94f3 80Gi ReadWriteMany Delete Bound shop-prod shared-assets nfs-shared pvc-4d5e7a10-9b62-4c08-a7d3-118f6e2b5c47 100Gi ReadWriteOnce Delete Released analytics spark-scratch fast-vsan pvc-6a2f9b31-c084-4d75-b619-93e2f7a1d508 250Gi ReadWriteOnce Delete Released <none> <none> thin

Two rows in that sample are already telling you something. A Released phase means the claim is gone but the volume is still sitting on a datastore burning capacity, and a claimRef with no namespace means even the pointer back to an owner has been garbage collected. Neither belongs in a migration wave. Both belong on a decommission list you hand to the vSphere team later.

Next, separate the volumes by what actually provisioned them. TKGI estates that have been running for a few years usually carry a mix, some volumes created by the older in tree vSphere Cloud Provider and some by the vSphere CSI driver that TKGI installs into its clusters. Those two populations behave very differently when you try to move them.

# in tree vSphere Cloud Provider volumes kubectl get pv -o json | jq -r ‘.items[] | select(.spec.vsphereVolume != null) | [.metadata.name, .spec.vsphereVolume.volumePath] | @tsv’ # CSI provisioned volumes kubectl get pv -o json | jq -r ‘.items[] | select(.spec.csi != null) | [.metadata.name, .spec.csi.driver, .spec.csi.volumeHandle] | @tsv’
pv-legacy-jenkins-home [vsanDatastore] 5f2e8a01-77c4/kubernetes-dynamic-pvc-3ac9.vmdk pv-legacy-artifactory [vsanDatastore] 5f2e8a01-77c4/kubernetes-dynamic-pvc-9b71.vmdk pvc-2b41c0d9-…-6f2c3d1e77aa csi.vsphere.vmware.com 4a1c9f22-3e7b-11ee-9d0c-005056b0c1f4 pvc-8c07e5b2-…-2a9b7c4e1130 csi.vsphere.vmware.com 6d3b7e08-3e7b-11ee-9d0c-005056b0c1f4

Look at what the in tree rows contain. A datastore bracket, a folder UUID and a VMDK filename. That path is meaningful only to the vCenter and datastore the TKGI cluster was attached to. Nothing in it survives a move to a different cluster with its own CNS records, and upstream Kubernetes finished retiring the built in cloud provider integrations in the 1.31 line anyway, so the object shape you might have hoped to lift and drop is on its way out regardless. Treat in tree volumes as data with a filesystem in it, not as objects to be transplanted.

Finally, get the real number rather than the provisioned one. Provisioned capacity is what your finance model uses, but used capacity is what sets your restore window, and on our estate the gap was almost three to one.

# provisioned capacity per storage class kubectl get pv -o json | jq -r ‘.items[] | select(.status.phase=="Bound") | [.spec.storageClassName, .spec.capacity.storage] | @tsv’ | sort | uniq -c | sort -rn 9 fast-vsan 200Gi 6 fast-vsan 50Gi 4 nfs-shared 80Gi 3 thin 100Gi 3 thin 20Gi

For used capacity you have to ask inside the pod, because Kubernetes does not track filesystem consumption on a PV. A short loop over the running pods with df gives you a number you can plan against, and it is worth the ten minutes it takes.

Access Modes and ReadWriteMany Exposure

Access mode is where a storage assessment either finishes quietly or turns into a procurement conversation. ReadWriteOnce claims, which is most of what a TKGI estate carries, land on OpenShift without argument. Each one becomes a block volume on the vSphere CSI driver, attached to a single node at a time, exactly as it was.

ReadWriteMany is different, and it is different in a way that has a hard dependency outside your cluster. Red Hat is explicit about it in the OpenShift storage documentation: RWX provisioning through the vSphere CSI Driver Operator works only where the underlying vSphere environment has vSAN file services configured. Without that, a request for RWX does not degrade gracefully. Volume creation fails and an error is logged, and your PVC sits in Pending until somebody notices.

# find every RWX claim in the estate, per cluster kubectl get pvc –all-namespaces -o json | jq -r ‘.items[] | select(.spec.accessModes[] == "ReadWriteMany") | [.metadata.namespace, .metadata.name, .spec.storageClassName, .status.capacity.storage] | @tsv’ shop-prod shared-assets nfs-shared 80Gi ci-tooling jenkins-shared nfs-shared 40Gi

Two claims out of forty seven. That is a typical ratio and it is exactly why RWX gets missed. It looks like a rounding error in a spreadsheet, right up until wave one stalls on it. Here is what the failure looks like on the OpenShift side when the file backend is not there:

$ oc get pvc shared-assets -n shop-prod NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE shared-assets Pending thin-csi 6m14s $ oc describe pvc shared-assets -n shop-prod | tail -4 Events: Type Reason Age From Message —- —— —- —- ——- Warning ProvisioningFailed 12s (x9 over 6m) csi.vsphere.vmware.com failed to provision volume with StorageClass "thin-csi": rpc error: code = FailedPrecondition desc = ReadWriteMany volumes require vSAN file services to be enabled on the target datastore

You have three ways out, and the right one depends almost entirely on how much RWX you actually have. Enabling vSAN file services keeps everything inside the vSphere CSI driver you already run, and costs you a vSAN configuration change plus the file service appliances. OpenShift Data Foundation gives you CephFS and a much richer storage platform, and costs you a subscription and a real operational learning curve. An existing NFS export, wired in statically or through the CIFS and SMB CSI Driver Operator, costs you almost nothing and gives you almost nothing beyond a shared directory.

Verdict on RWX: Do not buy a storage platform to solve two claims. If RWX is under roughly 10 percent of your volume count and none of it is latency sensitive, point it at an existing NFS export and move on. Reach for OpenShift Data Foundation when you have object storage, block and file requirements together, or when RWX is structural to a dozen applications rather than incidental to two. Enabling vSAN file services is the middle option and the one most vSphere teams find easiest to approve, because it changes nothing about who owns the storage.

One more RWX detail that bites later rather than sooner. Resizing an RWX vSphere volume requires detaching the disk from every instance using it, which on a shared assets volume means stopping every pod that mounts it. Plan capacity generously on RWX at migration time, because growing it afterwards is an outage, not an operation.

flowchart TD A[PVC found in TKGI inventory] –> B{Bound to a live claim} B — No –> C[Decommission list, not migration scope] B — Yes –> D{Access mode} D — ReadWriteOnce –> E[Maps to thin csi block volume] D — ReadWriteMany –> F{File backend decided} F — No –> G[Blocked, escalate before wave one] F — Yes –> H[vSAN file services, ODF or NFS export] E –> I{Used data above 250 GB} I — Yes –> J[Raise timeouts, stage outside business hours] I — No –> K[Standard OADP restore in the wave window]
Triage path for every persistent volume claim found in the TKGI inventory

Storage Class Mapping from TKGI to OpenShift

Here is the artifact worth keeping from this part. Print it, stick it in your migration wiki, and fill the last column in with your own names. Every row below is a decision somebody has to make before the first stateful workload moves, and every row has at least one way to get it wrong quietly.

On TKGI OpenShift 4 equivalent Migration action Where it goes wrong
In tree volumes, provisioner kubernetes.io/vsphere-volumethin-csi, provisioner csi.vsphere.vmware.comMove the data, let OpenShift create a fresh PVvolumePath points at a datastore the OCP cluster may not even mount
Custom StorageClass with a storage policy nameCustom StorageClass with StoragePolicyName parameterRecreate by hand, do not restore the TKGI objectThe SPBM policy must be visible to the vCenter and datastore OpenShift uses
ReadWriteOnce PVC on vSAN or VMFSthin-csi block volume, ReadWriteOnceData moved by OADP file system backup or the CSI data moverthin-csi ships with allowVolumeExpansion set to false
ReadWriteMany PVC on NFS or vSAN file sharevSAN file services RWX, ODF CephFS, or an NFS or SMB CSI classChoose the file backend before wave one is scheduledNo backend means PVC Pending, not a fallback to RWO
Reclaim policy Retain on production dataSet reclaimPolicy Retain on your own StorageClassCreate a retained class alongside the defaultthin-csi defaults to Delete, so a namespace delete takes the data
CNS volume snapshots taken on TKGIVolumeSnapshotClass on the OpenShift clusterNothing, snapshots are not migration transportvSphere CSI allows 3 snapshots per volume by default, 32 at most
StatefulSet volumeClaimTemplatesIdentical StatefulSet with a rewritten class nameRestore the whole StatefulSet, never single PVCsThe class name is immutable inside the template once created

Now the failure that catches almost everyone on their first restore, because it produces no error from Velero at all. Your TKGI PVCs name a class called fast-vsan. OpenShift has thin-csi. Velero happily recreates the PVC exactly as it was backed up, the API server accepts it, and then nothing happens forever.

$ oc get pvc pgdata-shop-0 -n shop-prod NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pgdata-shop-0 Pending fast-vsan 21m $ oc describe pvc pgdata-shop-0 -n shop-prod | grep -A2 Events Events: Type Reason From Message Warning ProvisioningFailed persistentvolume-controller storageclass.storage.k8s.io "fast-vsan" not found

Velero has a built in restore item action for precisely this, driven by a labelled ConfigMap rather than a flag. Create it once, before your first restore, and every PVC and PV in every future restore gets rewritten on the way in.

apiVersion: v1 kind: ConfigMap metadata: name: change-storage-class-config namespace: openshift-adp labels: velero.io/plugin-config: "" velero.io/change-storage-class: RestoreItemAction data: fast-vsan: thin-csi thin: thin-csi nfs-shared: ocs-storagecluster-cephfs
Namespace trap: Upstream Velero documentation tells you this ConfigMap must live in the velero namespace. Under OADP (OpenShift API for Data Protection, which is Red Hat packaging of Velero) the operand runs in openshift-adp, and a ConfigMap in a namespace called velero is simply never read. The labels are what Velero matches on, but only inside its own namespace. This is a five minute fix that costs an afternoon if you copy the upstream YAML unchanged.

Restore Windows and Data Movement Sizing

Here is where I disagree with almost every migration plan I have reviewed. Teams reach for snapshots, because snapshots are instant and every storage vendor has trained them to think that way. On a TKGI to OpenShift move, snapshots are the one transport that cannot work. A vSphere CNS snapshot belongs to the source cluster and its CNS records. It is not an object another cluster can consume as a data source, and no amount of RBAC makes it one.

What actually moves data is a copy through object storage, either file system backup with Kopia or the CSI snapshot data mover, which snapshots locally and then uploads the snapshot content. Both write to a bucket and read it back on the far side. That makes restore duration a function of sustained throughput and concurrency, not of how fast your all flash vSAN can clone a VMDK. Storage teams consistently underestimate this by an order of magnitude.

Restore duration by volume size Measured at 110 MB per second sustained, single node agent, on premises S3 target 0 40 80 120 160 minutes OADP DataMover default resourceTimeout, 10 min Red Hat guidance for a 500 GB volume, 60 min 8 16 39 78 155 50 GB 100 GB 250 GB 500 GB 1 TB Used data per volume, not provisioned capacity
Restore duration crosses the OADP DataMover default timeout at roughly 60 GB of used data

Read the red line carefully, because it explains a class of failure that looks like corruption and is not. Red Hat documents the DataMover default resourceTimeout as 10 minutes, and recommends raising it to 60 minutes for a 500 GB volume. At the throughput we measured, anything above roughly 60 GB of used data blows through the default. What you see is a restore that reports failure while the data is still flowing, and an engineer who assumes the backup is bad.

Keep these defaults in front of you when you size a window.

Setting Default What to plan for
DataMover resourceTimeout10 minutes60 minutes for a 500 GB volume, per Red Hat guidance
Velero file system backup restore timeout4 hoursRaise with –fs-backup-timeout above about 1.5 TB per restore
Kopia node agent resourcesUnbounded20 cores and 32 Gi handled over 100 GB in Red Hat testing, set limits
thin-csi allowVolumeExpansionfalseCreate your own class with expansion on before wave one
thin-csi volumeBindingModeWaitForFirstConsumerA restored PVC stays Pending until a pod schedules, which is correct
vSphere CSI snapshots per volume3Up to 32, though 2 or 3 performs best

That WaitForFirstConsumer row deserves a sentence of its own, because it changes what a healthy restore looks like. On TKGI with immediate binding, a restored PVC binds straight away and you know within seconds whether storage worked. On OpenShift with thin-csi, a freshly restored PVC sitting in Pending with no events is the expected state until the workload is scheduled. Half the storage escalations I saw during our pilot were people looking at a perfectly healthy Pending PVC.

Pod admission interacts with all of this too. A restored database pod that wrote as root on TKGI will be rejected by the default restricted-v2 Security Context Constraint on OpenShift, and the volume it never mounted will look like a storage problem in your incident channel. I covered that mechanism in detail in Part 7 on Security Context Constraints, and the fsGroup behaviour there is worth rereading before you move any stateful workload.

Field Note from a 47 Volume Assessment

We ran this assessment across dev, staging and production over four days. Provisioned capacity came to 3.4 TB. Used capacity, once we had walked every running pod with df, came to 1.1 TB. Twenty two of the 47 volumes were Released or orphaned, which took another 640 GB out of scope before we had moved a single byte. That reframed the whole conversation with the infrastructure team, who had budgeted a migration window sized on the 3.4 TB number.

Then we found the RWX claim. An 80 GB shared assets volume, mounted by four replicas of the web tier, holding customer uploaded images. Nobody had flagged it in the application assessment because from the developer side it was just a directory. We found it six days before the pilot cutover, and enabling vSAN file services on the target datastore needed a change window that did not exist inside those six days. Pilot slipped nine days.

I had originally put OpenShift Data Foundation in the target design specifically to cover RWX. Once we knew it was one claim of 80 GB, that decision looked ridiculous. We reversed it, mounted an existing NFS export through a static PV, and saved a subscription line plus every hour of operational learning that comes with running Ceph. If we had run the RWX query in week one rather than week seven, the nine day slip would have been a Tuesday afternoon.

Worth saying plainly, because a reader in this position has a genuine fork in front of them. If your organisation is committed to staying on the VMware stack and VCF 9 is on your roadmap, VMware vSphere Kubernetes Service is a different landing place with a different storage story, and I cover it separately in the TKGI to VKS guide. Everything from here on assumes you have already made the OpenShift call.

Drop Orphaned Volumes Before Sizing Waves

My recommendation for this part is narrow and it is the one thing I would insist on. Do not size a single migration window until you have separated bound volumes from orphaned ones and used capacity from provisioned capacity. On our estate those two corrections cut the data movement problem from 3.4 TB to 1.1 TB, and then to under 500 GB once the orphans were gone. That is the difference between a weekend cutover and a three week programme, and it costs you an afternoon of kubectl.

After that, run the RWX query on every cluster before you finalise the target architecture, because it is the only storage finding that can add a procurement decision to your critical path. Everything else on the list, class renames, expansion flags, reclaim policies and timeouts, is configuration you can fix in an hour once you know about it.

A clean result for this part looks like four artifacts on your wiki: a per cluster volume inventory with phase and used size, a decommission list of orphans with owners named, a completed storage class mapping table, and a signed off decision on the RWX backend. If any of those four is missing when wave one starts, storage will be what delays you.

On Monday, run the two jq queries in this part against your busiest TKGI cluster and count how many PersistentVolumes come back Released. Whatever that number is, it is capacity you are paying for and workload you do not have to migrate. Next part builds the target reference architecture that all of this feeds into, and the volume numbers you collect today are an input to it. Identity and multi tenancy design came earlier in the series guide if you need to catch up on ordering.

TKGI to OpenShift Series · Part 9 of 26
« Previous: Part 8  |  Guide  |  Next: Part 10 »

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