, ,

OpenShift Observability, Backup and Disaster Recovery (TKGI to OpenShift Series, Part 25)

A runbook for giving a freshly migrated OpenShift cluster real metric retention, restorable etcd snapshots and scheduled OADP application backups. Three recovery domains, three tools, and measured restore times for each.

TKGI to OpenShift Series · Part 25 of 26

Forty minutes after the 4.20.4 update landed, a capacity planner asked me for the p99 latency graph covering the previous fortnight, and I could not produce it. Prometheus was healthy, every Console dashboard rendered, and every one of them started at midnight that morning. Fourteen days of history had been sitting on an emptyDir volume, and the Machine Config Operator had rebooted the node underneath it.

Key takeaways:
• A migrated OpenShift Container Platform 4 (OCP) cluster has three separate recovery domains, and one backup tool covers exactly one of them. Application state goes to OADP, cluster state goes to etcd snapshots, telemetry goes nowhere unless you give it a persistent volume.
• Core platform monitoring ships with no persistent storage at all. Prometheus retains 15 days by default and loses all of it on a pod reschedule, which on OCP happens on every node update.
• Headline command, run on a control plane node only: /usr/local/bin/cluster-backup.sh /home/core/assets/backup.
• An etcd snapshot only restores a cluster on the same z-stream release it came from. Our 4.19.9 snapshots became worthless the moment Part 24 finished updating to 4.20.4.
• Do not turn on the automated etcd backup feature. It requires TechPreviewNoUpgrade, which permanently blocks minor version updates and cannot be switched off.
Who this is for: Platform engineers who have finished the migration. Production traffic is on OpenShift, TKGI (Tanzu Kubernetes Grid Integrated, formerly Enterprise PKS) is still powered on but idle, and the cluster now has to survive a bad Tuesday on its own. If you have not cut over yet, read Part 23 first.

Three Recovery Domains, Three Different Tools

On TKGI this question had a lazy answer. BOSH, the release engineering and VM lifecycle engine underneath TKGI, could recreate any VM it managed from its own database, and most teams backed up the BOSH director and the Tanzu Operations Manager installation, called it disaster recovery, and moved on. That answer worked because BOSH owned the machines. OpenShift does not work that way, and neither does the recovery story.

Break it into three domains and the tooling stops being confusing. Domain one is application state, meaning namespaces, Deployments, Secrets, ConfigMaps and the data on persistent volumes. That belongs to OADP (OpenShift API for Data Protection, the Operator that packages Velero), which we installed back in Part 17 to move workloads off TKGI and which now becomes a permanent operational service rather than a migration tool. Domain two is cluster state, meaning everything in etcd, the key value store that holds every resource object the API server knows about. That belongs to etcd snapshots and nothing else. Domain three is telemetry, meaning metrics, alerts and logs, which no backup product covers because losing it does not stop the platform, it only makes you blind.

Confusing the domains is how estates end up unprotected while believing they are covered. A weekly etcd snapshot does not contain one byte of your PostgreSQL data. An OADP backup of every namespace on the cluster does not restore a MachineConfig, an OAuth configuration, or a deleted ClusterRoleBinding. Both statements surprise people who ran TKGI, because on TKGI the platform and the workload were backed up by the same tile.

What brokeRecovery domainTool that fixes itMeasured recovery timeWhat it cannot do
Bad image rolled out to one DeploymentApplicationoc rollout undo2 minutesNothing outside that Deployment history
Namespace deleted by a bad pipeline runApplicationOADP restore from a scheduled Backup18 minutes, stateless namespaceRecover data written since the last backup
Database namespace lost with its volumesApplicationOADP restore including PVs2 hours 51 minutes for 240GiGuarantee a crash consistent database without a hook
Cluster wide RBAC or CRDs wipedClusteretcd restore to a previous cluster state4 hours 10 minutes, three control plane nodesRestore any persistent volume contents
One control plane node deadClusterReplace the unhealthy etcd member52 minutes, no API outageHelp once quorum is already lost
Metrics history gone after a node rebootTelemetryNothing, prevent it with a PVC and remote writeUnrecoverableBe fixed after the fact, ever
Recovery matrix for a migrated estate. Times measured on a twelve node OCP 4.20.4 cluster on vSphere.

Prerequisites and Preflight

Everything below assumes the estate we have been building since Part 12, now carrying production traffic after the cutover in Part 23 and running the release we updated to in Part 24. You need cluster-admin, a working default StorageClass backed by vSphere CSI (Container Storage Interface, the plugin that provisions vSphere volumes), and the same S3 compatible bucket OADP has been writing to since Part 17. Run the preflight before anything else, because two of these checks fail on a default install and both failures are silent.

# Tested against: OCP 4.20.4, oc client 4.20.4, Kubernetes 1.33, # OADP 1.5.0 from channel stable-1.5, Loki Operator 6.x, vSphere 8.0 U3. # Source estate for comparison: TKGI 1.18 with Velero 1.11. $ oc version –client Client Version: 4.20.4 # 1. Does platform monitoring have persistent storage? Empty output means no. $ oc -n openshift-monitoring get pvc No resources found in openshift-monitoring namespace. # 2. Is there a default StorageClass? Without one the PVC below stays Pending. $ oc get storageclass NAME PROVISIONER RECLAIMPOLICY AGE thin-csi (default) csi.vsphere.vmware.com Delete 64d # 3. Are all control plane nodes healthy before you snapshot anything? $ oc get etcd cluster -o jsonpath='{range .status.conditions[?(@.type=="EtcdMembersAvailable")]}{.message}{end}’ 3 members are available # 4. Is OADP still connected to its bucket? $ oc -n openshift-adp get backupstoragelocation NAME PHASE LAST VALIDATED AGE DEFAULT velero-sample-1 Available 41s 63d true

Check one is the finding that matters. An OpenShift cluster that has never been touched runs Prometheus and Alertmanager on emptyDir, which is a directory on the node that disappears with the pod. Red Hat documents persistent storage as highly recommended for production and as required for high availability on multi node clusters, but nothing in the installer or the Console warns you, and the stack looks perfectly healthy right up to the moment a node drains.

Steps 1 to 4, Storage and Retention for Platform Monitoring

Core platform monitoring is configured through a single ConfigMap named cluster-monitoring-config in the openshift-monitoring namespace. On a fresh cluster that ConfigMap does not exist, so step 1 is creating it. Steps 2 and 3 attach volume claim templates to Prometheus and Alertmanager and set retention explicitly rather than relying on the 15 day default. Step 4 sends a copy of the metrics somewhere the cluster cannot destroy.

# Step 1 to 3. Create or edit the ConfigMap. Applying this recreates the # Prometheus and Alertmanager StatefulSets, which is a short outage of the # monitoring stack itself. Nothing else on the cluster is affected. $ cat <<EOF | oc apply -f – apiVersion: v1 kind: ConfigMap metadata: name: cluster-monitoring-config namespace: openshift-monitoring data: config.yaml: | prometheusK8s: retention: 30d retentionSize: 34GB volumeClaimTemplate: spec: storageClassName: thin-csi volumeMode: Filesystem resources: requests: storage: 40Gi alertmanagerMain: volumeClaimTemplate: spec: storageClassName: thin-csi volumeMode: Filesystem resources: requests: storage: 20Gi EOF configmap/cluster-monitoring-config created # Verification. Two Prometheus replicas, two Alertmanager replicas, all Bound. $ oc -n openshift-monitoring get pvc NAME STATUS CAPACITY STORAGECLASS AGE alertmanager-main-db-alertmanager-main-0 Bound 20Gi thin-csi 3m alertmanager-main-db-alertmanager-main-1 Bound 20Gi thin-csi 3m prometheus-k8s-db-prometheus-k8s-0 Bound 40Gi thin-csi 3m prometheus-k8s-db-prometheus-k8s-1 Bound 40Gi thin-csi 3m

Two details in that manifest are deliberate. Raw block volumes are rejected outright because Prometheus cannot use them, so volumeMode must be Filesystem. And retentionSize is set to 34GB against a 40Gi claim rather than something closer to the ceiling, because compaction only runs every two hours and a volume can overshoot its retention size in between. Leave less headroom than that and the KubePersistentVolumeFillingUp alert starts firing at three in the morning about a volume that is behaving exactly as designed.

Here is the failure I hit on the first attempt, on a cluster where the vSphere CSI StorageClass had been created without the default annotation. Prometheus never came back, and the reason was two objects away from the pod that was complaining.

$ oc -n openshift-monitoring get pods | grep prometheus-k8s prometheus-k8s-0 0/6 Pending 0 6m14s $ oc -n openshift-monitoring describe pod prometheus-k8s-0 | tail -3 Warning FailedScheduling 6m default-scheduler 0/12 nodes are available: pod has unbound immediate PersistentVolumeClaims. preemption: 0/12 nodes are available: 12 Preemption is not helpful for scheduling. $ oc -n openshift-monitoring describe pvc prometheus-k8s-db-prometheus-k8s-0 | grep -i storageclass StorageClass: # Fix: name the class explicitly in the ConfigMap, or mark it default. $ oc patch storageclass thin-csi -p ‘{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}’ storageclass.storage.k8s.io/thin-csi patched

Step 4, Getting Logs Off the Cluster

Logging is a separate Operator on OpenShift, not part of the core stack. Install the Red Hat OpenShift Logging Operator and the Loki Operator, create a LokiStack backed by the same object storage bucket family OADP already uses, then create a ClusterLogForwarder using apiVersion observability.openshift.io/v1. One change from older releases catches every team migrating from a Fluentd based TKGI setup: the collector service account must be granted log collection permission explicitly, through the collect-application-logs, collect-infrastructure-logs and collect-audit-logs cluster roles. Skip that and the ClusterLogForwarder reports Ready while forwarding nothing at all.

If you already run a central log platform, and most estates leaving TKGI do, point the ClusterLogForwarder at it and skip LokiStack entirely. Running Loki in cluster only to ship the same lines onward twice is storage you pay for and an Operator you have to upgrade.

Steps 5 to 8, etcd Backups You Can Actually Restore From

etcd is where the cluster keeps itself. A snapshot of it plus the static pod resources is the only thing that will bring a cluster back after quorum is lost or after someone deletes something structural. Red Hat ships a wrapper script, cluster-backup.sh, maintained as part of the etcd Cluster Operator, and it produces two files: a snapshot database and a tarball of static pod resources that also carries the encryption keys when etcd encryption is on.

# Step 5. Take the snapshot from ONE control plane node. Not from each one. $ oc debug –as-root node/ocp-cp-1.lab.example.com Starting pod/ocp-cp-1labexamplecom-debug-p7vzq … sh-5.1# chroot /host sh-5.1# /usr/local/bin/cluster-backup.sh /home/core/assets/backup found latest kube-apiserver: /etc/kubernetes/static-pod-resources/kube-apiserver-pod-9 found latest kube-controller-manager: /etc/kubernetes/static-pod-resources/kube-controller-manager-pod-11 found latest kube-scheduler: /etc/kubernetes/static-pod-resources/kube-scheduler-pod-9 found latest etcd: /etc/kubernetes/static-pod-resources/etcd-pod-6 etcdctl version: 3.5.21 Snapshot saved at /home/core/assets/backup/snapshot_2026-08-14_021107.db {"hash":2214470118,"revision":1884213,"totalKey":41907,"totalSize":224382976} snapshot db and kube resources are successfully saved to /home/core/assets/backup # 214 MB, 41 seconds, on a twelve node cluster with 41907 keys. # The real failure, and it is the most common one. Wrong node. sh-5.1# /usr/local/bin/cluster-backup.sh /home/core/assets/backup sh-5.1: /usr/local/bin/cluster-backup.sh: No such file or directory # The script is placed only on control plane nodes by the etcd Cluster # Operator. Confirm which nodes those are before you start the debug pod: $ oc get nodes -l node-role.kubernetes.io/master= NAME STATUS ROLES AGE VERSION ocp-cp-1.lab.example.com Ready control-plane,master 64d v1.33.4 ocp-cp-2.lab.example.com Ready control-plane,master 64d v1.33.4 ocp-cp-3.lab.example.com Ready control-plane,master 64d v1.33.4

Step 6 is copying both files off the node, together, to storage that survives the cluster. Step 7 is the rule that catches everybody. A snapshot only restores a cluster running the same z-stream release that produced it. A 4.19.9 snapshot cannot restore a 4.20.4 cluster, which means every etcd backup you hold becomes scrap the moment an update completes. Take a fresh snapshot immediately before you start an update and another one immediately after it finishes, and write the release into the filename so nobody has to guess later. Step 8 is verifying the copy landed intact, because a truncated snapshot fails at restore time and not before.

Trap worth naming: OpenShift has a built in automated etcd backup feature, driven by an EtcdBackup custom resource, and it looks like exactly what you want. Turning it on requires setting the cluster feature set to TechPreviewNoUpgrade. That setting cannot be reversed, and it blocks every future minor version update on that cluster. Red Hat says plainly not to enable it on production. On a cluster you have just migrated a business onto, trading all future upgrades for a scheduled snapshot is a bad bargain. Schedule the script from outside the cluster instead, and revisit when the feature goes generally available.

Steps 9 to 12, Scheduled Application Backups with OADP

OADP arrived in this series as a migration tool. From here it is a backup product, and the change is mostly one object: a Schedule, which is a Velero custom resource carrying a cron expression and a backup template. Step 9 splits the estate into two schedules, because stateless namespaces and stateful ones deserve different frequencies and very different runtimes. Step 10 applies them. Step 11 sets a retention window with ttl. Step 12 restores something on purpose, into a scratch namespace, to prove the backups are real.

# Step 9 to 11. Two schedules. Note the real double hyphens in flags below. $ cat <<EOF | oc apply -f – apiVersion: velero.io/v1 kind: Schedule metadata: name: daily-app-config namespace: openshift-adp spec: schedule: 0 2 * * * template: includedNamespaces: – shop-frontend – shop-api – shop-worker snapshotVolumes: false ttl: 336h0m0s — apiVersion: velero.io/v1 kind: Schedule metadata: name: daily-app-data namespace: openshift-adp spec: schedule: 0 3 * * * template: includedNamespaces: – shop-db defaultVolumesToFsBackup: true ttl: 720h0m0s EOF schedule.velero.io/daily-app-config created schedule.velero.io/daily-app-data created $ oc -n openshift-adp get schedule NAME STATUS SCHEDULE LASTBACKUP AGE daily-app-config Enabled 0 2 * * * 8h 26h daily-app-data Enabled 0 3 * * * 7h 26h # Step 12. Restore into a scratch namespace and check what came back. $ velero restore create dr-drill-01 –from-backup daily-app-data-20260814030014 –namespace-mappings shop-db:shop-db-drill $ velero restore describe dr-drill-01 –details | head -8 Name: dr-drill-01 Phase: PartiallyFailed (run "velero restore logs dr-drill-01" for more) Errors: Namespaces: shop-db-drill: error restoring pods/shop-db-drill/postgres-0: pods "postgres-0" is forbidden: unable to validate against any security context constraint: [provider "restricted-v2": .spec.containers[0].securityContext.runAsUser: Invalid value: 1001]

That failure is the same Security Context Constraint problem this series has been warning about since Part 7, and it is worth seeing here rather than during a real incident. SCC is OpenShift admission control, and restricted-v2 is the default posture. A restore into a new namespace gets a new allocated UID range, so a manifest with a hardcoded runAsUser that was admitted in shop-db is rejected in shop-db-drill. Either drop runAsUser from the pod spec and let the namespace annotation supply it, or bind the workload service account to nonroot-v2. Details are in Part 19. The point for this Part is narrower: a restore is a fresh admission decision, so a backup that restores cleanly into its original namespace can still fail into a different one.

flowchart TD
  A[Incident declared] --> B{What is missing}
  B -->|One workload revision| C[oc rollout undo]
  B -->|Namespace or its data| D[OADP restore from schedule]
  B -->|Cluster scoped objects| E[etcd restore to previous state]
  B -->|One control plane node| F[Replace unhealthy etcd member]
  D --> G[Recheck SCC admission on restored pods]
  E --> H[Full API outage, plan hours not minutes]
  F --> I[No API outage, quorum preserved]
  C --> J[Service restored]
  G --> J
  H --> J
  I --> J
Recovery decision path. Reach for the etcd restore last, never first.

Verification, Rollback and Clean End State

Verification for backup work is unusual, because a green status field proves almost nothing. A Backup in phase Completed means Velero finished writing to a bucket. It does not mean the contents will start. Everything below is either a check that the plumbing exists or a check that a restore actually produced a running workload.

# Metrics survive a reschedule. Delete the pod, then query 20 days back. $ oc -n openshift-monitoring delete pod prometheus-k8s-0 $ oc -n openshift-monitoring exec prometheus-k8s-0 -c prometheus — promtool query instant http://localhost:9090 ‘count_over_time(up[20d])’ | head -2 up{instance="10.128.2.14:8443", job="kubelet"} => 57384 @[1786…] # Backups are landing and are not silently partial. $ oc -n openshift-adp get backup -o custom-columns= NAME:.metadata.name,PHASE:.status.phase,ERRORS:.status.errors | head -4 NAME PHASE ERRORS daily-app-config-20260815020011 Completed 0 daily-app-data-20260815030014 Completed 0 daily-app-data-20260814030014 Completed 0 # etcd is healthy and every member is on the same revision. $ oc -n openshift-etcd exec etcd-ocp-cp-1.lab.example.com -c etcdctl — etcdctl endpoint status –cluster -w table | grep -c false 0 # What green looks like: three Bound monitoring PVCs per replica set, two # Schedules Enabled with a LASTBACKUP under 25h, zero etcd endpoints # reporting an error, and one restore drill completed in the last 30 days.

Rollback here is refreshingly cheap, which is the one advantage of doing this work while TKGI is still powered on. Removing the volumeClaimTemplate stanzas from cluster-monitoring-config and reapplying puts the stack back on emptyDir within four minutes, at the cost of the history on those volumes. Deleting a Schedule stops future backups and leaves every existing Backup object and its bucket contents untouched. Neither action can take an application down. Only one operation in this Part is genuinely dangerous, and that is restoring etcd to a previous cluster state, which Red Hat describes as destructive and destabilising and a last resort. Treat it as the thing you do after the other three options have been ruled out, not the first lever you pull.

Measured recovery time by recovery path Wall clock minutes, twelve node OCP 4.20.4 on vSphere 8.0 U3, shop workload oc rollout undo OADP, stateless ns OADP, 240Gi stateful etcd previous state 2 18 171 250 0 60 120 180 240 minutes
Recovery time grows by two orders of magnitude across four paths. Pick the smallest one that fixes the problem.

Common Failures and Remediation

Error you seeActual causeFix
pod has unbound immediate PersistentVolumeClaimsNo default StorageClass and none named in the ConfigMapSet storageClassName explicitly, or annotate the class as default
cluster-backup.sh: No such file or directoryDebug session opened on a worker nodeRun it on a control plane node only, one node per backup
Restore leaves the cluster degraded, CVO reports a version mismatchSnapshot came from a different z-stream than the running clusterOnly ever restore from a snapshot of the same z-stream release
unable to validate against any security context constraintRestored namespace has a different UID range than the originalRemove hardcoded runAsUser, or bind the service account to nonroot-v2
Backup phase PartiallyFailed with a volume snapshotter errorNo VolumeSnapshotClass carrying the Velero CSI labelLabel the vSphere CSI VolumeSnapshotClass, or set defaultVolumesToFsBackup
Alertmanager silences disappear after a node updateAlertmanager still running on emptyDirGive alertmanagerMain a volumeClaimTemplate, not just Prometheus
ClusterLogForwarder Ready but no logs arriveCollector service account never granted collection rolesBind collect-application-logs and collect-infrastructure-logs to it
Failure to cause lookup for observability and backup setup on a migrated cluster.

Do I still need Velero on the TKGI side? Only until the last workload has moved and the last restore drill has passed. After that, retire it with the cluster in Part 26. Can OADP back up the OpenShift cluster itself? No. It backs up namespaced resources and volume data. Cluster scoped configuration belongs to etcd snapshots and to whatever Git repository holds your MachineConfigs. Does a bigger Prometheus volume buy longer history? Only alongside a longer retention setting. Retention time and retention size are both ceilings, and whichever is hit first wins.

Default Recovery Posture for a Migrated Estate

Field note: Losing fourteen days of metrics cost me a capacity review and about six hours of rebuilding a baseline from application logs, which was not fun and was not accurate. Worse was what I found while fixing it. Every etcd snapshot in our bucket had been taken on 4.19.9, and the cluster had been on 4.20.4 for two hours. For those two hours we had a production platform with a full backup catalogue and no valid restore point. Nobody had done anything wrong. Nobody had written down that an update invalidates the snapshots.

So here is the posture I now set on day one of any migrated cluster, before the first application arrives. Persistent volumes on Prometheus and Alertmanager with retention stated explicitly, because the default is amnesia. Remote write to something outside the cluster for anything you will need during an incident that takes the cluster down. Two OADP Schedules, split by whether volumes are involved, with a ttl that matches what compliance actually asked for. An etcd snapshot taken by an external scheduler, never by the Technology Preview feature, with the release baked into the filename and a fresh pair taken either side of every update. And one restore drill a month into a scratch namespace, on the calendar, owned by a name.

Teams landing on VMware vSphere Kubernetes Service instead of OpenShift face the same three domains with different plumbing, covered in the TKGI to VKS guide. Either way the lesson holds: on TKGI one tile protected the platform and its workloads together, and no platform you migrate to will do that again.

Do this on Monday. Run oc -n openshift-monitoring get pvc against your own cluster. If it says no resources found, you are one node drain away from the same phone call I took, and you can fix it in a single apply before lunch.

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

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