Rancher-managed RKE2 upgrades are Kubernetes workflows that intentionally mutate node-local filesystems.

That sounds odd until you inspect an upgrade pod. The pod is scheduled onto the node being upgraded, mounts host paths, compares the new RKE2 binary inside the upgrade image with the host binary, replaces the host binary, and restarts the node service.

That is the useful mental model:

Rancher desired version
  -> system-upgrade-controller Plan
  -> upgrade Job/Pod on each selected node
  -> host filesystem mounted under /host
  -> RKE2 binary/config/service update on that node
  -> node restarts components and rejoins

The pod is not just a health check. It is the delivery mechanism for node-local changes.

Pre-Upgrade Evidence First

Before letting a Rancher-managed plan move a cluster, capture state from both the target cluster and Rancher management cluster.

Target cluster captures:

BACKUP_DIR="$HOME/rancher-upgrade-backups/pre-v134-cluster-a-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"

kubectl get nodes -o wide > "$BACKUP_DIR/kubectl-get-nodes-wide.txt"
kubectl get pods -A -o wide > "$BACKUP_DIR/kubectl-get-pods-all-wide.txt"
kubectl get events -A --sort-by=.lastTimestamp > "$BACKUP_DIR/kubectl-get-events-all.txt"
kubectl get storageclasses -o yaml > "$BACKUP_DIR/storageclasses.yaml"
kubectl get pv -o yaml > "$BACKUP_DIR/persistentvolumes.yaml"
kubectl get pvc -A -o yaml > "$BACKUP_DIR/persistentvolumeclaims.yaml"
kubectl get ingress -A -o yaml > "$BACKUP_DIR/ingresses.yaml"
kubectl get services -A -o wide > "$BACKUP_DIR/services-wide.txt"
kubectl get apiservices -o yaml > "$BACKUP_DIR/apiservices.yaml"
kubectl get crds -o wide > "$BACKUP_DIR/crds-wide.txt"
kubectl get namespaces -o yaml > "$BACKUP_DIR/namespaces.yaml"
kubectl get plans -A -o yaml > "$BACKUP_DIR/system-upgrade-plans.yaml"
kubectl get pods,jobs -A | grep -i upgrade > "$BACKUP_DIR/system-upgrade-pods-jobs.txt" || true

Rancher management captures:

kubectl --context rancher-mgmt get clusters.management.cattle.io -o yaml \
  > "$BACKUP_DIR/rancher-management-clusters.yaml"

kubectl --context rancher-mgmt get clusters.provisioning.cattle.io -A -o yaml \
  > "$BACKUP_DIR/rancher-provisioning-clusters.yaml"

kubectl --context rancher-mgmt get clusters.fleet.cattle.io -A -o yaml \
  > "$BACKUP_DIR/fleet-clusters.yaml"

kubectl --context rancher-mgmt get bundles.fleet.cattle.io -A -o yaml \
  > "$BACKUP_DIR/fleet-bundles.yaml"

kubectl --context rancher-mgmt get bundledeployments.fleet.cattle.io -A -o yaml \
  > "$BACKUP_DIR/fleet-bundledeployments.yaml"

Also take an etcd snapshot and control-plane file archives according to the platform runbook. Verify the artifacts, not just the commands:

tar tzf control-plane-backup.tgz >/dev/null
ls -lh etcd-snapshot-name

Non-empty .err files are not automatically failures. For example, Kubernetes v1.33+ can warn that core Endpoints is deprecated. Classify warnings separately from failed captures.

Know Which Plans Are Active

Rancher can create managed Plans in cattle-system while older GitOps-managed Plans still exist in system-upgrade.

Check all Plans:

kubectl get plans -A -o wide

You may see both:

cattle-system   rke2-master-plan   rancher/rke2-upgrade   v1.34.9+rke2r1
cattle-system   rke2-worker-plan   rancher/rke2-upgrade   v1.34.9+rke2r1
system-upgrade  server-plan        rancher/rke2-upgrade   v1.32.8+rke2r1
system-upgrade  agent-plan         rancher/rke2-upgrade   v1.32.8+rke2r1

Do not assume every Plan is active. Inspect labels and ownership:

kubectl -n cattle-system get plan rke2-master-plan -o yaml
kubectl -n cattle-system get plan rke2-worker-plan -o yaml

Rancher-managed plans commonly show signals like:

metadata.labels.rancher-managed: "true"
metadata.finalizers: systemcharts.cattle.io/rancher-managed-plan
spec.concurrency: 1
spec.cordon: true

The worker Plan should wait for the master Plan through prepare, rather than upgrading workers before control-plane completion.

The Upgrade Pod Writes Through /host

Inspecting a Rancher-managed upgrade pod makes the mechanism visible:

kubectl -n cattle-system get jobs,pods -o wide | grep -i rke2
kubectl -n cattle-system describe pod <upgrade-pod>
kubectl -n cattle-system logs <upgrade-pod> --previous=false

Log shape:

[INFO] rke2 binary is running with pid 486375
RKE2_BIN_PATH=/usr/local/bin/rke2
FULL_BIN_PATH=/host/usr/local/bin/rke2
Comparing old and new binaries
sha256sum /opt/rke2 /host/usr/local/bin/rke2

That tells you the upgrade container sees:

/opt/rke2                  new binary from the upgrade image
/host/usr/local/bin/rke2   host binary on the node
/host/proc/<pid>/cmdline   host process metadata

The pod is using Kubernetes scheduling to run node-local maintenance. This is why permissions, host mounts, and node selection matter.

Labels Control The Blast Radius

Rancher-managed Plans select nodes with labels such as:

upgrade.cattle.io/kubernetes-upgrade=true

Before the run, verify the selected nodes:

kubectl get nodes -l upgrade.cattle.io/kubernetes-upgrade=true \
  -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion

If the wrong nodes are selected, fix the labels before the controller starts new jobs:

kubectl label node worker-6 worker-9 upgrade.cattle.io/kubernetes-upgrade- --overwrite
kubectl label node cp-1 cp-2 cp-3 upgrade.cattle.io/kubernetes-upgrade=true --overwrite

This is especially important when moving from one hop to the next. Stale labels can make a worker Plan start before the intended control-plane sequence.

Monitor The Rollout By Plans And Nodes

Poll instead of relying only on watches during control-plane restarts:

while true; do
  date
  kubectl get nodes -o json \
    | jq -r '.items[] | [.metadata.name, (if (.spec.unschedulable // false) then "cordoned" else "schedulable" end), .status.nodeInfo.kubeletVersion, ([.status.conditions[] | select(.type=="Ready")][0].status)] | @tsv'
  echo
  kubectl -n cattle-system get plans -o json \
    | jq -r '.items[] | [.metadata.name, ([.status.conditions[]? | select(.type=="Complete")][0].status // ""), (.status.applying // [] | join(",")), .status.latestVersion] | @tsv'
  echo
  kubectl -n cattle-system get jobs,pods -o wide | grep -i rke2 || true
  sleep 30
done

Completion shape:

rke2-master-plan  True  v1.34.9-rke2r1
rke2-worker-plan  True  v1.34.9-rke2r1

Every node should be Ready, schedulable, and on the target version.

Post-Upgrade Cleanup Signals

Upgrade pods often become ContainerStatusUnknown or Failed because the node restarted underneath them. If the owning Jobs are complete and Plans are complete, these pods are usually stale artifacts.

Clean them after verification:

kubectl -n cattle-system delete pod --field-selector=status.phase=Failed

Then check the platform add-ons:

kubectl get ds -A -o json \
  | jq -r '.items[] | select(.status.desiredNumberScheduled != .status.numberReady) | [.metadata.namespace, .metadata.name, .status.desiredNumberScheduled, .status.numberReady] | @tsv'

kubectl get deploy -A -o json \
  | jq -r '.items[] | select((.status.readyReplicas // 0) < (.spec.replicas // 1)) | [.metadata.namespace, .metadata.name, (.status.readyReplicas // 0), (.spec.replicas // 1)] | @tsv'

One post-upgrade symptom worth recognizing is a host-port stale bind:

port 80 is already in use. Please check the flag --http-port

For an ingress DaemonSet pod after a node restart, deleting the one bad pod can force a fresh sandbox and clear the stale bind:

kubectl -n kube-system delete pod rke2-ingress-nginx-controller-abcde

Do not delete the whole DaemonSet first. Confirm the failure is isolated to one pod/node.

Acceptance Criteria

The upgrade is complete when these are true:

  • Rancher desired version and reported actual version match.
  • active Rancher-managed Plans are Complete=True.
  • all nodes are Ready, schedulable, and on the target RKE2 version.
  • stale failed upgrade pods are cleaned after Jobs complete.
  • DaemonSets and deployments are at expected ready counts.
  • event scans show no current port in use, sandbox, PDB, or image-pull blockers.
  • pre-existing unrelated workload failures are documented separately.

The operating rule: a Rancher RKE2 upgrade pod is a privileged node maintenance action packaged as a Kubernetes workload. Treat it with the same care you would give an SSH-based node patch, but use Kubernetes evidence to prove what it changed and when it is done.