OCP Cluster Upgrade — Best Practices Guide
The Golden Rule
Never upgrade a sick cluster. Every check below exists because upgrading on top of an existing problem turns a recoverable issue into a potential outage.
Phase 1 — Pre-Upgrade Preparation (Days Before)
1.1 Know Your Upgrade Path
OCP does not support arbitrary version jumps. Upgrades must follow a validated graph:
4.12 → 4.13 → 4.14 ✅ valid sequential path4.12 → 4.14 ❌ may not be supported (check the graph)4.14.5 → 4.14.12 ✅ z-stream (patch) always OK4.14.12 → 4.15.0 ✅ minor version (must check graph)
Always verify the upgrade path first:
# Check available updates from the official graphoc adm upgrade# Output example:# Recommended updates:# VERSION IMAGE# 4.14.13 quay.io/openshift-release-dev/ocp-release@sha256:...# 4.14.14 quay.io/openshift-release-dev/ocp-release@sha256:...
Use the official upgrade path tool:
https://access.redhat.com/labs/ocpupgradegraph/update_path
1.2 Read the Release Notes
For every version you are jumping through:
https://docs.openshift.com/container-platform/<version>/release_notes/ocp-<version>-release-notes.html
Look specifically for:
- Deprecated APIs — if your workloads use removed APIs, they break post-upgrade
- Known issues — bugs that affect your environment
- Operator compatibility — OLM-managed operators may need separate upgrades
- Behavior changes — network policy, SCC, authentication changes
1.3 Check Operator Compatibility (OLM)
Operators installed via OLM have their own upgrade lifecycle, separate from OCP:
# List all installed operators and their current versionsoc get csv -A# Check operator subscription channelsoc get subscription -A -o custom-columns=\'NAME:.metadata.name,CHANNEL:.spec.channel,SOURCE:.spec.source'
Check the OperatorHub or vendor docs to confirm your operators support the target OCP version before upgrading the platform.
1.4 Check for Deprecated API Usage
OCP removes old API versions between minor releases. If your workloads use removed APIs they will fail silently after upgrade:
# Check for deprecated API usage in the clusteroc get apirequestcounts -o json | jq -r \ '.items[] | select(.status.removedInRelease != null) | select(.status.requestCount > 0) | "\(.metadata.name) → removed in \(.status.removedInRelease) | used \(.status.requestCount)x"'
Any resource showing removedInRelease with a non-zero requestCount must be migrated before upgrading.
1.5 Validate etcd Health
etcd is the most critical component. A degraded etcd during upgrade can corrupt cluster state:
# Check etcd cluster operatoroc get co etcd# Check etcd member statusoc get etcd -o jsonpath=\ '{.items[0].status.conditions[?(@.type=="EtcdMembersAvailable")].status}'# Expected: True# Check etcd pods directlyoc get pods -n openshift-etcd -l app=etcd# Check etcd endpoint healthoc rsh -n openshift-etcd $(oc get pods -n openshift-etcd -l app=etcd \ -o jsonpath='{.items[0].metadata.name}') \ etcdctl endpoint health \ --cluster \ --cacert /etc/kubernetes/static-pod-certs/configmaps/etcd-serving-ca/ca-bundle.crt \ --cert /etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-peer-$(hostname).crt \ --key /etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-peer-$(hostname).key
Expected output:
https://192.168.1.10:2379 is healthy: committed revision: 294858, took: 3mshttps://192.168.1.11:2379 is healthy: committed revision: 294858, took: 4mshttps://192.168.1.12:2379 is healthy: committed revision: 294858, took: 3ms
1.6 Verify All Nodes Are Ready
# All nodes must be Ready with no scheduling issuesoc get nodes# Check for any taints blocking schedulingoc get nodes -o custom-columns=\'NAME:.metadata.name,STATUS:.status.conditions[-1].type,TAINTS:.spec.taints'# Check node resource pressureoc adm top nodes
1.7 Check All Cluster Operators
# All must be: AVAILABLE=True PROGRESSING=False DEGRADED=Falseoc get co# Quick one-liner to find any not healthy:oc get co -o json | jq -r \ '.items[] | select( (.status.conditions[] | select(.type=="Degraded" and .status=="True")) or (.status.conditions[] | select(.type=="Available" and .status=="False")) ) | .metadata.name'# Expected: empty output
1.8 Check MachineConfigPools
# All pools must be: UPDATED=True UPDATING=False DEGRADED=Falseoc get mcp# Check for paused pools (they won't upgrade automatically)oc get mcp -o jsonpath='{.items[?(@.spec.paused==true)].metadata.name}'
1.9 Review PodDisruptionBudgets
# Find PDBs that currently block all disruptionsoc get pdb -A -o json | jq -r \ '.items[] | select(.status.disruptionsAllowed == 0) | "\(.metadata.namespace)/\(.metadata.name) — minAvailable:\(.spec.minAvailable)"'
For each blocking PDB, verify whether it will open up during drain (more replicas than minAvailable) or whether it is misconfigured and will permanently block node drains.
1.10 Check Active Alerts
# Via AlertManager routeALERTMANAGER=$(oc get route alertmanager-main \ -n openshift-monitoring \ -o jsonpath='{.spec.host}')TOKEN=$(oc create token prometheus-k8s -n openshift-monitoring)curl -sk -H "Authorization: Bearer $TOKEN" \ "https://${ALERTMANAGER}/api/v2/alerts" | \ jq -r '.[] | select(.labels.severity=="critical") | .labels.alertname'
No critical alerts should be firing before upgrade begins.
Phase 2 — The Day Before
2.1 Take an etcd Backup
This is non-negotiable. If the upgrade corrupts cluster state, this is your only recovery path:
# SSH to a master nodessh core@master-0# Run backupsudo /usr/local/bin/cluster-backup.sh /home/core/backup# Verify both files createdls -lh /home/core/backup/# snapshot_<timestamp>.db# static_kuberesources_<timestamp>.tar.gz# Copy off-cluster immediately (S3, NFS, bastion host)scp core@master-0:/home/core/backup/* bastion:/backups/pre-upgrade/
The backup must be from the same z-stream as the running cluster. A 4.14.12 cluster backup cannot restore a 4.15.x cluster.
2.2 Notify Stakeholders
Upgrades cause rolling node reboots. Even with zero downtime on properly configured workloads, communicate:
- Upgrade window start/end time
- Which node groups reboot and in what order (masters first, then workers)
- Expected impact on any workloads that are not highly available
2.3 Scale Down Non-Critical Workloads (Optional)
For resource-constrained clusters, reducing workload during upgrade gives the upgrade controller more room to drain nodes:
# Note current replicas first, then scale downoc get deployments -A --no-headers | \ awk '{print $1, $2, $3}' > /tmp/pre-upgrade-replicas.txt# Scale down non-critical namespacesoc scale deployment --all -n non-critical-namespace --replicas=0
Phase 3 — Initiating the Upgrade
3.1 Choose the Right Channel
OCP upgrade channels control which versions are offered:
stable-4.14 → production-recommended, fully validatedfast-4.14 → validated but released sooner than stablecandidate-4.14 → pre-release, NOT for productioneus-4.14 → Extended Update Support (long-term maintenance)
# Check current channeloc get clusterversion -o jsonpath='{.items[0].spec.channel}'# Set channel if neededoc patch clusterversion version \ --type merge \ --patch '{"spec":{"channel":"stable-4.14"}}'
3.2 Trigger the Upgrade
# Upgrade to the latest available in channeloc adm upgrade --to-latest=true# Upgrade to a specific version (recommended — explicit and auditable)oc adm upgrade --to=4.14.13# For disconnected clusters (specify the full image digest)oc adm upgrade \ --to-image=quay.io/openshift-release-dev/ocp-release@sha256:<digest> \ --allow-explicit-upgrade
3.3 Monitor the Upgrade Progress
# Watch CVO progress — the top-level viewwatch oc get clusterversion# More detailoc describe clusterversion version | grep -A 30 "Conditions:"# Watch all cluster operators rolling through upgradewatch oc get co# Watch nodes rebooting (masters first, then workers)watch oc get nodes# Tail CVO operator logs for real-time detailoc logs -n openshift-cluster-version \ $(oc get pods -n openshift-cluster-version \ -o jsonpath='{.items[0].metadata.name}') -f
3.4 Understand the Upgrade Sequence
OCP upgrades in a strict order — never all at once:
1. CVO downloads new release image │ ▼2. Control plane operators update (etcd, kube-apiserver, controller-manager, scheduler) One at a time, with health checks between each │ ▼3. Master nodes drain → reboot → rejoin One at a time (never all 3 simultaneously) │ ▼4. Cluster operators update (authentication, console, ingress, monitoring, etc.) │ ▼5. Worker nodes drain → reboot → rejoin Controlled by MachineConfigPool maxUnavailable setting │ ▼6. CVO marks upgrade complete clusterversion: Progressing=False, Available=True
3.5 Control Worker Upgrade Speed
By default only 1 worker upgrades at a time. For large clusters you can safely increase this:
# Check current maxUnavailable (default is 1)oc get mcp worker -o jsonpath='{.spec.maxUnavailable}'# Allow up to 20% of workers to upgrade simultaneouslyoc patch mcp worker \ --type merge \ --patch '{"spec":{"maxUnavailable":"20%"}}'
⚠️ Never set maxUnavailable higher than your workloads can tolerate. If minAvailable in your PDBs is 2 and you only have 3 replicas, draining 2 workers simultaneously will block.
Phase 4 — During the Upgrade
Watch for Stuck Upgrades
# If upgrade appears frozen, check:# 1. Any operator stuck progressing?oc get co | grep -v "True.*False.*False"# 2. Any node stuck draining?oc get nodes | grep SchedulingDisabled# 3. Any pods blocking drain (PDB)?oc get pdb -A | grep -v "^NAMESPACE"# 4. Pending CSRs (workers trying to re-join after reboot)oc get csr | grep Pendingoc get csr -o name | xargs oc adm certificate approve# 5. Any pods stuck Terminating (blocking node drain)?oc get pods -A | grep Terminating# Force delete if stuck:oc delete pod <pod> -n <ns> --grace-period=0 --force
Approve Pending CSRs
After each node reboots, it issues a CSR to re-register with the cluster. In some configurations these need manual approval:
# Watch for and approve all pending CSRswatch -n 10 "oc get csr | grep Pending"# Approve all at onceoc get csr -o name | xargs oc adm certificate approve
Phase 5 — Post-Upgrade Validation
5.1 Confirm Upgrade Completed
# Check cluster versionoc get clusterversion# AVAILABLE=True PROGRESSING=False VERSION=<new-version># Confirm exact versionoc get clusterversion -o jsonpath='{.items[0].status.desired.version}'
5.2 Validate All Operators
# All must return to healthy stateoc get co# All: AVAILABLE=True PROGRESSING=False DEGRADED=False
5.3 Validate All Nodes
# All nodes Ready, running new kubelet versionoc get nodes# All: STATUS=Ready, VERSION=v1.27.x (new version)
5.4 Validate Workloads
# Check for any crashlooping or failed podsoc get pods -A | grep -vE "Running|Completed|Succeeded"# Check deployments fully availableoc get deployments -A | grep -v "^\(NAMESPACE\)" | \ awk '$3 != $4 {print}' # READY != DESIRED# Check routes are respondingoc get routes -A
5.5 Take a Post-Upgrade etcd Backup
# The pre-upgrade backup is now stale — take a fresh onesudo /usr/local/bin/cluster-backup.sh /home/core/backup-post-upgradescp core@master-0:/home/core/backup-post-upgrade/* bastion:/backups/post-upgrade/
Phase 6 — OLM Operator Upgrades
Platform upgrade does not upgrade OLM-managed operators. Do this separately:
# Check which operators have updates availableoc get csv -A | grep -v Succeeded# Check subscription installPlanApproval settingsoc get subscription -A -o custom-columns=\'NS:.metadata.namespace,NAME:.metadata.name,APPROVAL:.spec.installPlanApproval'# For Manual approval — approve pending InstallPlansoc get installplan -A | grep RequiresApprovaloc patch installplan <name> -n <namespace> \ --type merge \ --patch '{"spec":{"approved":true}}'
Complete Upgrade Checklist
PRE-UPGRADE □ Verify upgrade path on Red Hat graph □ Read release notes for target version □ Check deprecated API usage (apirequestcounts) □ Verify etcd health (all 3 members healthy) □ All cluster operators: Available=True, Degraded=False □ All MachineConfigPools: Updated=True, Degraded=False □ All nodes: Ready □ No PDBs blocking all disruptions (disruptionsAllowed=0 on critical apps) □ No critical alerts firing □ Operator compatibility confirmed (OLM operators) □ Stakeholders notified □ etcd backup taken and stored off-cluster □ Upgrade channel set correctly (stable/fast/eus)DURING UPGRADE □ Monitor CVO progress (watch oc get clusterversion) □ Monitor cluster operator rollout (watch oc get co) □ Monitor node reboots (watch oc get nodes) □ Approve pending CSRs as nodes rejoin □ Watch for stuck Terminating pods □ Watch for PDB-blocked drainsPOST-UPGRADE □ clusterversion: Available=True, Progressing=False □ All cluster operators healthy □ All nodes Ready on new version □ All workload pods Running/Completed □ All routes responding □ Upgrade OLM operators to compatible versions □ Take fresh etcd backup □ Update documentation / CMDB with new version
Key Timings to Expect
| Phase | Typical Duration |
|---|---|
| CVO image pull + validation | 5–15 min |
| Control plane operator updates | 30–60 min |
| Master node rolling reboots | 15–30 min |
| Cluster operator updates | 20–40 min |
| Worker node rolling reboots | 30 min – several hours (depends on count) |
| Total (small cluster ~6 nodes) | ~2–3 hours |
| Total (large cluster 50+ workers) | ~4–8 hours |