Back to Blog

Kubelet Configuration Internals for the CKA Exam: KubeletConfiguration vs Flags, the --config-dir Drop-in Directory, kubeadm's kubelet-config ConfigMap & kubeadm-flags.env, Cgroup Drivers & cgroup v2, Swap, Image Pull & GC Settings, Log Rotation, CrashLoopBackOff Tuning and the /configz Endpoint

A practitioner's CKA guide to how the kubelet is actually configured on a kubeadm node — the systemd unit chain and the four environment variables behind ExecStart, the exact precedence between the config file, drop-in files and flags, how the --config-dir drop-in directory merges scalars, lists and maps, the kubelet-config ConfigMap and the kubeadm upgrade node phase kubelet-config loop, instance-specific settings through kubeadm-flags.env, patches and kubeletExtraArgs, why cgroup drivers must match the runtime and how KubeletCgroupDriverFromCRI changed that, cgroup v2 and failCgroupV1, failSwapOn and LimitedSwap, the defaults that matter (evictionHard, image GC, serializeImagePulls, containerLogMaxSize, maxPods, podPidsLimit, crashLoopBackOff), reading the live configuration from /configz, and a symptom-to-fix table for a kubelet that will not start.

By Sailor Team , October 29, 2026

Every other control-plane component on a kubeadm cluster is configured the same way: edit a static Pod manifest under /etc/kubernetes/manifests, wait for the kubelet to restart it. The kubelet is the exception. It is a systemd service, it reads a configuration file rather than a manifest, that file is generated from a ConfigMap you may never have looked at, and a handful of flags are still layered on top from two different environment files. When a CKA task says “the kubelet on node01 is not starting” or “configure the node to allow 200 Pods”, you are being tested on whether you understand that chain.

Our kubeadm installation guide covers init and join, the troubleshooting guide covers the general method for a broken node, and the Node Allocatable guide covers the reservation fields. This article is about the configuration mechanism itself: where every kubelet setting comes from, in what order they override each other, how to change one safely on one node or on all of them, which defaults the exam expects you to know, and how to read what the kubelet is actually running with. Everything below is based on the Kubernetes v1.37 documentation.

The Startup Chain on a kubeadm Node

Start with what systemd runs. The kubeadm package installs a drop-in unit at /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf that augments the basic kubelet.service. The kubeadm CLI never touches that file; the package owns it. Its content on a current cluster is:

[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
# This is a file that "kubeadm init" and "kubeadm join" generate at runtime, populating
# the KUBELET_KUBEADM_ARGS variable dynamically
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
# This is a file that the user can use for overrides of the kubelet args as a last resort.
# KUBELET_EXTRA_ARGS should be sourced from this file.
EnvironmentFile=-/etc/default/kubelet
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS

Four variables, four sources, and the order on the ExecStart line is the order of precedence for flags: KUBELET_EXTRA_ARGS is last and therefore wins any conflict. Map each variable to its file and you have the whole picture:

FileOwnerWhat it holds
/etc/kubernetes/bootstrap-kubelet.confkubeadm, temporaryCA certificate plus bootstrap token used for TLS bootstrap; used only while kubelet.conf does not exist and deleted by kubeadm afterwards
/etc/kubernetes/kubelet.confkubeadm, then the kubeletthe kubelet’s own kubeconfig with its unique client identity, obtained by TLS bootstrap (see the certificate management guide for rotation)
/var/lib/kubelet/config.yamlkubeadmthe KubeletConfiguration object, written at init/join and overwritten on every kubeadm upgrade from the cluster-wide ConfigMap
/var/lib/kubelet/kubeadm-flags.envkubeadmone line, KUBELET_KUBEADM_ARGS="...", holding the dynamic, node-specific flags kubeadm decided on at init/join time (the container runtime endpoint, the pause image, --hostname-override or --node-ip if you asked for them)
/var/lib/kubelet/instance-config.yamlkubeadma local record of the CRI socket kubeadm detected on this node
/etc/default/kubelet (DEB) or /etc/sysconfig/kubelet (RPM)youKUBELET_EXTRA_ARGS="...", the documented last resort for flag overrides; highest priority in the flag chain

The documentation adds one more layer you should know exists even if you never use it: if you want to override the unit itself, create /etc/systemd/system/kubelet.service.d/ (not the /usr/lib path) and put a file such as local-overrides.conf there. That is standard systemd behaviour, and it is what you would do to add a --config-dir flag permanently, as the drop-in section below shows.

The two commands that show you this chain live, and which you should run before touching anything on a broken node:

systemctl cat kubelet          # the unit plus every drop-in, concatenated
ps -ef | grep '[k]ubelet'      # the exact flags the running (or last attempted) process got

Precedence: Config File, Drop-ins and Flags

The kubelet documents an exact merge order at startup, from lowest to highest precedence:

  1. Feature gates specified on the command line (lowest).
  2. The kubelet configuration file (--config).
  3. Drop-in configuration files from --config-dir, in sort order.
  4. Command-line arguments other than feature gates (highest).

Two things in that list surprise people. First, a flag beats the file: the documentation says plainly that “command line flags which target the same value as a config file will override that value”, which exists for backwards compatibility and is why a stray flag in kubeadm-flags.env can silently defeat an edit you made in config.yaml. Second, --feature-gates on the command line is at the bottom, not the top, so a featureGates: map in the config file or a drop-in wins over the flag.

Almost every kubelet flag that has a config-file equivalent is deprecated as a flag, and the config file is the recommended mechanism because “it simplifies node deployment and configuration management”. The file is a JSON or YAML serialisation of KubeletConfiguration and must carry type metadata:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
address: "192.168.0.8"
port: 20250
serializeImagePulls: false
evictionHard:
  memory.available: "100Mi"
  nodefs.available: "10%"
  nodefs.inodesFree: "5%"
  imagefs.available: "15%"
  imagefs.inodesFree: "5%"

Relative paths inside the file resolve relative to the file’s own location, while relative paths given as flags resolve relative to the kubelet’s working directory, which is one more reason to keep everything absolute.

Flag defaults and file defaults are not the same

The documentation warns that “some default values differ between command-line flags and the kubelet config file”. The differences are exactly the security-relevant ones, and they are a reliable exam distractor because an old blog post will quote the flag default while your cluster runs the file default:

SettingFlag defaultKubeletConfiguration default
anonymous requests to the kubelet API--anonymous-auth=trueauthentication.anonymous.enabled: false
kubelet API authorization--authorization-mode=AlwaysAllowauthorization.mode: Webhook
unauthenticated read-only port--read-only-port=10255readOnlyPort: 0 (disabled)

On any cluster started with --config, the file defaults apply unless a flag overrides them. The hardening story around those three fields belongs to the CKS cluster setup guide; for the CKA you need to know which default you are getting and why ps -ef matters.

Where did --pod-manifest-path go?

Into the file, as staticPodPath. The static Pods guide covers the mechanics; the point here is that whenever a task refers to a classic kubelet flag, your first move is to find the corresponding field in /var/lib/kubelet/config.yaml, and your second is to check that no flag in kubeadm-flags.env or /etc/default/kubelet is overriding it.

Reading the Effective Configuration: /configz

Because configuration can be spread across a file, several drop-ins and flags, the kubelet exposes the final, actuated configuration on its /configz endpoint. You reach it through the API server’s node proxy, which needs nodes/proxy permission, so it works with the admin kubeconfig you have on the exam:

kubectl get --raw "/api/v1/nodes/node01/proxy/configz" | python3 -m json.tool | less

The response is wrapped in a top-level kubeletconfig object and shows every field with its effective value, including the defaults you never set. It is the fastest way to answer “is this node really running with maxPods: 200?” after an edit, and the only way to see what a drop-in merge produced without reasoning through the rules by hand. The documentation’s own example uses kubectl proxy and curl http://127.0.0.1:8001/api/v1/nodes/<node-name>/proxy/configz | jq ., which is the same request.

Pair it with journalctl -u kubelet on the node. The kubelet validates only the final merged structure, so a bad value in a drop-in shows up as a startup error against the merged config, not against the file you edited.

The —config-dir Drop-in Directory

The drop-in directory solves a real operational problem: kubeadm upgrade overwrites /var/lib/kubelet/config.yaml, so any node-local edit you make there is lost. A drop-in directory is a place kubeadm never writes to.

The mechanics, exactly as documented:

  • The kubelet does not look in any directory by default; you opt in with a flag such as --config-dir=/etc/kubernetes/kubelet.conf.d.
  • Files must end in .conf, for example 99-kubelet-address.conf. Anything else in the directory is ignored.
  • Files are processed by sorting the entire file name alphanumerically: 00-kubelet.conf first, then overridden by 01-kubelet.conf.
  • Each file may be a partial configuration, but must be valid and must include apiVersion and kind.
  • Validation runs once, on the final merged structure.
  • The only patch strategy is replace: every field present in a higher-priority file replaces the same field from a lower one. This is deliberately different from the kubeadm --patches mechanism described later.
  • For Kubernetes v1.28 and v1.29 the flag only worked if the kubelet process also had the environment variable KUBELET_CONFIG_DROPIN_DIR_ALPHA set to any value. The feature is stable in current releases and the variable is no longer needed.

Because kubeadm does not pass --config-dir, you add it yourself, and the documented place for a persistent flag is KUBELET_EXTRA_ARGS:

# /etc/default/kubelet  (Debian/Ubuntu)   or   /etc/sysconfig/kubelet  (RHEL family)
KUBELET_EXTRA_ARGS="--config-dir=/etc/kubernetes/kubelet.conf.d"

Then systemctl daemon-reload && systemctl restart kubelet.

How the merge treats each data type

The merge rules are the part worth memorising, because they decide whether your drop-in did what you think:

Data typeBehaviourExample
scalar fields (address, port, maxPods)later file replaces the valueaddress: 192.168.0.8 in the drop-in wins
nested structs (authorization.webhook.*)merged field by field; untouched sub-fields survivesetting authorization.mode alone keeps cacheAuthorizedTTL from the main file
lists (clusterDNS, tlsCipherSuites, allowedUnsafeSysctls)the entire list is replaced, never appendeda drop-in clusterDNS: ["10.96.0.2"] discards the main file’s entries
maps (featureGates, evictionHard)merged key by key; keys absent from the drop-in survivefeatureGates: {MemoryQoS: false} keeps AllAlpha: false from the main file
map[string][]string (staticPodURLHeader)key by key, but each key’s list is replaced wholeonly the key you mention is rewritten

The documentation’s worked example makes the struct rule concrete. Main file:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
port: 20250
authorization:
  mode: Webhook
  webhook:
    cacheAuthorizedTTL: "5m"
    cacheUnauthorizedTTL: "30s"
serializeImagePulls: false
address: "192.168.0.1"

Drop-in:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authorization:
  mode: AlwaysAllow
  webhook:
    cacheAuthorizedTTL: "8m"
    cacheUnauthorizedTTL: "45s"
address: "192.168.0.8"

Result: port: 20250 and serializeImagePulls: false survive because the drop-in never mentioned them, while authorization and address take the drop-in values. For lists the rule is blunter: a main file with clusterDNS: ["192.168.0.9", "192.168.0.8"] and a drop-in with clusterDNS: ["192.168.0.2", "192.168.0.3", "192.168.0.5"] ends up with exactly the three drop-in entries.

One trap sits at the intersection of the map rule and eviction defaults. evictionHard is a map, so a drop-in that sets only memory.available keeps the other thresholds from the main file. But if the main file itself set only one threshold, the others are already zero, because of the eviction default behaviour described later. The drop-in merge cannot restore defaults that the main file discarded.

How kubeadm Manages the Kubelet

kubeadm treats the kubelet configuration as cluster state with a per-node copy.

The kubelet-config ConfigMap

kubeadm init marshals the KubeletConfiguration (the defaults, plus anything you passed under a KubeletConfiguration document in your --config file) to /var/lib/kubelet/config.yaml and uploads the same content to a ConfigMap named kubelet-config in kube-system, under the data.kubelet key. kubeadm join performs TLS bootstrap with the bootstrap token, uses the resulting credential to download that ConfigMap, and writes it to the same local path. That is why every node in a kubeadm cluster starts from an identical kubelet configuration, and why the ConfigMap is the right place to change something cluster-wide.

To see the full default structure before you decide what to change:

kubeadm config print init-defaults --component-configs KubeletConfiguration

Changing a setting on every node

The documented reconfiguration loop has three steps, and the exam-relevant detail is that the second one is per node:

# 1. Edit the cluster-wide source of truth (once)
kubectl edit cm -n kube-system kubelet-config

# 2. On each node, one at a time, pull the ConfigMap into /var/lib/kubelet/config.yaml
sudo kubeadm upgrade node phase kubelet-config

# 3. Restart the kubelet on that node
sudo systemctl restart kubelet

Editing the ConfigMap alone changes nothing on any running kubelet; the file on disk is what the process reads. Conversely, editing /var/lib/kubelet/config.yaml directly works immediately on that node but is reverted the next time anyone runs kubeadm upgrade node phase kubelet-config or a full kubeadm upgrade, because kubeadm “downloads the KubeletConfiguration from the kubelet-config ConfigMap and overwrite the contents of /var/lib/kubelet/config.yaml”. If a task asks for a durable, cluster-wide change, touch the ConfigMap and then roll it out; if it asks for a quick fix on one node and nothing more, a local edit plus restart is acceptable, but say so in your notes.

The same page documents the sibling procedure for the control plane: ClusterConfiguration lives in the kubeadm-config ConfigMap, and changes to apiServer, controllerManager, scheduler or etcd are regenerated into the static Pod manifests with kubeadm init phase control-plane <component> --config <file> (or kubeadm init phase etcd local), one node at a time. Our cluster upgrade guide shows where these phases run during an upgrade.

Instance-specific configuration

Some settings legitimately differ per node, and kubeadm gives you three ways to express that, in descending order of preference:

MechanismWhereSurvives kubeadm upgrade?Notes
kubeadm patches with the kubeletconfiguration targeta patches directory given in InitConfiguration, JoinConfiguration or UpgradeConfiguration (patches.directory), or --patchesonly if you pass the same directory again during the upgradefile naming target[suffix][+patchtype].extension, e.g. kubeletconfiguration0+merge.yaml; patch types strategic (default), merge, json; applied as the last step before the config is written to disk
nodeRegistration.kubeletExtraArgsInitConfiguration / JoinConfigurationwritten into kubeadm-flags.env, which upgrade preservesflags, not config fields; in the v1beta4 API it is a list of name/value pairs; check the flag is not deprecated first
KUBELET_EXTRA_ARGS/etc/default/kubelet or /etc/sysconfig/kubeletyes, kubeadm never writes itthe documented “last resort”; highest flag priority

The documentation lists the classic candidates for instance-specific treatment: --resolv-conf (systemd-resolved hosts need /run/systemd/resolve/resolv.conf), --hostname-override, --container-runtime-endpoint, and historically --cgroup-driver. A node-specific --node-ip on a multi-homed machine is the one you are most likely to meet on the exam, because without it the kubelet picks the node’s default IPv4 address and API server to kubelet traffic (kubectl logs, kubectl exec) can end up on the wrong interface.

Cgroup Drivers and cgroup v2

The kubelet and the container runtime both write to the cgroup tree, and the documentation is categorical: “It’s critical that the kubelet and the container runtime use the same cgroup driver and are configured the same.”

cgroupfs vs systemd

DriverWho manages cgroupsWhen to use
cgroupfsthe kubelet and runtime write directly to the cgroup filesystemthe kubelet’s file default, but “not recommended when systemd is the init system”, because that gives the node two cgroup managers and makes it “unstable under resource pressure”
systemdsystemd is the single cgroup manager; the kubelet and runtime ask it for cgroupsevery systemd-based distribution, and required for cgroup v2

Three facts keep this straight on the exam:

  1. The raw KubeletConfiguration default for cgroupDriver is cgroupfs, but kubeadm defaults it to systemd when you do not set the field, so a kubeadm-generated config.yaml contains cgroupDriver: systemd.

  2. containerd does not default to systemd; you set it in /etc/containerd/config.toml, and the TOML path depends on the containerd major version:

    # containerd 1.x
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
      SystemdCgroup = true
    
    # containerd 2.x
    [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
      SystemdCgroup = true

    CRI-O “uses the systemd cgroup driver per default”.

  3. Since Kubernetes 1.34 the KubeletCgroupDriverFromCRI feature is GA: when the runtime supports the RuntimeConfig CRI call, the kubelet “automatically detects the appropriate cgroup driver from the runtime, and ignores the cgroupDriver setting within the kubelet configuration”. Older runtimes, “specifically, containerd 1.y and below”, do not implement RuntimeConfig, so the kubelet falls back to its own setting; the documentation states that in Kubernetes 1.38 that fallback is dropped and old containerd versions will fail with newer kubelets.

So on a modern pairing (kubelet 1.34+ with containerd 2.x or CRI-O) the mismatch class of bug disappears, because the runtime is the single source of truth. On the mixed clusters the CKA environment may give you, the mismatch is still possible, and its signature is a kubelet that starts, registers the node, and then fails to create Pod sandboxes with cgroup errors in journalctl, or Pods stuck in ContainerCreating on one node only.

Changing the driver on a node that has already joined is “a sensitive operation”: Pods created under the old driver may fail to have their sandboxes recreated, and restarting the kubelet does not fix that. The documented recommendation is to replace the node, or reinstall it with automation, rather than flip the driver in place.

cgroup v2 and failCgroupV1

cgroup v2 has been stable since Kubernetes 1.25 and needs a Linux kernel 5.8 or newer, a runtime that supports it (containerd 1.4+, CRI-O 1.20+), and the systemd driver on both sides. Check which version a node runs with:

stat -fc %T /sys/fs/cgroup/    # cgroup2fs = v2, tmpfs = v1

cgroup v1 went into maintenance mode in Kubernetes 1.31 and is deprecated since 1.35. The current documentation states that the kubelet “will no longer start on a cgroup v1 node by default”, and the KubeletConfiguration reference lists failCgroupV1 with a default of true. If you are ever handed an old node image that still boots cgroup v1, the only way to run a current kubelet on it is failCgroupV1: false in the kubelet configuration, and the correct long-term answer is a distribution that boots cgroup v2 (Ubuntu 22.04+, Debian 11+, RHEL 9+ and their relatives all do).

The kubelet detects cgroup v2 automatically and needs no configuration for it. What does need attention is anything that reads the cgroup filesystem inside containers; the documentation calls out older Java runtimes and Node.js 18, which misread v2 memory limits.

Swap: failSwapOn and LimitedSwap

For most of Kubernetes’ history the rule was simple: swap on, kubelet off. The field behind that is failSwapOn, default true, and the kubeadm install guide still describes the two responses: either disable swap (sudo swapoff -a now, and remove it from /etc/fstab or the relevant systemd.swap unit so it stays off after reboot), or tolerate it with failSwapOn: false.

Tolerating swap does not give Pods swap. The documentation is explicit that with failSwapOn: false the kubelet directs the runtime to allocate zero swap to workloads by default, although processes outside Kubernetes, including the kubelet itself, can still swap. Workload swap is a separate decision, made with memorySwap.swapBehavior:

swapBehaviorEffect
NoSwap (default; an empty value behaves the same)Pods “do not and cannot use swap”
LimitedSwapBurstable Pods may use swap, within a computed limit

NodeSwap graduated to stable in Kubernetes 1.34, and LimitedSwap comes with rules that make good exam questions:

  • Only Burstable QoS Pods are eligible; Guaranteed and BestEffort Pods “are prohibited from utilizing swap memory”.
  • High-priority Pods are excluded “to ensure the memory they consume always resides in RAM”.
  • A Burstable container can opt out by setting its memory request equal to its limit.
  • The per-container limit is (containerMemoryRequest / nodeTotalMemory) × totalPodsSwapAvailable, so swap is shared in proportion to requested memory.
  • It needs cgroup v2 (the kubelet sets memory.swap.max on the container cgroup) and Linux.
  • Memory-backed volumes (secret mounts, emptyDir with medium: Memory) are mounted tmpfs with the noswap option so their contents stay in RAM; on kernels older than 6.3 the kubelet checks for support and logs a warning if it cannot guarantee it.

The project’s own recommendations: encrypt swap space, run control-plane nodes without swap, taint swap-enabled nodes, prefer SSD-backed swap, and remember that the scheduler does not account for swap at all, so it changes nothing about where Pods land. For the exam, the two-step distinction (failSwapOn lets the kubelet start; swapBehavior lets Pods swap) is the whole point.

The Defaults the Exam Expects You to Know

You will not be asked to recite the reference page, but you will be asked to change a field and recognise a symptom caused by a default. These are the ones that matter, with their KubeletConfiguration defaults from the v1beta1 reference:

FieldDefaultWhy it matters
staticPodPath"" (kubeadm sets /etc/kubernetes/manifests)no static Pods unless set; see the static Pods guide
maxPods110the classic “allow more Pods on this node” task; the Pod CIDR size and the CNI must have room for them too
podPidsLimit-1 (unlimited)fork-bomb protection; a positive value is enforced per Pod
evictionHardmemory.available: 100Mi, nodefs.available: 10%, nodefs.inodesFree: 5%, imagefs.available: 15%see the trap below; soft thresholds and evictionPressureTransitionPeriod (5m) are covered in the Node Allocatable guide
mergeDefaultEvictionSettingsfalseset true so that unspecified eviction signals keep their defaults instead of becoming zero
imageGCHighThresholdPercent / imageGCLowThresholdPercent85 / 80image GC starts above 85% disk usage and deletes least-recently-used images until 80%
imageMinimumGCAge2man image must be unused this long before GC may remove it
imageMaximumGCAge0s (disabled)stable since 1.35; removes images unused for longer than this regardless of disk pressure; the age tracking resets on kubelet restart
serializeImagePullstrueone image pull at a time per node; set false for parallel pulls
maxParallelImagePullsunset (no limit)stable since 1.35; caps parallel pulls, must be ≥1 and requires serializeImagePulls: false when ≥2, or the kubelet refuses to start
registryPullQPS / registryBurst5 / 10registry pull rate limiting; 0 disables the QPS limit
containerLogMaxSize / containerLogMaxFiles10Mi / 5rotation thresholds; kubectl logs returns only the latest file, so 40 MiB of output yields at most 10 MiB
containerLogMaxWorkers / containerLogMonitorInterval1 / 10sparallel rotation workers and how often the kubelet checks log sizes
nodeStatusUpdateFrequency / nodeStatusReportFrequency / nodeLeaseDurationSeconds10s / 5m / 40heartbeat mechanics; the node failure lifecycle guide explains what happens when they stop
shutdownGracePeriod / shutdownGracePeriodCriticalPods0s / 0sGraceful Node Shutdown is off until you set them; same guide
cgroupDrivercgroupfs (kubeadm writes systemd)see above
failSwapOn / memorySwap.swapBehaviortrue / NoSwapsee above
failCgroupV1truekubelet refuses to start on cgroup v1 hosts
rotateCertificates / serverTLSBootstrapfalse / false (kubeadm enables client rotation)certificate management guide
authentication.anonymous.enabled / authorization.mode / readOnlyPortfalse / Webhook / 0the hardened file defaults from the table earlier
clusterDNS / clusterDomain / resolvConfset by kubeadm (10.96.0.10, cluster.local)wrong values break in-Pod DNS on that node only; the CoreDNS guide covers the symptoms
seccompDefaultfalsetrue applies RuntimeDefault to every container without an explicit profile
crashLoopBackOff.maxContainerRestartPeriodunset (effective 300s)beta and on by default since 1.35; accepts 1s–300s and caps the restart backoff per node
imagePullCredentialsVerificationPolicyNeverVerifyPreloadedImagesbeta since 1.35; controls whether the kubelet re-checks pull credentials for images already on the node
featureGates{}a map, so drop-ins merge it key by key

The evictionHard trap

The documentation spells this out and the exam loves it: “by changing the default value of only one parameter for evictionHard, the default values of other parameters will not be inherited and will be set to zero.” A config file that says only

evictionHard:
  memory.available: "500Mi"

has just disabled disk-pressure eviction entirely, because nodefs.available, nodefs.inodesFree and imagefs.available are now zero. Either list every threshold explicitly or set mergeDefaultEvictionSettings: true so the unspecified ones keep their defaults.

Container restart backoff

The default CrashLoopBackOff curve starts at 10s, doubles each restart (10s, 20s, 40s, …), caps at 300s, and resets after a container has run for 10 minutes. Two newer knobs change it: crashLoopBackOff.maxContainerRestartPeriod in the kubelet config lowers the cap per node (a value under 10s also becomes the initial delay), and the alpha, off-by-default ReduceDefaultCrashLoopBackOffDecay feature gate changes the cluster default to start at 1s and cap at 60s. Per-node configuration wins over the gate. If a troubleshooting task’s Pod seems to take forever to retry after you fixed its image, this is why; kubectl delete pod or a rollout restart skips the wait.

A Symptom-to-Fix Table for a Kubelet That Will Not Start

The method is always the same: systemctl status kubelet, then journalctl -u kubelet -xe --no-pager | tail -50, then read the ExecStart line and open whichever file the error points at. The common failures:

Symptom in journalctl / kubectlCauseFix
failed to load kubelet config file ... strict decoding error: unknown fieldtypo in a config.yaml field name (or a flag-style name used in the file)correct the field; the reference page is the source of names; restart
failed to load kubelet config file ... no such file or directorywrong --config path, usually a tampered 10-kubeadm.conf or a missing /var/lib/kubelet/config.yamlfix the path, or regenerate the file with kubeadm upgrade node phase kubelet-config; daemon-reload if you edited a unit
running with swap on is not supportedswap enabled and failSwapOn: trueswapoff -a plus /etc/fstab, or failSwapOn: false if the task says to tolerate swap
kubelet exits immediately on an old image, log mentions cgroup v1failCgroupV1: true on a cgroup v1 hostboot a cgroup v2 image, or set failCgroupV1: false as a stopgap
validate service connection: ... container runtime is not running or connect: no such file or directorycontainerd/CRI-O stopped, or --container-runtime-endpoint in kubeadm-flags.env points at the wrong socketstart the runtime; compare the socket with crictl info and /var/lib/kubelet/instance-config.yaml
node NotReady, Pods stuck ContainerCreating with cgroup errors on one nodecgroup driver mismatch between kubelet and runtimealign cgroupDriver and SystemdCgroup; on a joined node, prefer rebuilding it
x509: certificate has expired / Unauthorized in kubelet logskubelet client certificate expired or kubelet.conf brokenthe certificate management guide
kubelet healthy but kubectl logs/exec time out for that nodewrong node address, often a multi-NIC host without --node-ipadd --node-ip via kubeadm-flags.env or KUBELET_EXTRA_ARGS; check kubectl get node -o wide
DNS fails only for Pods on one nodenode-specific clusterDNS or resolvConf driftcompare /configz between a good and a bad node
edit to config.yaml has no effecta flag in kubeadm-flags.env or /etc/default/kubelet overrides it, or you forgot to restart`ps -ef
edit to config.yaml disappearedkubeadm upgrade rewrote it from the ConfigMapput the change in the kubelet-config ConfigMap, a patch, or a drop-in directory

Two universal rules: systemctl daemon-reload is needed after editing a unit or an EnvironmentFile referenced by one, and never needed after editing config.yaml; and kubeadm upgrade node phase kubelet-config is both the repair command for a mangled config.yaml and the reason your local edits vanish.

Exam Drills

Work these on a two-node kubeadm cluster. Each is sized like a real CKA task.

  1. Raise the Pod limit on one worker. Set maxPods: 150 on node01 only, without changing the ConfigMap. Confirm with /configz and kubectl describe node node01 | grep -A1 Allocatable.
  2. Make the change cluster-wide and durable. Edit the kubelet-config ConfigMap to set maxPods: 150, roll it out to each node with kubeadm upgrade node phase kubelet-config and a restart, one node at a time, draining first if the task says workloads must not be disrupted.
  3. Use a drop-in. Add --config-dir=/etc/kubernetes/kubelet.conf.d through KUBELET_EXTRA_ARGS, create 10-pids.conf with podPidsLimit: 2048 and apiVersion/kind, restart, and prove via /configz that maxPods from the main file survived and podPidsLimit changed.
  4. Break and fix. Introduce a typo (maxPodz) in config.yaml, restart, read the strict-decoding error in journalctl, and fix it. Then point --config at a non-existent path in a unit override and practise systemctl cat kubelet to find it.
  5. Swap. Enable swap on a worker (fallocate, mkswap, swapon), watch the kubelet refuse to start, then solve it both ways: swapoff -a with /etc/fstab cleaned, and failSwapOn: false with swapBehavior: LimitedSwap, checking kubectl top nodes --show-swap.

Common Mistakes

  • Editing the ConfigMap and expecting nodes to change. The ConfigMap is the source; kubeadm upgrade node phase kubelet-config plus a restart is the delivery.
  • Editing config.yaml and losing it at the next upgrade. Local edits are overwritten; durable node-specific settings go in a patch, kubeadm-flags.env, KUBELET_EXTRA_ARGS or a drop-in directory.
  • Forgetting that flags beat the file. A --max-pods flag lingering in kubeadm-flags.env silently wins over maxPods in the file. Always check ps -ef.
  • Assuming the file defaults match the flag defaults. Anonymous auth, authorization mode and the read-only port differ.
  • Setting one evictionHard threshold. The rest become zero unless mergeDefaultEvictionSettings: true.
  • Appending to a list in a drop-in. Lists are replaced wholesale; include every entry you want to keep.
  • Flipping the cgroup driver on a joined node. Recreate the node instead; and remember that with KubeletCgroupDriverFromCRI the runtime’s setting is the one that counts.
  • Confusing failSwapOn: false with enabling swap for Pods. Pods only swap under swapBehavior: LimitedSwap, and only Burstable ones.
  • Running daemon-reload for a config.yaml change and skipping it for a unit change. It is the other way round.
  • Restarting the kubelet on all nodes at once. The documented procedure is one node at a time, so workloads can be rescheduled.

Frequently Asked Questions

Where is the kubelet configuration file on a kubeadm cluster?

/var/lib/kubelet/config.yaml, passed to the kubelet through the --config flag in the KUBELET_CONFIG_ARGS variable of /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf. Dynamic flags live in /var/lib/kubelet/kubeadm-flags.env and user overrides in /etc/default/kubelet (DEB) or /etc/sysconfig/kubelet (RPM). Run systemctl cat kubelet to see the whole chain.

What takes precedence, kubelet flags or the config file?

Flags, with one exception. The documented order from lowest to highest is: --feature-gates on the command line, then the config file, then drop-in files in sort order, then every other command-line flag. So a flag in kubeadm-flags.env overrides the file, but a featureGates map in the file overrides the --feature-gates flag.

How do I change a kubelet setting on every node in a kubeadm cluster?

Edit the kubelet-config ConfigMap in kube-system (kubectl edit cm -n kube-system kubelet-config), then on each node run kubeadm upgrade node phase kubelet-config to rewrite /var/lib/kubelet/config.yaml and systemctl restart kubelet. Do it one node at a time. Editing the ConfigMap alone changes nothing on running kubelets.

How do the kubelet drop-in configuration files merge?

Files in --config-dir must end in .conf, include apiVersion and kind, and are applied in alphanumeric order of their full file names, later files replacing fields from earlier ones. Scalars are replaced, nested structs are merged field by field, maps such as featureGates are merged key by key, and lists such as clusterDNS are replaced in their entirety. Validation happens once on the merged result.

Why must the kubelet and container runtime use the same cgroup driver?

Both manage cgroups for the same containers, and two different managers on a systemd host make the node unstable under resource pressure. kubeadm defaults the kubelet to systemd; containerd must be told with SystemdCgroup = true, while CRI-O defaults to systemd. Since Kubernetes 1.34 the GA KubeletCgroupDriverFromCRI feature lets the kubelet read the driver from a runtime that supports the RuntimeConfig CRI call and ignore its own cgroupDriver setting, which removes the mismatch on modern pairings.

Does the kubelet still refuse to start when swap is enabled?

By default yes, because failSwapOn defaults to true. Set it to false to let the kubelet start on a node with swap, which still gives Pods no swap. To let Burstable Pods use swap, additionally set memorySwap.swapBehavior: LimitedSwap; NodeSwap has been stable since Kubernetes 1.34 and requires cgroup v2.

What happens if I set only one evictionHard threshold?

Every other hard eviction threshold becomes zero, which disables disk-pressure eviction. Specify all of them, or set mergeDefaultEvictionSettings: true so the unspecified signals keep their defaults (memory.available: 100Mi, nodefs.available: 10%, nodefs.inodesFree: 5%, imagefs.available: 15%).

How can I see the configuration a kubelet is actually running with?

Query its /configz endpoint through the API server: kubectl get --raw "/api/v1/nodes/<node>/proxy/configz". The response shows the final merged configuration, including defaults and the effect of any drop-ins, which makes it the fastest way to confirm an edit took effect and to compare a healthy node with a broken one.

Conclusion

The kubelet is configured through a chain, not a file: a systemd unit assembles four variables, the --config file carries most settings, optional drop-ins layer node-specific overrides on top, and flags from kubeadm-flags.env and KUBELET_EXTRA_ARGS sit above all of it. kubeadm keeps the file in step with the kubelet-config ConfigMap, which is why cluster-wide changes go through kubectl edit cm plus kubeadm upgrade node phase kubelet-config, and why local edits vanish at upgrade time. Around that mechanism sit a few defaults with outsized consequences: cgroupDriver must match the runtime unless the runtime is telling the kubelet what to use, failCgroupV1 and failSwapOn decide whether the kubelet starts at all, evictionHard zeroes what you do not mention, and serializeImagePulls, containerLogMaxSize and maxPods shape the behaviour of every node. Read systemctl cat kubelet, ps -ef and /configz before you edit anything, and the “kubelet will not start” task becomes a five-minute item rather than a twenty-minute one.

If you want to rehearse these tasks under time pressure, Sailor.sh’s CKA Certification-Ready Mock Exam Bundle includes kubelet and node configuration scenarios in its Cluster Architecture and Troubleshooting sections across five timed mock exams with explanations, so each wrong turn on precedence or on the reconfiguration loop becomes a fixed habit before exam day. Pair it with the CKA exam guide, the 30-day study plan and the free CKA practice questions.

Continue with the Node Allocatable guide for the reservation and eviction fields, the node failure lifecycle guide for heartbeats and graceful shutdown, and the kubeadm installation guide for the init and join flows that write these files in the first place.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now