, ,

Application Assessment and Migration Waves for TKGI to VKS (TKGI to VKS Series, Part 6)

Assess every TKGI application, pick between a Velero restore and a pipeline redeploy, and sort your workloads into migration waves that reach VKS on VCF 9 without a big-bang cutover.

TKGI to VKS Series · Part 6 of 26

Eighteen applications, three clusters, one that actually matters. That was the shape of the reference estate after the Part 5 inventory, and it matches most TKGI estates I have assessed: a long tail of services that move with almost no drama, and a short list that decides the entire timeline. Tanzu Kubernetes Grid Integrated (TKGI) is the platform you are leaving; the vSphere Kubernetes Service (VKS) on VMware Cloud Foundation 9 is where these apps land. Assessment is how you separate the easy tail from the timeline-setting few before you touch a single workload.

Key takeaways: Every application gets two decisions in this phase, a disposition (how it moves) and a wave (when it moves). Two patterns cover almost everything, a Velero backup-and-restore lift for hand-deployed apps, and a pipeline retarget redeploy for anything already under GitOps or CI/CD. Migrating a GitOps app with Velero is the most common mistake I see, and it starts fighting your pipeline within minutes. Stateful workloads, not stateless ones, set your schedule. Sequence low-risk and low-blast-radius apps first, and keep your most critical stateful app for last, not first.
Who this is for: A platform engineer or Kubernetes operator who has already inventoried the TKGI estate in Part 5 and now needs to decide what moves, how, and in what order, before VKS is stood up beside TKGI. Terms on first use: a disposition is the chosen method for moving one application; a migration wave is a batch of applications cut over together; stateless means the app keeps no local data and can be rebuilt from config; stateful means it owns data on a Persistent Volume Claim (PVC), a request for disk that Kubernetes binds to real storage; GitOps means the running state is driven from a Git repository by a controller such as Argo CD or Flux; Velero is an open source tool that backs up and restores Kubernetes objects and volume data; a CRD is a Custom Resource Definition, an API type an add-on installs into the cluster.

Two migration patterns, and when each fits

Almost every Kubernetes to Kubernetes move reduces to one of two patterns, and the whole assessment turns on which one a given app deserves. Pattern one is lift-and-shift: Velero reads the source Kubernetes API, bundles the objects into an S3-compatible bucket, and replays them into VKS. Because VKS is CNCF-conformant, the API details land cleanly on the far side. Pattern two is pipeline retarget: if the app already lives in Git and ships through Jenkins, Flux, Argo CD or Harness, you point that pipeline at the new VKS API endpoint and let it deploy a fresh instance. Same binary, new cluster, no backup tarball involved.

Choosing between them is not a taste question, it is a maturity question. Hand-deployed apps with no reliable Git source of truth are honest candidates for Velero, because the running cluster is the only place their full state exists. Anything under GitOps is the opposite case. VMware Cloud Foundation guidance is blunt about it: if you are already storing configuration in Git and deploying through a pipeline, do not use Velero for the application migration, because a restore breaks the link between your repository and the running cluster and immediately drifts from source. Retarget instead.

A migration is also the cheapest moment you will ever get to pay down platform debt. Retargeting a pipeline lets you adopt VCF-native habits on the way in, using VCF Automation to create Projects and self-service VKS clusters, and moving cluster add-ons like ingress and cert-manager under Argo CD or Flux. If that self-service angle is where you want to end up, the VCF Automation Series covers the tenancy plumbing in depth so this series does not have to repeat it.

FactorLift-and-shift (Velero restore)Pipeline retarget (redeploy)
Best whenhand-deployed, no reliable Git sourcealready GitOps or CI/CD driven
Source of truth afterthe running cluster, debt carried overGit, clean and reproducible
Effort per applow upfront, manual cleanup afterlow if pipeline already exists
Drift riskhigh, restore fights any controllerlow, controller owns state
Stateful datahandled via CSI snapshot backupneeds a separate data copy step
Verdictdefault for legacy, hand-built appsdefault for anything Git-managed

Neither pattern solves stateful data on its own, and pretending otherwise is how migrations slip. You cannot simply move a disk object between clusters, because a PVC is bound to a specific volume id and CSI driver in the source infrastructure. Data that already lives on external NFS or object storage is easy, you point the new instance at the same share. Block storage is the hard case, where you need a backup tool that supports CSI snapshotting so it copies the data itself, not just the manifest, and rehydrates a fresh volume on VKS during restore. That distinction, config versus data, is the reason wave 3 exists as its own category rather than folding into wave 2.

Disposition choices for a Kubernetes to Kubernetes move

Two patterns are the spine, but real estates need a slightly wider set of dispositions, because not every workload should move at all. Five choices cover the reference estate cleanly. Redeploy retargets a pipeline for Git-managed apps. Restore uses Velero for hand-deployed apps and for stateful data that must come across intact. Refactor is a redeploy that first fixes something blocking, most often a deprecated API version or an ingress controller that does not exist on the target. Retire deletes a workload the inventory turned up that no team will claim. Replace swaps a self-run component for a platform service, for example moving off a self-managed registry onto the VKS-integrated Harbor, or off self-run PostgreSQL onto Data Services Manager.

Retire earns special attention because it is where migrations get faster instead of slower. In the reference estate the Part 5 inventory surfaced a reporting service that nobody owned and no dashboard referenced. Cutting it removed an entire assessment thread. Every workload you retire is one you never have to test, cut over or roll back, so run the retire question first, before any disposition, on every namespace.

flowchart TD
  A[Application from inventory] --> B{Anyone owns it}
  B -- No --> R[Retire]
  B -- Yes --> C{In Git and pipeline driven}
  C -- Yes --> D{Uses a removed API or missing controller}
  D -- Yes --> F[Refactor then redeploy]
  D -- No --> G[Redeploy via pipeline]
  C -- No --> H{Owns data on a PVC}
  H -- No --> I[Restore with Velero]
  H -- Yes --> J{Better as a platform service}
  J -- Yes --> K[Replace with managed service]
  J -- No --> L[Restore with CSI snapshot]
Disposition decision path. Run the ownership question first, so retired workloads leave the plan before you spend effort assessing them.

Scoring an application for migration readiness

A disposition tells you how an app moves. A readiness score tells you how safely, and it is what orders the waves. I score six dimensions from 1 to 5, where 1 is trivial and 5 is dangerous: statefulness, automation maturity, dependency depth, blast radius if it breaks, API and CRD compatibility with the target Kubernetes version, and downtime tolerance during cutover. Sum them and the estate self-sorts. A stateless Git-managed service with no downstream consumers might score 8; the production database that everything depends on will score in the low twenties.

Three of those dimensions you can measure with quick discovery commands rather than guessing. Statefulness comes from listing PVCs, GitOps ownership comes from checking for controller annotations or Application objects, and API compatibility comes from scanning for deprecated versions against the target. Run these against each source cluster while you still have TKGI in front of you.

# versions tested: TKGI 1.18 source (Kubernetes 1.27), VKS on VCF 9 target (Kubernetes 1.31), Velero 1.18, kubectl 1.31 # which namespaces own persistent data (statefulness signal) kubectl get pvc –all-namespaces -o custom-columns=NS:.metadata.namespace,PVC:.metadata.name,SIZE:.spec.resources.requests.storage NS PVC SIZE shop-prod postgres-data-0 220Gi cache redis-data-0 8Gi messaging rabbitmq-data-0 20Gi # which apps are already GitOps managed (redeploy, do not Velero) kubectl get applications.argoproj.io -A -o custom-columns=NAME:.metadata.name,NS:.spec.destination.namespace NAME NS web-frontend shop-prod api-gateway shop-prod notifications shop-prod

Compatibility is the dimension that ambushes people, so screen for it now rather than at restore time. A quick pass with pluto against the target version surfaces objects that will not survive the jump from Kubernetes 1.27 to 1.31.

pluto detect-all-in-cluster –target-versions k8s=v1.31.0 NAME KIND VERSION REPLACEMENT DEPRECATED REMOVED legacy-ingress Ingress extensions/v1beta1 networking.k8s.io/v1 true true # Real failure if you skip this and let Velero try the restore anyway: # error restoring ingresses.extensions/legacy-ingress: the server could not find the requested resource # Fix: extensions/v1beta1 Ingress was removed at Kubernetes 1.22. Convert the manifest to # networking.k8s.io/v1 in Git and redeploy. That flips this app from Restore to Refactor.

That single check moved two apps in the reference estate from a planned Velero restore to a refactor-then-redeploy, and it caught them on a whiteboard instead of at 2 a.m. during a cutover. Velero will not restore into a cluster running a lower Kubernetes version than the source, and it cannot conjure an API that the target removed, so a compatibility scan is not optional homework, it is the difference between a clean wave and a half-restored namespace.

Grouping workloads into migration waves

With every app scored, waves fall out of the numbers. I use four. Wave 0 is a single throwaway pilot in non-prod, chosen precisely because losing it costs nothing, and its only job is to prove the toolchain and the target platform end to end. Wave 1 is the stateless production tier, the long tail that redeploys or restores with low blast radius. Wave 2 is stateful but recreatable, caches and queues that hold data you can rebuild or drain rather than preserve byte for byte. Wave 3 is the critical stateful core, the database and the app that cannot run without it, moved last and moved carefully.

Here is where the obvious approach is wrong. Plenty of teams sort waves by business criticality and, wanting an early win in front of leadership, put the flagship revenue app in wave 1 to prove the platform on something that matters. That inverts risk. Criticality should push a workload later, not earlier, because the whole point of wave 0 and wave 1 is to fail cheaply and learn before the expensive workload moves. Sort by a blend of statefulness and blast radius, and let importance decide lateness, not priority.

Downtime tolerance deserves its own line in the plan, because it quietly decides the technique inside each wave. A stateless app in wave 1 can usually run in both clusters at once during cutover, so its tolerance is generous and its rollback is a DNS change. A database in wave 3 often needs a quiet window to quiesce and snapshot cleanly, so its tolerance is measured in minutes and its rollback means pointing back at source data you deliberately kept read only. Write the acceptable window next to each app now, and the later cutover parts stop being guesswork about who can afford how much disruption.

Isolation is the other rule worth stating plainly. Running stateful workloads on the same cluster as stateless ones makes lifecycle management harder for the entire cluster, so where the target design allows it, land databases on dedicated VKS clusters. That choice shapes wave 3 more than any command does, and it is one reason the VKS Series is worth reading in parallel for the cluster-class and node-pool mechanics this series leans on rather than reteaching.

Sequencing the reference migration

Applied to the reference estate, the four waves carry very uneven load, and seeing the shape changes how you staff the effort. One pilot app in wave 0, eleven stateless services in wave 1, five recreatable stateful workloads in wave 2, and a single critical database app in wave 3. Wave 1 looks heavy but is mostly repetition of a known-safe procedure. Wave 3 is one app that will consume more planning than the other seventeen combined.

Applications per migration wave, reference estateeighteen apps, most of the risk in the last one061211151Wave 0 pilotWave 1 statelessWave 2 recreatableWave 3 criticalcount is not the same as effort, wave 3 outweighs the rest
Wave sizing for the reference estate. The tallest bar is the easiest work, and the shortest bar is the one that decides the schedule.
War story: On an early wave I lifted a stateless frontend across with Velero because it was quick, and I ignored the small argocd tracking annotation on it. The restore succeeded, the pods came up, and about ninety seconds later Argo CD noticed the new objects it had never deployed, flagged them out of sync, and started reverting them toward an old Git revision that still pointed at the source cluster. I spent close to six hours chasing a reconciliation loop before I accepted the obvious: that app should have been a pipeline retarget, not a restore. The fix was to delete the restored copy and let the pipeline deploy it fresh, which took four minutes. I now grep every namespace for controller annotations before I ever reach for Velero.

Verdict on sequencing: run wave 0 as a genuine dress rehearsal, not a demo. Pick something you would be willing to delete, and use it to time the Velero backup, measure the object-storage sync interval of one minute, and confirm the target storage class binds. Everything you learn there is free. Everything you skip there, you pay for in wave 3 with real data on the line.

Application disposition matrix

This is the artifact to keep and return to. One row per application, disposition and wave decided, key risk named. Build your own version of this table for your estate and it becomes the plan of record for every part that follows, from stateless cutover through the stateful database move. A representative slice of the reference estate looks like this.

ApplicationStateManaged byDispositionWaveKey risk
batch-worker (dev)statelesshand-deployedRestore0toolchain unproven
web-frontendstatelessArgo CDRedeploy1ingress class swap
api-gatewaystatelessArgo CDRefactor1removed API version
notificationsstatelessHelm and CIRedeploy1secret re-creation
redis-cachestateful, recreatableHelmRedeploy fresh2cold-cache latency
rabbitmqstateful, recreatablehand-deployedRestore, drain first2in-flight messages
harbor registrystatefulplatform teamReplace with VKS Harbor2image re-push
legacy-reportingstatelessno ownerRetiren/anobody claims it
shop-webstatelessArgo CDRedeploy3cutover coordination
shop-postgresstateful, 220Gihand-deployedRestore, CSI snapshot3data integrity, downtime

Notice that shop-web and shop-postgres share wave 3 even though the web tier is stateless and would score low on its own. An app that cannot function without its database inherits the database wave, because splitting them across waves means one of them runs blind to the other during the gap. Coupling like that is exactly what an assessment exists to surface before it becomes a cutover surprise.

Start with a throwaway, not your flagship

If you take one habit from this part, make it this: the first workload you move should be one you would happily delete, and the last should be the one your business cannot lose. Assessment is what lets you say which is which with a straight face, backed by a score and a disposition rather than a hunch. Do the boring work of scoring the estate now, while TKGI is still running and you can query it freely, and the hands-on parts ahead become procedure instead of improvisation.

Do this on Monday: Take your Part 5 inventory and add three columns, disposition, wave and readiness score. Run the pvc, argocd and pluto checks against each source cluster to fill them from evidence rather than memory. Circle every row that is GitOps-managed and mark it redeploy, not Velero. That single distinction prevents the most expensive mistake in the whole migration, and it takes an afternoon.
TKGI to VKS Series · Part 6 of 26
« Previous: Part 5  |  Guide  |  Next: Part 7 »

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