, ,

Day-2 on OpenShift, Operators, Upgrades and Scaling vs BOSH (TKGI to OpenShift Series, Part 24)

Production traffic is on OpenShift, so now you own a platform that updates itself. Here is the runbook we use for a minor update, Operator channels, MachineSet scaling and health checks, with the timings and the two failures that cost us a window.

TKGI to OpenShift Series · Part 24 of 26
Key takeaways:
• Day-2 on OpenShift Container Platform 4 (OCP) is driven by the Cluster Version Operator and by Operators, not by a BOSH deployment and an Ops Manager Apply Changes.
• Our 4.19.9 to 4.20.4 update on a twelve node cluster took 3 hours 29 minutes wall clock, and 96 of those minutes were nothing but the Machine Config Operator rebooting nine workers one at a time.
• Headline command: oc adm upgrade --to 4.20.4, run after the admin acknowledgement is in place and never before it.
• Cluster updates do not roll back. Paused MachineConfigPools and a fresh etcd backup are the only fallbacks you actually have.
• Build new clusters on the current even numbered release. Odd numbered releases carry no Extended Update Support term at all.

Day-2 on OpenShift is not a lighter version of Day-2 on TKGI. It is a different job, with different failure modes, and in most organisations it ends up belonging to a different person. On TKGI (Tanzu Kubernetes Grid Integrated, formerly Enterprise PKS) the platform team owned a sequence of clicks: import a tile into Tanzu Operations Manager, import a stemcell, press Apply Changes, then let BOSH, the release engineering and VM lifecycle engine underneath TKGI, converge every virtual machine it manages. Nothing moved until a human started that sequence, and everything moved when they did.

OCP inverts that. A cluster carries roughly thirty named ClusterOperators that continuously reconcile their own piece of the platform, and a single object called clusterversion that says which release payload the whole thing should be running. You do not upgrade components. You change one field and then watch a fleet of controllers argue with reality until they win. That is a better model, and it is also a model that fails in ways an Ops Manager operator has never seen, usually at four in the morning, usually because of a PodDisruptionBudget somebody copied over during the migration.

Who this is for: production traffic already sits on OpenShift after the cutover in Part 23. Your reference estate is OCP 4.19 on vSphere, installed with the installer provisioned infrastructure method, three control plane nodes and nine workers across three MachineSets, OVN-Kubernetes as the container network, OAuth bound to the same LDAP directory that UAA used to read, and OADP (OpenShift API for Data Protection, the Red Hat packaging of Velero) still installed from Part 17. The old TKGI 1.18 estate is still powered on, which is a Part 26 problem. This part is about running what you now own.

Day-2 Ownership Moves From Ops Manager to Operators

Before any commands, it helps to put the two operating models side by side, because almost every argument I have had with a TKGI team in the first quarter after cutover traces back to one row in this table. Print it, put it in the runbook repository, and hand it to whoever is on call. A note on tooling while you are here: I use oc rather than kubectl throughout this series because oc understands OpenShift only resources such as Route, Project and SecurityContextConstraints, and because oc adm carries the cluster administration verbs that have no kubectl equivalent. Everything a kubectl user knows still works.

Day-2 taskHow you did it on TKGIHow you do it on OpenShift 4What actually changes
Patch the node operating systemImport a stemcell, Apply ChangesMachine Config Operator ships RHCOS inside the release payloadYou no longer pick an OS version separately from a Kubernetes version
Move to a new Kubernetes minorUpload a tile, then tkgi upgrade-cluster per clusterOne oc adm upgrade against the whole clusterA single object, clusterversion, drives control plane and nodes together
Add worker capacityEdit the plan, Apply Changes, wait for BOSHoc scale machineset or a MachineAutoscalerCapacity becomes a spec field instead of a tile change
Replace a dead nodeBOSH resurrector recreates the VM automaticallyMachineHealthCheck deletes the Machine, the MachineSet builds a new oneRemediation is opt in. Nothing self heals until you create the object
Add a platform capabilityNew tile, new BOSH release, another Apply ChangesSubscribe an Operator through OLM (Operator Lifecycle Manager)Capability lifecycle detaches from cluster lifecycle, and can drift
Check platform healthOps Manager status page plus bosh vmsoc get clusteroperatorsHealth is a list of named operators, not a list of virtual machines
Undo a platform changeRevert the tile configuration, Apply Changes againNo supported minor downgrade existsRollback stops being a button and becomes a restore procedure
Day-2 task mapping, TKGI to OpenShift 4. Row seven is the one that surprises people.

Row four deserves a sentence on its own. BOSH resurrected unresponsive VMs by default, so TKGI operators grew used to a platform that quietly fixed itself. OpenShift ships the machinery for that but does not switch it on. Until you write a MachineHealthCheck, a worker that loses its kubelet stays broken and stays in the inventory. I have watched three separate teams assume otherwise for weeks.

Preflight Before Any OpenShift Update

Everything below was run against OCP 4.19.9 moving to 4.20.4, with an oc client at 4.20.4, OADP 1.5 in the openshift-adp namespace, Velero 1.16 still installed on the TKGI 1.18 side, and vSphere CSI as the only storage provisioner. Match your client to the target release, not to the current one, or oc adm will hand you stale advice.

$ oc version Client Version: 4.20.4 Kubernetes Version: v1.32.7 $ oc get clusterversion version NAME VERSION AVAILABLE PROGRESSING SINCE STATUS version 4.19.9 True False 41d Cluster version is 4.19.9 $ oc get clusteroperators –no-headers | awk ‘$3!="True" || $4!="False" || $5!="False"’ (no output)

That last command is the single best preflight gate I know. It prints every ClusterOperator that is not Available, or is Progressing, or is Degraded. Empty output means the platform believes it is healthy. Start an update with even one line of output there and you are stacking a new failure on top of an old one, which turns a three hour window into a support case.

Next comes the part that has no TKGI equivalent at all. OpenShift will refuse to offer you the next minor release if your workloads are still calling Kubernetes APIs that the target release removes. You clear that in two moves: find the callers, then acknowledge the removal.

$ oc get apirequestcounts -o jsonpath='{range .items[?(@.status.removedInRelease!="")]}{.status.removedInRelease}{"t"}{.metadata.name}{"n"}{end}’ 1.33 flowschemas.v1beta3.flowcontrol.apiserver.k8s.io 1.33 prioritylevelconfigurations.v1beta3.flowcontrol.apiserver.k8s.io $ oc get apirequestcount flowschemas.v1beta3.flowcontrol.apiserver.k8s.io -o jsonpath='{range .status.currentHour.byUser[*]}{.username}{"t"}{.requestCount}{"n"}{end}’ system:serviceaccount:platform-tools:policy-sync 412 $ oc get configmap admin-acks -n openshift-config -o jsonpath='{.data}’ {"ack-4.19-kube-1.33-api-removals-in-4.20":"false"}

Read the acknowledgement key off your own cluster, every single time. That string encodes the release you are on, the Kubernetes minor whose APIs are going away and the release you are heading for, and it changes on every hop. Copying the key from a blog post, including this one, is how people end up patching a key that does not exist and then wondering why the update still refuses to start. Only once the byUser list is empty, meaning nothing is calling the doomed API any more, do you set it to true.

flowchart TD
  A[Read clusterversion and channel] --> B[List apirequestcounts with removals]
  B --> C[Fix callers, then set admin ack]
  C --> D[Set target channel]
  D --> E{Target recommended}
  E -->|yes| F[Start control plane update]
  E -->|no| G[Read the conditional risk]
  G --> F
  F --> H[MCO drains and reboots workers one by one]
  H --> I[Approve pending operator install plans]
  I --> J[Verify cluster operators and routes]
Minor update flow. Every gate before the control plane step is cheap. Everything after it is a reboot budget.

Cluster Update Procedure, 4.19 to 4.20 Step by Step

Step 1, take an etcd backup and prove it exists

This is not ceremony. Since there is no supported way to move a cluster back to an earlier minor, a snapshot taken minutes before you start is the difference between a bad night and a rebuild. Run the Red Hat supplied script on a control plane host and copy the two files off the node immediately.

$ oc debug node/ocp-cp-01 — chroot /host /usr/local/bin/cluster-backup.sh /home/core/assets/backup Starting pod/ocp-cp-01-debug … found latest kube-apiserver: /etc/kubernetes/static-pod-resources/kube-apiserver-pod-27 etcdctl version: 3.5.21 Snapshot saved at /home/core/assets/backup/snapshot_2026-08-14_020431.db {"level":"info","msg":"saved","size":"1.4 GB"} snapshot db and kube resources are successfully saved to /home/core/assets/backup

Step 2, set the channel and read what the graph offers

$ oc adm upgrade channel stable-4.20 $ oc adm upgrade Cluster version is 4.19.9 Upstream is unset, so the cluster will use an appropriate default. Channel: stable-4.20 (available channels: candidate-4.19, candidate-4.20, eus-4.20, fast-4.19, fast-4.20, stable-4.19, stable-4.20) Recommended updates: VERSION IMAGE 4.20.4 quay.io/openshift-release-dev/ocp-release@sha256:1f6d… 4.19.14 quay.io/openshift-release-dev/ocp-release@sha256:9ab2…

Two things to notice. Releases promoted to a stable channel land in the matching eus channel at the same moment, so choosing eus-4.20 over stable-4.20 changes nothing about which builds you can reach today. Its real purpose is the Control Plane Only update, formerly called an EUS to EUS update, which lets you go from one even numbered release to the next while rebooting worker nodes only once. That path only exists between even numbered releases, which is exactly why we are heading to 4.20 rather than sitting on 4.19.

Also watch for a Supported but not recommended updates block. Red Hat publishes conditional updates with a named risk and a reference link when a build is known to misbehave under specific conditions, and declares those risks across all channels at once. Read the risk. If it names a driver, a topology or a workload pattern you do not have, take the update. If it names vSphere CSI, stop and wait.

Step 3, start the control plane update

$ oc adm upgrade –to 4.20.4 Requested update to 4.20.4 $ watch -n 60 oc get clusterversion NAME VERSION AVAILABLE PROGRESSING SINCE STATUS version 4.19.9 True True 22m Working towards 4.20.4: 341 of 998 done (34% complete), waiting on machine-config

Once the percentage passes roughly seventy and the message names machine-config, the control plane is done and the Machine Config Operator has taken over. From here the clock belongs to your node count, because the operator cordons, drains, reboots and uncordons one worker at a time by default. Nine workers is nine sequential reboots whether the cluster is busy or idle.

Step 4, watch the pools rather than the percentage

$ oc get machineconfigpool NAME CONFIG UPDATED UPDATING DEGRADED MACHINECOUNT READYMACHINECOUNT UPDATEDMACHINECOUNT master rendered-master-7c41e0b9a2 True False False 3 3 3 worker rendered-worker-2f9d8c1a55 False True False 9 8 4

UPDATEDMACHINECOUNT should climb by one every eight to twelve minutes on vSphere. If it holds at the same number for more than twenty minutes with DEGRADED still False, you are almost certainly stuck in a drain, not in a reboot, and the next section explains why.

Operator Updates and Channel Discipline

Cluster updates and Operator updates are separate lifecycles, and that separation is where a migrated estate quietly rots. Every Operator you installed during the migration, OADP most of all, tracks its own channel through a Subscription object. If that subscription is set to automatic approval, an Operator can update itself on a Tuesday afternoon and take your backup path with it. Set every production subscription to manual and treat approvals as change tickets.

apiVersion: operators.coreos.com/v1alpha1 kind: Subscription metadata: name: redhat-oadp-operator namespace: openshift-adp spec: channel: stable-1.5 name: redhat-oadp-operator source: redhat-operators sourceNamespace: openshift-marketplace installPlanApproval: Manual — $ oc get csv -n openshift-adp NAME DISPLAY VERSION REPLACES PHASE oadp-operator.v1.5.2 OADP 1.5.2 oadp-operator.v1.5.1 Pending $ oc get installplan -n openshift-adp NAME CSV APPROVAL APPROVED install-x9k2t oadp-operator.v1.5.2 Manual false $ oc patch installplan install-x9k2t -n openshift-adp –type merge -p ‘{"spec":{"approved":true}}’ installplan.operators.coreos.com/install-x9k2t patched

A CSV, or ClusterServiceVersion, is the object that describes one installed Operator version. Seeing it sit at Pending after a cluster update is normal with manual approval and is not a fault. Seeing it sit at Pending with no InstallPlan at all is a fault, and it usually means the channel you pinned no longer carries an entry compatible with your new OCP minor. Fix that by moving the subscription to the channel the new catalogue actually offers, not by deleting the Operator, which on OADP would take your DataProtectionApplication and its backup schedules with it.

Ordering rule: approve Operator install plans after the cluster update finishes, not during it. An Operator that jumps a minor version while the Machine Config Operator is still rolling nodes will land some of its pods on old nodes and some on new ones, and any admission webhook it owns can then reject workloads in the gap. We lost eighteen minutes to exactly that with a service mesh Operator and now hold every approval until the worker pool reads Updated True.

Node Scaling, MachineSets and Health Checks

Scaling was covered as a build activity in Part 14. As a day-2 activity there are only two extra objects worth writing, and both of them replace behaviour BOSH gave you for free. A ClusterAutoscaler sets fleet wide ceilings, a MachineAutoscaler binds a range to one MachineSet, and the MachineAutoscaler only takes effect once a ClusterAutoscaler exists.

apiVersion: autoscaling.openshift.io/v1beta1 kind: MachineAutoscaler metadata: name: worker-zone-a namespace: openshift-machine-api spec: minReplicas: 3 maxReplicas: 6 scaleTargetRef: apiVersion: machine.openshift.io/v1beta1 kind: MachineSet name: ocp-prod-worker-zone-a — apiVersion: machine.openshift.io/v1beta1 kind: MachineHealthCheck metadata: name: worker-health namespace: openshift-machine-api spec: selector: matchLabels: machine.openshift.io/cluster-api-machineset: ocp-prod-worker-zone-a maxUnhealthy: 40% nodeStartupTimeout: 20m unhealthyConditions: – type: Ready status: Unknown timeout: 300s – type: Ready status: "False" timeout: 300s

Set maxUnhealthy deliberately. Left unset it defaults to one hundred percent, which means remediation proceeds no matter how much of the pool is unhealthy, and a vSphere storage incident that makes six workers go NotReady at once will cause OpenShift to delete six machines while the datastore is still misbehaving. Forty percent short circuits that: past the threshold the check stops remediating and waits for a human, which on a nine node pool means it will replace at most three at a time. Keep MachineHealthCheck away from control plane machines until you are comfortable, because deleting a control plane machine is an etcd member operation, not a reboot.

Verification, Rollback and Common Failures

A clean finish looks like this, and I check all four before I write the change record closed.

$ oc get clusterversion version NAME VERSION AVAILABLE PROGRESSING SINCE STATUS version 4.20.4 True False 14m Cluster version is 4.20.4 $ oc get clusteroperators –no-headers | awk ‘$3!="True" || $4!="False" || $5!="False"’ (no output) $ oc get machineconfigpool –no-headers | awk ‘{print $1, $3, $4, $5}’ master True False False worker True False False $ oc get nodes -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion –no-headers | sort -u -k2 ocp-cp-01 v1.33.3 ocp-w-09 v1.33.3

Now the uncomfortable part. Rollback of a completed minor update does not exist on OpenShift. You get three fallbacks and they degrade sharply. Before the Cluster Version Operator begins applying a requested target, oc adm upgrade --clear cancels it cleanly. Once the control plane is moving, pausing the worker MachineConfigPool with oc patch machineconfigpool worker --type merge -p and a paused true field freezes your nodes on the old operating system while you decide, which buys hours rather than minutes. Past that point the only route back is restoring the etcd snapshot from Step 1 across the whole control plane, which is a full disaster recovery procedure and costs far more downtime than the update ever would. Plan forward, not backward.

What you seeReal causeRemediation
4.20 never appears under Recommended updatesAdmin acknowledgement not set, or the channel is still stable-4.19Set the channel, clear the removed API callers, then patch the admin-acks key the cluster itself names
Worker pool frozen, MCO log shows Cannot evict pod as it would violate the pod's disruption budgetA PodDisruptionBudget with minAvailable 1 sitting on a single replica DeploymentFind it with oc get pdb -A, scale the Deployment to two replicas. Deleting the budget hides the design problem
ingress ClusterOperator goes Degraded, router pods PendingRouter replicas carry anti affinity and there are not enough schedulable workers mid rolloutKeep at least two schedulable workers outside the draining node, or add capacity before the window
OADP CSV stays Pending with no InstallPlan createdPinned Operator channel has no entry valid for the new OCP minorPatch the Subscription channel to one the catalogue offers, then approve. Never delete the Operator
Node stuck SchedulingDisabled long after its rebootA pod stuck Terminating, usually a finaliser or a hung CSI unmountForce delete that pod only. Force deleting the Machine while the volume is still attached corrupts the PVC binding
Pods Pending, autoscaler adds nothingMachineAutoscaler maxReplicas reached, or a request larger than the MachineSet instance sizeRead oc logs -n openshift-machine-api deploy/cluster-autoscaler-default, which names the reason directly
Failure to remediation lookup for an OpenShift minor update on vSphere.
Field note: our first post cutover update stalled at four workers of nine for 65 minutes. DEGRADED read False the whole time, so no alert fired and nobody looked. The Machine Config Operator log had been repeating one line since minute nine: Cannot evict pod as it would violate the pod's disruption budget. Behind it sat a PodDisruptionBudget with minAvailable 1 on a one replica build controller, carried over verbatim from TKGI during the CI/CD move in Part 21, where a permissive PodSecurityPolicy posture and a different drain behaviour had let it pass for years. Scaling that Deployment to two replicas released the drain in under ninety seconds. Cost: 65 of a 240 minute window, and a change record I had to explain twice.

With that fixed, the second attempt produced timings I now use for planning. They are worth plotting, because the shape tells you where to spend engineering effort and it is not where most people assume.

Where a minor update actually spends its window Wall clock minutes per phase, OCP 4.19.9 to 4.20.4, three control plane and nine worker nodes on vSphere Preflight and acks Control plane update Worker rollout, 9 nodes Operator install plans Verification 25 min 48 min 96 min 18 min 22 min 0 20 40 60 minutes Total 209 minutes. Same estate on TKGI 1.18, tile plus stemcell plus three upgrade-cluster runs: 442 minutes.
Forty six percent of the window is worker reboots. Optimising anything else is rearranging the small bars.

Half the window is worker reboots you cannot compress by being clever, which is a useful thing to know before somebody asks you to shorten the change window. What you can compress is how often you pay it, and that is a release selection decision rather than an operational one.

Pin New Clusters to Even Releases and Budget for Reboots

Here is where I disagree with what almost every installation walkthrough implies. We installed 4.19 because it was current on the day we built the cluster, and taking the newest release felt like the responsible choice. It was not. OpenShift ships a minor roughly every four months, gives every release eighteen months of Maintenance Support, and reserves Extended Update Support entirely for even numbered releases, where add on terms stretch a single release to twenty four, thirty six or forty eight months. Landing on 4.19 meant we had bought an eighteen month clock with no extension available, and our first day-2 task after a migration that took nine months was another migration of sorts. Build new clusters on the current even numbered release even if that means installing one minor behind, then ride the Control Plane Only path from even to even and reboot your workers once per hop instead of twice.

A clean day-2 posture, three months after cutover, reads like this. Every ClusterOperator Available and neither Progressing nor Degraded. Both MachineConfigPools Updated with zero degraded machines. Every production Subscription set to manual approval with a named owner. A MachineHealthCheck on each worker MachineSet with maxUnhealthy well below one hundred percent. An etcd backup taken and copied off cluster within the last twenty four hours. Channel set to the eus stream of the current even release. Zero apirequestcounts reporting a removedInRelease value. If any one of those is false, fix it before you plan the next update, because updates surface debt rather than creating it.

Worth one clause for completeness: if you are reading this while still choosing a destination and staying inside the VMware ecosystem matters more to you than the Operator model, the TKGI to VKS guide covers that landing place instead. And if OpenShift is now your platform and GPU workloads are on the roadmap, the Red Hat Gen AI guide picks up OpenShift AI and model serving from here. Part 25 takes on observability, backup and disaster recovery, which is the other half of owning this platform.

On Monday, run one command against your own cluster: oc get pdb -A. Every PodDisruptionBudget whose minAvailable equals the replica count of its Deployment is a node drain that will hang during your next update. Fix those now, while nothing is on fire.

TKGI to OpenShift Series · Part 24 of 26
« Previous: Part 23  |  Guide  |  Next: Part 25 »

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