Kubernetes v1.37 ("Garhwal") shipped on 26 August 2026 and swapped the kubelet's embedded cAdvisor for the leaner github.com/google/cadvisor/lib module in PR #139870. Eighteen cAdvisor flags went with it, --housekeeping-interval survived, and a kubelet handed any of the other eighteen exits at argument parsing with failed to parse kubelet flag: unknown flag: --containerd. Three metric families disappeared in the same change, and a separate fix inverted what eventRecordQPS: 0 means.
Most upgrade advice still names the older removals, --network-plugin (gone in 1.24) and --pod-infra-container-image (gone in 1.34), which is a different flag set. The cAdvisor list exists in the 1.37 changelog and the PR body, and almost nowhere else yet. These eleven checks are for whoever owns node images, kubeadm-flags.env, a Cluster API KubeadmConfigTemplate, or a systemd drop-in. If a node in your fleet already refuses to start and the journal blames cgroup v1 on a v2 host, settle that first, because the flag failure below leaves a completely different line in the log. This is also only the boot half of the upgrade: the admission half, static pods with secretRef no longer being admitted, hits after the node is already up.
The checks
- Grep for the actual eighteen, not the flags the upgrade posts list. PR #139870 removes
--application-metrics-count-limit,--boot-id-file,--container-hints,--containerd,--containerd-namespace,--enable-load-reader,--event-storage-age-limit,--event-storage-event-limit,--global-housekeeping-interval,--log-cadvisor-usage,--machine-id-file, and the seven--storage-driver-*flags. Run this against a live node before you run it against the repo, since the repo does not know what the image bakes in:
grep -rnE -- '--(application-metrics-count-limit|boot-id-file|container-hints|containerd|containerd-namespace|enable-load-reader|event-storage-(age|event)-limit|global-housekeeping-interval|log-cadvisor-usage|machine-id-file|storage-driver-[a-z-]+)' \
/var/lib/kubelet/kubeadm-flags.env /etc/systemd/system/kubelet.service.d/ /etc/default/kubelet
- Read argv from
/proc, because the unit file is one of three inputs. systemd composes the packaged unit, the drop-in andkubeadm-flags.envinto one command line, so readingkubelet.servicealone under-reports what the process actually receives. The running process answers without ambiguity:
tr '\0' '\n' < /proc/"$(pgrep -x kubelet)"/cmdline
- Diff that argv against the 1.37 binary's own
--helpand let the shell name the fatal flags. No memory of which release dropped what is involved, and the same three commands keep working in 1.38:
kubelet --help 2>&1 | grep -oE '^ +--[a-z0-9-]+' | tr -d ' ' | sort -u > /tmp/flags-137
tr '\0' '\n' < /proc/"$(pgrep -x kubelet)"/cmdline | grep -oE '^--[a-z0-9-]+' | sort -u > /tmp/flags-node
comm -23 /tmp/flags-node /tmp/flags-137
Empty output means this node boots. Anything printed is a guaranteed unknown flag on upgrade day.
- Keep
--housekeeping-interval. It is the single cAdvisor flag the PR retains, and a bulk "delete every cAdvisor flag" sweep takes it along with the dead ones. Dropping it does not fail the node, it returns container stats collection to the default cadence, which moves CPU and memory sampling under everything that scrapes/metrics/cadvisor. - Do not "fix"
--containerdby pointing it at the CRI endpoint. cAdvisor's--containerdand--containerd-namespaceaimed cAdvisor's own stats client at a socket; the kubelet's runtime has always been--container-runtime-endpoint/containerRuntimeEndpoint. Moving the old value across converts a node that fails loudly at boot into one that fails later at pod sandbox creation, which costs you an afternoon instead of a minute. Delete the flag and change nothing else. - Set
eventRecordQPSexplicitly, whatever it currently is. PR #117119 landed in 1.37 and made0mean unlimited, matching the field's documentation for the first time; before 1.37 the zero value fell back to the default. The changelog's instruction is blunt: if you set it to 0 and want the old behaviour, set it to 50. Anyone who wrote0believing it disabled event recording now gets an unthrottled event writer on every node, pointed at the API server.
grep -rn 'eventRecordQPS' /var/lib/kubelet/config.yaml
- Treat the three deleted metric families as lost alert coverage.
container_cpu_load_average_10s,container_cpu_load_d_average_10sandcontainer_tasks_stateno longer come out of/metrics/cadvisor. A PromQL rule over a series that stopped existing does not error, it returns no data and never fires again, which is the failure mode you discover months later. Find the references first, then give each surviving rule anabsent()companion:
grep -rn 'container_cpu_load_average_10s\|container_cpu_load_d_average_10s\|container_tasks_state' \
./prometheus-rules/ ./grafana-dashboards/
- Audit anything that reads
userDefinedMetricsfrom/stats/summary. The same PR removed cAdvisor's application and custom metrics surface, so thecontainer_application_*families and theuserDefinedMetricsfield are gone. Custom autoscalers and chargeback exporters that walk/stats/summarywill parse a response missing a key they assume is always present, and that usually arrives as a nil dereference rather than a message naming the field. - Migrate kubeadm config with a 1.36 binary, before 1.37 is anywhere near the machine. PR #136016 removed the
v1beta3API, deprecated since v1.31, along with thePublicKeysECDSAfeature gate that existed only for v1beta3 compatibility. The 1.37 binary cannot read the file it would need to convert, so the conversion has to run on the older one:
kubeadm version # must still be 1.36.x
kubeadm config migrate --old-config kubeadm.yaml --new-config kubeadm-v1beta4.yaml
Stored cluster config counts as well as the file in git. External-etcd clusters carrying a kubeadm.k8s.io/v1beta3 document are a documented 1.37 breakage.
- Rename the scheduling feature gates in the same pass. PR #139520 removed
GangSchedulingandWorkloadAwarePreemptionand folded both intoGenericWorkload. An unrecognized gate is fatal to the component reading it, so a control-plane static manifest still carrying the old name produces an API server or scheduler that will not start, which beats losing one node by a wide margin:
grep -rn 'GangScheduling\|WorkloadAwarePreemption\|AnyVolumeDataSource' /etc/kubernetes/manifests/
AnyVolumeDataSource belongs in that grep for the same reason: the 1.37 changelog lists its removal under PR #135336, after it had been locked on since v1.33.
- Canary on a pool you can afford to lose, and alarm on instance churn instead of node conditions. A kubelet that exits at flag parsing never creates a Node object, so
NotReadynever appears, no node-condition alert fires, and the autoscaler keeps replacing the instance in a loop that looks healthy from the control plane. Machine count is the signal that moves, and the journal on one held instance is the confirmation:
journalctl -u kubelet -b --no-pager | grep -m1 'failed to parse kubelet flag'
On Cluster API, roll a new KubeadmConfigTemplate rather than editing the existing one, and hold the first machine with a cluster.x-k8s.io/paused annotation so there is something left to read.
Wrap-up
Delete these tripwires when every pool reports 1.37 and comm -23 returns empty, which is a condition rather than a date. The habit worth keeping past that point is check 3. I would not trust any published list of removed flags, this one included, over a binary sitting on disk that answers the question directly: kubelet --help on the version you are about to ship costs one command and stays correct through 1.38.
Two other 1.37 defaults bite without any config change on your side. SELinuxMount going GA stalls shared PVCs, and the release announcement for Kubernetes v1.37 is worth a read for the enabled-by-default list before you schedule the window.
Comments
Be the first to comment.