, ,

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

Scoring 47 TKGI namespaces on six signals turns a flat inventory into four migration waves. Here is the rubric, the wave rules, and why business criticality is the wrong sort key.

TKGI to OpenShift Series · Part 6 of 26

Business criticality is the wrong axis to sequence a migration on. Sort your namespaces by how badly they will fail OpenShift admission instead, and the wave order writes itself.

Who this is for: You finished the twelve field inventory from Part 5 and you are holding a spreadsheet with one row per namespace. Nothing is installed on the OpenShift Container Platform 4 (OCP) target yet. You need to turn that flat list into an ordered plan that tells you what moves in week one and what does not move until somebody rebuilds a container image. If you are still weighing VMware Kubernetes Service as the landing place, the TKGI to VKS guide covers that path, and the scoring model below still applies.
Key takeaways:
• Sequence by admission score, not by business criticality. Criticality tells you when downtime hurts, it does not tell you what breaks on restore.
• Six signals, each scored 0, 2 or 5, produce a number from 0 to 30 per namespace. That number is the wave.
• Headline command: kubectl get pods -A -o json | jq -r ‘.items[] | select((.spec.securityContext.runAsUser == 0) or ([.spec.containers[].securityContext.runAsUser] | index(0))) | .metadata.namespace’ | sort -u
• On our 47 namespace estate the split came out 19 lift, 16 adjust, 8 rebuild, 4 deferred. Three of the four deferred namespaces were retired rather than moved.
• Wave 1 must contain at least one production namespace. A Wave 1 built entirely from dev passes for the wrong reasons and teaches you nothing.

Scoring Every Namespace for Migration Cost

A migration inventory is a description. A wave plan is a decision. Getting from one to the other needs a scoring rule that anyone on the team can apply without asking you, because you are not going to be in the room for all 47 conversations.

Six signals do the work. Each one is scored 0, 2 or 5, and the six add up to a number between 0 and 30. I deliberately kept the scale coarse. A finer scale invites arguing about whether something is a 3 or a 4, and that argument produces no better plan than a coarse one. Every signal below traces back to a column you already filled in during discovery, so scoring 47 namespaces is an afternoon of spreadsheet work, not a new data collection exercise.

Admission posture carries the same maximum weight as everything else, but it is the signal that dominates in practice, because it is the only one that produces a hard rejection at the API server rather than a slow afternoon. A Security Context Constraint (SCC) is the OpenShift admission object that decides what a pod is allowed to ask for, and the default one granted to every authenticated user on a fresh install is restricted-v2. It drops all Linux capabilities from containers, forces seccompProfile to runtime/default, requires allowPrivilegeEscalation to be unset or false, and uses a MustRunAsRange strategy for runAsUser that reads the openshift.io/sa.scc.uid-range annotation off the project. A pod that hardcodes runAsUser 0 does not get a warning under that regime. It does not start.

SignalScore 0Score 2Score 5
Admission postureRuns as an arbitrary UID, no privileged container, no hostPath, no hostNetworkFixed non zero runAsUser, nothing else exoticrunAsUser 0, privileged, hostPath or hostNetwork anywhere in the pod spec
Persistent stateStateless, no PersistentVolumeClaimOne or more ReadWriteOnce claims, a cold copy is acceptableReadWriteMany, or a database that needs a consistent snapshot and a quiesce window
Ingress and load balancingClusterIP only, reached from inside the clusterPlain Ingress, one host and path, TLS from a SecretService type LoadBalancer backed by an NSX-T virtual server, or annotation heavy Ingress
Image provenanceBuilt from a Dockerfile you own, in a pipeline you can run todaySitting in Harbor, source available, no working pipelineVendor image you cannot rebuild, or a base image that only functions as root
API currencyNo deprecated API requests observed, manifests use stable groupsDeprecated on the source but still served by the target API serverUses an API version the target no longer serves at all
Coupling and blast radiusNo cross namespace dependency, nothing outside the cluster calls it directlyCalls one or two in cluster services by DNS nameShares a data store, is a cluster singleton, or something external depends on its IP

Six signal scoring rubric. Copy this beside your Part 5 worksheet and add one score column per signal plus a total.

Wave Assignment Rules and Wave Shapes

Four bands map the total onto a wave. Bands are not equal in width, because the cost of moving a namespace is not linear in its score. Going from 6 to 8 changes an afternoon into a day. Going from 14 to 16 changes a day into a conversation with a vendor about a release date.

WaveScoreWhat the work actually isOur count
Wave 1, Lift0 to 6Back up with Velero on TKGI, restore with OADP on OpenShift, change nothing but the Route host19 namespaces
Wave 2, Adjust7 to 14Manifest edits, an SCC decision, a StorageClass remap, a Route rewrite. No image work16 namespaces
Wave 3, Rebuild15 to 22Container image has to change. Someone builds, tests and re-certifies before anything moves8 namespaces
Wave 4, Defer or retire23 to 30Blocked on a third party, or genuinely not worth moving. Decide retire versus defer explicitly4 namespaces

Wave bands and the counts they produced on a three cluster TKGI 1.18 estate.

Wave 4 deserves a moment. Four namespaces landed there, and when we walked each one back to a named owner, three of them had no owner willing to fund a rebuild. Those three were switched off rather than migrated. A wave plan that never produces a retirement is a wave plan that has not asked hard enough, because every estate of this age carries something nobody has looked at in two years.

Contradicts the usual advice: Every migration playbook says start with dev. Do not build Wave 1 out of dev namespaces alone. Dev namespaces score low because nobody ever tightened them, not because they are portable, and they carry no real traffic, no real data volume and no real owner watching. A Wave 1 of pure dev completes in two days and proves nothing about your target. Put at least one production stateless namespace, with a named owner and real users, in the first wave. That is the run that finds the problems while you still have time to fix them.

Running the Score Against a Real Estate

Four of the six signals come straight from the Part 5 worksheet with no new commands. Two of them, admission posture and API currency, are worth re-running carefully, because both are easy to measure wrong in a way that flatters your estate.

# Versions this Part was written and tested against TKGI 1.18.5 source clusters on NSX-T kubectl v1.28.9, matched to our source cluster server version OpenShift Container Platform 4.19 as the target oc 4.19.9 OADP 1.5, which packages Velero 1.16 jq 1.7.1

Confirm your own server minor with kubectl version before you copy anything, because the Kubernetes version underneath TKGI shifts across patch levels and the deprecated API list moves with it.

Here is the measurement that surprised us. Our first admission sweep read only the pod level securityContext, which is where most examples put it, and it reported seven namespaces running as root. That number felt too good, so we re-ran it as a union of pod level and container level fields.

# Naive check, pod level securityContext only $ kubectl get pods -A -o json | jq -r ‘.items[] | select(.spec.securityContext.runAsUser == 0) | .metadata.namespace’ | sort -u | wc -l 7 # Correct check, pod level OR any container level $ kubectl get pods -A -o json | jq -r ‘.items[] | select((.spec.securityContext.runAsUser == 0) or ([.spec.containers[].securityContext.runAsUser] | index(0))) | .metadata.namespace’ | sort -u | wc -l 23

Seven became twenty three. A container level securityContext overrides the pod level one, so any manifest that sets runAsUser on the container and leaves the pod block empty is invisible to the naive query. Sixteen namespaces would have been scored 0 on the signal that matters most, dropped into Wave 1, and failed on first restore. Score that signal from the union or do not score it at all.

API currency is the other one worth measuring properly, and the honest answer here is uncomfortable. Reading the deprecation counter on the source tells you very little.

$ kubectl get –raw /metrics | grep apiserver_requested_deprecated_apis Error from server (Forbidden): forbidden: User "pranay" cannot get path "/metrics" # Fix, bind a role that grants the nonResourceURL $ cat <<EOF | kubectl apply -f – apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-reader rules: – nonResourceURLs: ["/metrics"] verbs: ["get"] EOF clusterrole.rbac.authorization.k8s.io/metrics-reader created $ kubectl get –raw /metrics | grep apiserver_requested_deprecated_apis apiserver_requested_deprecated_apis{group="flowcontrol.apiserver.k8s.io",removed_release="1.29",resource="flowschemas",version="v1beta2"} 1 apiserver_requested_deprecated_apis{group="flowcontrol.apiserver.k8s.io",removed_release="1.29",resource="prioritylevelconfigurations",version="v1beta2"} 1

Two control plane APIs and nothing from an application. That is not a clean bill of health, it is a measurement artifact. Your source cluster cannot report a deprecated API that it already removed, and PodSecurityPolicy in policy/v1beta1 stopped being served at Kubernetes 1.25, which is well behind where TKGI 1.18 sits. So every PodSecurityPolicy manifest still sitting in your Git repositories is silent on this counter while being completely dead on arrival. Score API currency by diffing your stored manifests against the target removal list, not by trusting a runtime counter on the platform you are leaving.

Estate split by migration wave Running pods per wave and the pods restricted-v2 would reject as written. 47 namespaces, 312 pods, TKGI 1.18 estate, August 2026. Running pods Rejected by restricted-v2 Wave 1 Lift 96 0 rejected, by construction Wave 2 Adjust 121 41 Wave 3 Rebuild 78 47 Wave 4 Defer 17 11 Wave 3 holds 25 percent of the pods and 47 percent of the admission risk. That concentration is what makes the wave order worth arguing about.
Admission risk is not spread evenly across the estate. It clusters in the namespaces that also need image work, which is why those two signals should never be scored independently of each other.

Sequencing Waves Around Dependencies

Scoring gives you four buckets. It does not give you an order inside them, and it does not stop you scheduling two halves of the same application a month apart. Three rules fix that, and all three are cheaper to apply now than to discover during a cutover weekend.

First, a shared data store is scored once and it drags everything that touches it into the same wave. If two namespaces read the same PostgreSQL instance, they are one migration unit regardless of what the rubric says about them individually. Second, anything a Wave 1 namespace calls must already be on OpenShift or must be reachable across the two platforms during transition. Cross platform reachability is a real design item, not an assumption, and it gets its own treatment in Part 20 when Routes and DNS cutover come up. Third, a Wave 3 rebuild starts the moment you have the score, not when the wave starts. Image work is the only item on this plan with a lead time you do not control.

flowchart TD
  A[Namespace row from the Part 5 worksheet] --> B{Admission signal scores 5}
  B -->|Yes| C{Image owner can rebuild before cutover}
  C -->|No| D[Wave 4, defer or retire]
  C -->|Yes| E[Wave 3, start image work now]
  B -->|No| F{Shares a data store}
  F -->|Yes| G[Merge with the other consumers, score once]
  G --> H[Wave 2, sequence as one unit]
  F -->|No| I{Any PVC or LoadBalancer service}
  I -->|Yes| H
  I -->|No| J[Wave 1, lift with Velero and OADP]
Wave assignment as a decision path. Admission is the first gate because it is the only one that can end in a retirement decision.
Note on network segmentation: If a namespace scores 0 on coupling but your NSX-T distributed firewall carries rules that reference its pod addresses, that segmentation does not travel with a Velero backup. Score it as a 2 and treat the rule set as a separate migration item. Part 16 covers the translation into OVN-Kubernetes NetworkPolicy, and the NSX Complete Guide has the source side detail.

Assessment Mistakes That Cost Us Time

What you seeCauseFix
Admission risk count looks implausibly lowjq read only .spec.securityContext and missed container level overridesUnion the pod level and container level fields, then recount from scratch
A namespace scores 0 but the pilot restore still fails admissionImage runs as root via a USER directive in the Dockerfile, not via runAsUser in the manifestInspect image config with skopeo or podman inspect, not just the pod spec
Deprecated API counter on TKGI reports almost nothingThe source already removed those API versions, so it cannot count requests for themDiff your stored manifests against the target removal list instead
Wave 1 lands in two days, Wave 2 takes five weeksWave 1 was built entirely from dev namespaces with no real traffic or ownersMove one production stateless namespace into Wave 1 and re-run the pilot
Two namespaces in different waves break each other on cutoverCoupling was scored per namespace rather than per data storeScore the data store once and pull every consumer into the same wave
Namespace count disagrees with the service catalogue by double digitsCatalogue tracks applications, and one application spans three namespacesReconcile on namespace as the unit and treat the catalogue as a naming hint

Six assessment failures we hit, and what each one actually was.

Field Note from a 47 Namespace Assessment

We built the first wave plan on business criticality, because that is what the steering group asked for and it made a clean slide. Tier 3 first, tier 2 second, tier 1 last. Six low criticality namespaces went into week one.

Four of the six failed. Not failed slowly, failed at admission, with pods stuck in a create loop and an event line naming restricted-v2 as the constraint that would not admit them. One of the four was a monitoring sidecar that mounted a hostPath to read node logs, which is a design that simply has no restricted-v2 equivalent and needs an SCC decision rather than a manifest tweak. Another was a vendor appliance whose image only starts as UID 0. We opened a case, and the vendor confirmed a non root image was on the roadmap six weeks out. That namespace sat in limbo for eleven days while we argued internally about granting anyuid to a third party image, which is a conversation you want to have during assessment, not during a migration window with a change record open.

Total cost of sorting by the wrong axis: nine working days, and a credibility hit with the steering group that took longer than nine days to repair. We rebuilt the model around the six signals above, re-scored all 47 namespaces in an afternoon, and the plan we produced on the second attempt survived contact with the estate. The four failures from week one landed in Wave 3 and Wave 4 where they belonged, and the vendor appliance was eventually deferred and then retired when its owning team could not justify the licence renewal.

Verdict: Score on admission posture and image provenance first, and let those two signals set the wave. Avoid criticality ordered waves entirely, and avoid a Wave 1 assembled only from dev namespaces. Criticality belongs in the cutover window conversation from Part 23, where it genuinely decides something. It has no place in deciding what is technically portable.

Score Before You Sequence

A wave plan is worth having only if it is falsifiable. Six signals, four bands, one number per namespace, and anyone on the team can check your arithmetic and disagree with a specific score rather than with the whole plan. That is the difference between a migration schedule people commit to and one they nod at.

What a finished assessment looks like: every namespace from the Part 5 worksheet carries six scores and a total, every Wave 3 namespace has a named image owner with a date, every shared data store appears exactly once with its consumers grouped, and Wave 1 contains at least one production namespace with real users. If any of those four are missing, you have a spreadsheet rather than a plan.

On Monday, open your inventory, add seven columns, and score the ten namespaces you think are easiest. My prediction is that at least two of them come out above 7, and those two are the most useful rows on the sheet, because they are the ones your instinct got wrong. Part 7 goes deep on the signal that dominated this whole exercise, Security Context Constraints and pod admission, and shows exactly what restricted-v2 rejects and which of the alternatives is the right answer.

TKGI to OpenShift 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