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.
• 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.
| Signal | Score 0 | Score 2 | Score 5 |
|---|---|---|---|
| Admission posture | Runs as an arbitrary UID, no privileged container, no hostPath, no hostNetwork | Fixed non zero runAsUser, nothing else exotic | runAsUser 0, privileged, hostPath or hostNetwork anywhere in the pod spec |
| Persistent state | Stateless, no PersistentVolumeClaim | One or more ReadWriteOnce claims, a cold copy is acceptable | ReadWriteMany, or a database that needs a consistent snapshot and a quiesce window |
| Ingress and load balancing | ClusterIP only, reached from inside the cluster | Plain Ingress, one host and path, TLS from a Secret | Service type LoadBalancer backed by an NSX-T virtual server, or annotation heavy Ingress |
| Image provenance | Built from a Dockerfile you own, in a pipeline you can run today | Sitting in Harbor, source available, no working pipeline | Vendor image you cannot rebuild, or a base image that only functions as root |
| API currency | No deprecated API requests observed, manifests use stable groups | Deprecated on the source but still served by the target API server | Uses an API version the target no longer serves at all |
| Coupling and blast radius | No cross namespace dependency, nothing outside the cluster calls it directly | Calls one or two in cluster services by DNS name | Shares 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.
| Wave | Score | What the work actually is | Our count |
|---|---|---|---|
| Wave 1, Lift | 0 to 6 | Back up with Velero on TKGI, restore with OADP on OpenShift, change nothing but the Route host | 19 namespaces |
| Wave 2, Adjust | 7 to 14 | Manifest edits, an SCC decision, a StorageClass remap, a Route rewrite. No image work | 16 namespaces |
| Wave 3, Rebuild | 15 to 22 | Container image has to change. Someone builds, tests and re-certifies before anything moves | 8 namespaces |
| Wave 4, Defer or retire | 23 to 30 | Blocked on a third party, or genuinely not worth moving. Decide retire versus defer explicitly | 4 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.
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.
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.
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.
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.
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.
Assessment Mistakes That Cost Us Time
| What you see | Cause | Fix |
|---|---|---|
| Admission risk count looks implausibly low | jq read only .spec.securityContext and missed container level overrides | Union the pod level and container level fields, then recount from scratch |
| A namespace scores 0 but the pilot restore still fails admission | Image runs as root via a USER directive in the Dockerfile, not via runAsUser in the manifest | Inspect image config with skopeo or podman inspect, not just the pod spec |
| Deprecated API counter on TKGI reports almost nothing | The source already removed those API versions, so it cannot count requests for them | Diff your stored manifests against the target removal list instead |
| Wave 1 lands in two days, Wave 2 takes five weeks | Wave 1 was built entirely from dev namespaces with no real traffic or owners | Move one production stateless namespace into Wave 1 and re-run the pilot |
| Two namespaces in different waves break each other on cutover | Coupling was scored per namespace rather than per data store | Score the data store once and pull every consumer into the same wave |
| Namespace count disagrees with the service catalogue by double digits | Catalogue tracks applications, and one application spans three namespaces | Reconcile 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.
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.
References
- Red Hat, Managing security context constraints, OpenShift Container Platform 4.18
- Red Hat, OADP application backup and restore, OpenShift Container Platform 4.19
- Kubernetes, Deprecated API Migration Guide


DrJha