A removed kubelet flag does not warn you. It kills the kubelet at startup, and the node never joins the cluster. That is the entire difference between a deprecated flag and a removed one, and Kubernetes 1.37 just turned a batch of the former into the latter — finishing a cleanup that was explicitly deferred from 1.36.
If you run managed Kubernetes, your provider absorbs this class of breakage before it reaches you. If you run Cluster API on machines you own, nobody does. Every stale flag baked into your KubeadmConfigTemplate, your node image, or a systemd drop-in is a node that will refuse to start the day you roll 1.37. This post gives you the pre-upgrade audit that finds every one of them first.
Removed means the kubelet exits: learn the error signature
When the kubelet meets a flag it no longer recognizes, startup fails immediately with a parse error. Operators who lived through past removals will recognize the shape:
Error: failed to parse kubelet flag: unknown flag: --network-pluginThat exact line stranded nodes during the 1.24 upgrade, when --network-plugin was deleted and clusters still passing it in /var/lib/kubelet/kubeadm-flags.env had to have the flag surgically removed before the kubelet would start. A later wave did the same to anyone still passing --pod-infra-container-image — deprecated since 1.27, removed in 1.34, and fatal on any 1.34+ kubelet started with it present:
failed to parse kubelet flag: unknown flag: --pod-infra-container-imageThe 1.37 cleanup follows the identical pattern: flags that previously logged Flag --foo has been deprecated now produce unknown flag and a dead kubelet instead. Grep your logs for has been deprecated and you are looking at warnings; grep for unknown flag after an upgrade and you are looking at nodes that will never become Ready. The audit below is about converting the second outcome into the first — finding the flags while they still only warn.
What the 1.37 cleanup actually removes
The honest caveat first: the authoritative list of removed flags lives in the 1.37 release notes for your exact target patch version, not in any blog post. Flag removals land across several minor releases and backports shift the boundaries. So step zero of the audit is deriving the list from the release itself rather than trusting a static table — including this article's.
That said, every flag in this cleanup wave belongs to one well-documented class: kubelet CLI flags whose only supported home is now the KubeletConfiguration file passed via --config. Upstream has been migrating this surface for years — "all flags other than --config, --write-config-to, and --cleanup are deprecated, please begin using a config file ASAP" has been the kubelet's startup advice for a long time — and distributors have been doing the migration visibly. Kops' 1.36 release notes record migrating its deprecated kubelet CLI flags into the configuration file, and Typhoon did the same migration to prepare for exactly these removals. Confirmed members of this class from recent waves, any of which may still be hiding in your templates, include:
| Flag | Status | Replacement |
|---|---|---|
--network-plugin | Removed in 1.24 | CNI is the only runtime path; delete the flag |
--pod-infra-container-image | Deprecated since 1.27, removed in 1.34 | podInfraContainerImage in KubeletConfiguration |
--cgroup-driver | Removed from kubeadm-flags.env generation | cgroupDriver in KubeletConfiguration (set in config.yaml, not flags) |
--iptables-masquerade-bit / --iptables-drop-bit | Deprecated in 1.28, since removed | iptablesMasqueradeBit / iptablesDropBit in KubeletConfiguration |
--image-gc-high-threshold / --image-gc-low-threshold / --image-maximum-gc-age | Silently dropped by kubeadm on 1.35+ | imageGCHighThresholdPercent and siblings in KubeletConfiguration |
Legacy klog flags (--log-dir, --logtostderr, and friends) | Removed in 1.26; only -v / -vmodule survive | Structured log configuration |
Derive your exact list with three commands against the version you plan to run: read the "Removals" section of its release notes, run the new kubelet binary's --help output against your current flag set, and check kubeadm upgrade plan output for deprecation warnings. Anything in your flag set that is absent from the new --help is a node that will not start.
Why a Cluster API fleet is uniquely exposed
A managed node pool has a vendor-owned translation layer between your intent and the kubelet command line. A Cluster API fleet has the opposite: your intent is the kubelet command line, stamped onto every node by machinery you own. Stale flags hide in at least four places, and a typical fleet has all four:
KubeadmConfigTemplatekubeletExtraArgs. The most common hiding spot. Extra args declared here are rendered intokubeadm-flags.envon every machine the template produces — control plane and workers alike. One stale entry fans out to the whole fleet./var/lib/kubelet/kubeadm-flags.envon live nodes. The rendered artifact. Even after you fix the template, every existing node still carries the old file until it is reprovisioned or the file is rewritten.- Systemd drop-ins and environment files.
/etc/default/kubelet,/etc/sysconfig/kubelet(RPM-based images), and any customEnvironmentFilelines your image build adds. Past removals explicitly required operators to clean the flag out of these files by hand. - Node-image build scripts. Packer, image-builder, or Dockerfile steps that inject flags or pin old kubeadm behavior. Fix the cluster without fixing the image pipeline and every scale-up event reintroduces the breakage.
Compounding all of it: MachineDeployments roll node by node, so a bad flag set does not fail fast at step one. It fails once per machine, turning one bad template into a fleet-wide rolling outage with each replacement node refusing to join.
The pre-upgrade audit: five steps, copy-paste ready
Run this against a checkout of your fleet repo plus one representative cluster, before cutting any MachineDeployment over to 1.37.
Step 1 — Inventory every flag you pass. Search the fleet repo for all three injection points at once:
grep -rn "kubeletExtraArgs" --include="*.yaml" .
grep -rn "KUBELET_EXTRA_ARGS\|kubeadm-flags.env" --include="*" . | grep -v vendor
grep -rn "pod-infra-container-image\|network-plugin\|cgroup-driver\|iptables-masquerade-bit\|iptables-drop-bit\|image-gc-" --include="*.yaml" .The third grep is the triage shortcut: it catches the historically-removed flags directly. The first two catch everything else you pass, which you then diff against the target kubelet's --help.
Step 2 — Check the live nodes, not just the repo. Templates drift from reality. On one node per MachineDeployment:
cat /var/lib/kubelet/kubeadm-flags.env
cat /var/lib/kubelet/config.yaml | head -30
journalctl -u kubelet --since "7 days ago" | grep -i "deprecated" | sort -uThe journalctl line is the highest-value command in this post: the kubelet tells you, for free, every flag it is still accepting only out of backward compatibility. Every distinct has been deprecated hit is a line item to migrate before the upgrade.
Step 3 — Diff your flag set against the target kubelet. Pull the 1.37 kubelet (or its --help from the release docs) and confirm every flag from Step 1 still exists. Anything missing is a guaranteed unknown flag fatal on upgrade day. Pay special attention to flags your image pipeline injects outside the CAPI templates — those are the ones Step 1's YAML grep misses and Step 2's live-node check catches.
Step 4 — Migrate each hit into KubeletConfiguration. The fix pattern is always the same: delete the CLI flag, set the corresponding field in the kubelet config file that kubeadm writes to /var/lib/kubelet/config.yaml via --config. Before-and-after for the classic case:
# Before: KubeadmConfigTemplate snippet that will break a 1.37 kubelet
kubeletExtraArgs:
pod-infra-container-image: "registry.k8s.io/pause:3.9"# After: the same intent expressed where the kubelet still reads it
kubeletExtraArgs: {}
# ... with podInfraContainerImage set in the KubeletConfiguration
# delivered via the JoinConfiguration / ClusterConfiguration nodeRegistration
# or a kubelet.config.k8s.io/v1beta1 KubeletConfiguration file referenced by --configVerify the rendered result with kubelet --write-config-to or the /configz endpoint on a test node, and confirm the deprecated-warning grep from Step 2 comes back empty — silence there is the definition of done.
Step 5 — Roll one MachineDeployment first. Cut a non-production pool to 1.37 and watch replacement nodes join before touching anything else. The specific signals: kubectl get nodes shows the new nodes Ready, journalctl -u kubelet on a fresh node contains no unknown flag, and MachineDeployment status shows no machines stuck provisioning. Only then roll the rest, control plane first per the standard kubeadm ordering.
The maintenance cost nobody else pays for you
Zoom out and this is a recurring line item, not a one-off. Every Kubernetes release retires some surface your fleet depends on — kubelet flags this cycle, beta APIs the next, a kube-proxy backend the cycle after. Managed-Kubernetes customers experience each of these as a support email and a changed default. Self-hosted fleets experience them as audit checklists exactly like this one, run against templates and images no vendor ever sees.
The good news is that the checklist is the same shape every time: inventory what you pass, diff it against the target release, migrate to the supported surface, verify silence, roll incrementally. Teams that run it during the freeze window — 1.37's cycle gave a five-week heads-up between code freeze and GA — upgrade boringly. Teams that skip it debug unknown flag on upgrade day, one unjoinable node at a time.
Run the Step 2 journalctl grep on one node per pool this week. If it comes back empty, you just bought certainty for free. If it doesn't, you now hold the complete list of what to fix, in what order, before 1.37 makes the decision for you.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



