Upgrading a bare-metal (KubeOne) cluster
Kubernetes version upgrades for clusters provisioned with the generic
bare-metalprovider. No flags, no hand-runkubeonebinary:general.yamlis the source of truth, and kubeaid-cli drives the embedded KubeOne.
Supported Kubernetes range: v1.33 – v1.35 (KubeOne v1.13). The range moves when kubeaid-cli bumps its embedded KubeOne.
Steps
-
Bump the Kubernetes version in your cluster's
general.yaml:cluster:name: demok8sVersion: v1.35.2One minor version at a time — kubeadm (and therefore KubeOne) cannot skip minors. Patch-only bumps within the same minor are fine too.
-
Run the upgrade:
kubeaid-cli cluster upgradeThe provider is auto-detected from
general.yaml, so there's nobare-metalsubcommand or version flag. What happens:- Pre-flight — the current version is read from the live cluster (lowest kubelet version across nodes; falls back to the rendered KubeOne manifest in kubeaid-config when the cluster isn't reachable). The run refuses downgrades, minor-skips, and NotReady nodes. When the target is beyond v1.34, every host is additionally SSHed into and checked for cgroup v2 (see below).
- Render + push —
kubeone/kubeone-cluster.yamlis re-rendered fromgeneral.yamland pushed to your kubeaid-config repo. By default this goes through the PR workflow (the run waits until you merge); pass--skip-pr-workflowto push directly to the default branch. - Apply — the embedded
kubeone applyperforms the rolling upgrade: control plane first, then the static workers, one node at a time (drain, upgrade, uncordon). - Verify — the run waits until every node is Ready at the target kubelet version.
-
Multi-minor jumps = repeat. Going v1.33 → v1.35 means editing
general.yamlto v1.34.x, running the upgrade, then editing to v1.35.x and running it again.
Reconcile semantics
The manifest is re-rendered from general.yaml in full. Any other
pending change in there (a host you added to a node-group, an SSH port
change, …) rides along into the same kubeone apply. Keep unrelated
general.yaml edits out of an upgrade if you don't want that.
Config-only changes: cluster sync
For changes that don't bump the Kubernetes version — the bundled helm releases (e.g. Cilium), addons, a new host in a node-group — run:
kubeaid-cli cluster sync
Sync first shows what it's about to do and asks for one confirmation —
until you answer yes it touches nothing, not even a PR, so an
accidentally issued sync is harmless. Pass --yes (-y) for
unattended runs; disruptive steps below keep their own, separate
prompts.
- Verifies the cluster already runs
cluster.k8sVersion(a pending version bump is refused — runcluster upgradefirst). - Re-renders and pushes the KubeOne manifest (same PR workflow,
--skip-pr-workflowworks here too). - Runs a plain
kubeone apply: KubeOne's steady-state task set reconciles helm releases and addons, joins newly added static workers, renews soon-to-expire certificates and re-labels nodes. It never cordons or drains in-version nodes, so sync is non-disruptive and safe to rerun anytime.
Kubelet tuning (cloud.bare-metal.kubelet) needs more: KubeOne
rewrites kubelet flags only during its per-node upgrade procedure.
Sync SSHes into every host, compares the flags in
/var/lib/kubelet/kubeadm-flags.env against general.yaml, and when
they differ shows the drift (--max-pods : 300 → 250) and asks for
consent. On approval the apply runs with --force-upgrade — KubeOne
cordons, drains and restarts one node at a time (the single-node PDB
guard below applies here too). On decline — or with no TTY, e.g. CI —
the kubelet changes stay pending and the plain apply still runs.
If a run fails
Rerun kubeaid-cli cluster upgrade. The flow is idempotent:
- kubeaid-config may be ahead of the cluster (manifest pushed, apply
failed) — the rerun detects the unchanged manifest and goes straight
to
kubeone apply, which converges the remaining nodes. - The "current version" pre-flight uses the lowest kubelet version, so a half-upgraded cluster still validates as one hop away from the target.
Single-node clusters
KubeOne cordons and drains each node during the upgrade. On a
single-node cluster an evicted pod has nowhere to reschedule, so any
PodDisruptionBudget selecting running pods deadlocks the drain — even
maxUnavailable: 1 (the first eviction consumes the budget, the
replacement stays Pending on the cordoned node, and every further
eviction is forbidden).
kubeaid-cli cluster upgrade — and cluster sync, when a consented
kubelet reconcile forces the upgrade procedure — detects this: when the cluster has
exactly one node, every pod-selecting PDB is listed and the run asks
for consent before touching anything. On approval, the PDBs are
removed, kept removed while the drain runs (ArgoCD self-heal
recreates its PDBs within seconds otherwise), and restored afterwards
— ArgoCD-managed PDBs come back via ArgoCD's own sync, the rest are
re-created by the run itself. On decline (or when there's no TTY to
ask on), the upgrade aborts and prints the exact kubectl delete pdb
commands to run by hand before retrying.
cgroup v2 (Kubernetes ≥ v1.35)
Kubernetes v1.35 dropped cgroup v1 support — a kubelet beyond v1.34 refuses to start on a cgroup v1 host. The upgrade pre-flight verifies every host before touching anything. To check a host yourself:
stat -fc %T /sys/fs/cgroup # must print: cgroup2fs
Hosts on cgroup v1 need systemd.unified_cgroup_hierarchy=1 on the
kernel command line (and a reboot) before the upgrade.
Caveats
- Clusters still on v1.32 or older: KubeOne v1.13 (embedded since
this kubeaid-cli version) can't manage them. Do one manual hop to
v1.33 with a KubeOne v1.12 binary first, then use
kubeaid-cli cluster upgradefrom there on. - containerd 1.7 → 2.x: KubeOne v1.13 moves nodes to containerd 2.x as part of node upgrades. This is handled per-node during the rolling upgrade; no action needed, but expect it in the diff of installed packages.
- After the upgrade,
kubectl get nodesis the quick sanity check — every node Ready, every kubelet at the target version.