🏗️ Kubelet Architecture

The kubelet is the node agent — a long-running process on every node (including control-plane nodes). It watches the API server for Pods assigned to its node (spec.nodeName == thisNode), then drives each Pod through its full lifecycle using three plugin interfaces: CRI (container runtime), CNI (networking), and CSI (storage).

kube-apiserver Watch pods assigned to node kubelet Pod Manager desired ↔ actual PLEG Pod Lifecycle Event Gen Eviction Manager node pressure Volume Mgr attach / mount CRI containerd / CRI-O CNI Calico / Cilium / Flannel CSI EBS / GCP PD / Longhorn Worker Node (Linux kernel: cgroups, namespaces, iptables)

PLEG

Pod Lifecycle Event Generator — polls the container runtime every second and emits events when container state changes (started, died, etc.).

Pod Manager

Reconciles desired pod state (from API server) with actual state (from PLEG). Drives CRI calls to create/stop/restart containers.

Eviction Manager

Monitors node memory, disk, and PID pressure. Evicts pods when thresholds are exceeded to protect node stability.

Volume Manager

Attaches/detaches volumes via CSI and mounts/unmounts them into pod sandbox directories before container start.

🚀 Pod Lifecycle — Admit to Running

PendingnodeName set
Pullingimage pull
Creatingsandbox + volumes
Startinginit containers
Runningapp containers
TerminatingSIGTERM → SIGKILL

1 — Admission

When the kubelet sees a new Pod assigned to its node it first runs admission handlers — these are local checks (not API server admission):

  • Resource availability — enough CPU/memory on node, not exceeding node allocatable
  • Node selectors / affinity — should have been enforced by scheduler but kubelet double-checks
  • Eviction thresholds — if the node is already under memory pressure, the pod may be rejected

2 — Image Pull

Kubelet asks the CRI (containerd) to pull required images. Pull policy controls this:

imagePullPolicyBehaviour
AlwaysPull on every pod start — ensures latest image but slower startup
IfNotPresentPull only if image not cached locally (default for tagged images)
NeverNever pull — image must already exist on node
⚠️ Use digest-pinned images in production With imagePullPolicy: IfNotPresent and a mutable tag like :latest, different nodes may run different image versions. Pin with image: nginx@sha256:abc… for reproducibility.

3 — Sandbox & Volume Setup

Kubelet creates the pause container (infra/sandbox) first. This container holds the network namespace shared by all containers in the pod. Then CNI is called to attach the network interface and assign the pod IP. Then CSI volumes are mounted.

4 — Init Containers

Init containers run sequentially to completion before any app containers start. A failure restarts the init container (per restartPolicy). Use cases: DB schema migration, config file generation, waiting for dependencies.

spec:
  initContainers:
    - name: wait-for-db
      image: busybox
      command: ['sh', '-c',
        'until nc -z postgres-svc 5432; do echo waiting; sleep 2; done']
    - name: run-migrations
      image: myapp:latest
      command: ['./migrate', '--up']
  containers:
    - name: app
      image: myapp:latest

5 — Lifecycle Hooks & Probes

spec:
  containers:
    - name: app
      lifecycle:
        postStart:               # runs immediately after container starts
          exec:
            command: ["/bin/sh", "-c", "echo started > /tmp/ready"]
        preStop:                 # runs before SIGTERM — use for graceful drain
          exec:
            command: ["/bin/sh", "-c", "nginx -s quit; sleep 5"]
      startupProbe:              # delays liveness until app is ready to start
        httpGet: { path: /healthz, port: 8080 }
        failureThreshold: 30
        periodSeconds: 10
      livenessProbe:             # restart container if unhealthy
        httpGet: { path: /healthz, port: 8080 }
        initialDelaySeconds: 0
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:            # remove from Service endpoints if not ready
        httpGet: { path: /ready, port: 8080 }
        periodSeconds: 5
        failureThreshold: 3
💡 preStop hook + terminationGracePeriodSeconds Kubernetes sends SIGTERM concurrently with the preStop hook. Set terminationGracePeriodSeconds long enough for both the hook and the graceful shutdown to finish. Add a sleep 5 in preStop to let the load balancer drain connections before the process exits.

🔌 CRI, CNI & CSI Integration

CRI — Container Runtime Interface

The kubelet talks to the container runtime via a gRPC API defined by CRI. This decouples the kubelet from any specific runtime. The two main CRI implementations are containerd and CRI-O.

CRI callWhat it does
RunPodSandboxCreates the pause container and network namespace
PullImagePulls an OCI image if not cached; returns image ref
CreateContainerCreates a container in the sandbox (not yet started)
StartContainerStarts the created container
StopContainerSends SIGTERM then SIGKILL after grace period
RemovePodSandboxTears down sandbox and network namespace
ContainerStatusGets container state, exit code, start time
# Inspect containers via crictl (bypasses kubelet/docker)
crictl ps                          # running containers
crictl pods                        # running sandboxes
crictl images                      # cached images
crictl logs <container-id>         # container stdout/stderr
crictl exec -it <container-id> sh  # shell into container
crictl inspect <container-id>      # full container spec + state

# Check CRI socket kubelet is using
ps aux | grep kubelet | grep container-runtime-endpoint
# or in kubelet config: --container-runtime-endpoint=unix:///run/containerd/containerd.sock

CNI — Container Network Interface

After the CRI creates the pod sandbox, the kubelet invokes the CNI plugin binary to configure networking. CNI plugins are executables in /opt/cni/bin/ called with a JSON config from /etc/cni/net.d/.

# CNI is called with ADD/DEL/CHECK commands
# ADD: called when sandbox is created — assigns IP, sets up veth pair, routes
# DEL: called when sandbox is deleted — tears down networking
# CHECK: verifies network state matches config

# CNI config example (Flannel)
cat /etc/cni/net.d/10-flannel.conflist
# {
#   "name": "cbr0",
#   "plugins": [
#     { "type": "flannel", "delegate": { "hairpinMode": true, "isDefaultGateway": true } },
#     { "type": "portmap", "capabilities": { "portMappings": true } }
#   ]
# }

# View pod network namespace
POD_ID=$(crictl pods --name myapp -q)
crictl inspectp $POD_ID | jq '.status.network'

CSI — Container Storage Interface

The kubelet's Volume Manager interacts with CSI drivers via gRPC to attach, mount, and unmount volumes. The flow for a PVC-backed volume:

  1. Attach — CSI controller plugin attaches the cloud disk to the node (EBS AttachVolume)
  2. Mount — CSI node plugin mounts the device to a global mount point on the node
  3. Bind-mount — Kubelet bind-mounts from the global path into the pod's container filesystem
# Check volume attachment status
kubectl get volumeattachments
# NAME                               ATTACHER                  PV           NODE     ATTACHED
# csi-abc123...                      ebs.csi.aws.com           pvc-xyz...   node-1   true

# Inspect mounted volumes on a node
ls /var/lib/kubelet/pods/<pod-uid>/volumes/
# kubernetes.io~configmap/  kubernetes.io~secret/  kubernetes.io~csi/

# Check if a CSI driver is registered
kubectl get csidrivers
kubectl get csinodes

📊 Node Status, Eviction & Production Tips

Node Status Reporting

Kubelet updates the Node object in the API server every --node-status-update-frequency (default 10s). It reports conditions, allocatable resources, images cached, and attached volumes.

# Node conditions reported by kubelet
kubectl describe node worker-1 | grep -A 20 Conditions
# Type                 Status   Reason
# MemoryPressure       False    KubeletHasSufficientMemory
# DiskPressure         False    KubeletHasNoDiskPressure
# PIDPressure          False    KubeletHasSufficientPID
# Ready                True     KubeletReady

# Allocatable vs Capacity — kubelet reserves resources for OS + kubelet itself
kubectl get node worker-1 -o json | jq '{capacity: .status.capacity, allocatable: .status.allocatable}'
# capacity.memory:    16Gi
# allocatable.memory: 15.5Gi  ← ~500Mi reserved for system

Node-Pressure Eviction

When the eviction manager detects resource pressure it evicts pods — lowest priority and highest consumption first — to protect node stability.

SignalSoft thresholdHard thresholdEffect
memory.available< 500Mi (grace period)< 100Mi (immediate)Evict pods, set MemoryPressure=True
nodefs.available< 10% (grace period)< 5% (immediate)Evict pods, set DiskPressure=True
imagefs.available< 15%< 10%Garbage collect images, evict if still low
pid.available< 20%< 10%Evict pods, set PIDPressure=True
# Configure eviction thresholds in kubelet config
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
evictionHard:
  memory.available: "200Mi"
  nodefs.available: "10%"
  imagefs.available: "15%"
evictionSoft:
  memory.available: "500Mi"
  nodefs.available: "15%"
evictionSoftGracePeriod:
  memory.available: "1m30s"
  nodefs.available: "2m"
evictionMaxPodGracePeriod: 90   # cap on graceful termination during eviction

Key kubelet Configuration Flags

# Preferred: use KubeletConfiguration file
--config=/etc/kubernetes/kubelet-config.yaml

# Key settings:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd                    # must match container runtime (containerd)
maxPods: 110                             # max pods per node (default 110)
kubeReserved:                            # reserve for kubelet/OS
  cpu: "200m"
  memory: "512Mi"
systemReserved:
  cpu: "200m"
  memory: "256Mi"
nodeStatusUpdateFrequency: "10s"
imageGCHighThresholdPercent: 85          # start GC when image fs > 85%
imageGCLowThresholdPercent: 80           # stop GC when image fs < 80%
containerLogMaxSize: "50Mi"              # per-container log rotation size
containerLogMaxFiles: 5

Match cgroupDriver

Kubelet and containerd must use the same cgroup driver (systemd or cgroupfs). Mismatch causes pod failures with cgroup errors.

Set kubeReserved

Always reserve CPU and memory for the kubelet and system. Without it, node-level processes compete with pods and can cause OOM kills of system daemons.

Monitor PLEG

Watch for "PLEG is not healthy" in kubelet logs — means the runtime is slow to respond. Usually indicates containerd or the node itself is under heavy load.

Use crictl not docker

On modern nodes containerd is the runtime. Use crictl for debugging — docker commands won't see kubelet-managed containers.

📝 Knowledge Check

Q1. A pod has been running for weeks and suddenly its container is restarted without any deployment change. What kubelet mechanism most likely triggered this?
  • A) The scheduler evicted the pod and rescheduled it
  • B) The liveness probe failed — kubelet restarted the container
  • C) The API server sent a restart command
  • D) The node's cgroup driver was changed
B) Liveness probe failure. Kubelet continuously runs liveness probes. If a probe fails failureThreshold consecutive times, kubelet restarts the container in-place (without rescheduling). Check kubectl describe pod for "Liveness probe failed" events and kubectl get pod -o json | jq '.status.containerStatuses[].restartCount'.
Q2. What is the purpose of the pause container (infra container) in every pod sandbox?
  • A) It runs health checks for all containers in the pod
  • B) It holds the shared network namespace (IP, ports) that all pod containers join
  • C) It provides the shared process namespace for inter-container communication
  • D) It is used by the kubelet to inject secrets into containers
B) Holds the shared network namespace. The pause container is created first by the CRI and holds the network namespace (IP address, loopback, routes). All other containers in the pod join this namespace — that's why they can reach each other on localhost and share the same pod IP. The pause container does nothing else (it just sleeps).
Q3. A node's memory.available drops below the hard eviction threshold. What does kubelet do immediately?
  • A) Cordons the node and waits for an operator to drain it
  • B) Restarts the kubelet process to free memory
  • C) Evicts pods starting with the lowest-priority and highest-memory consumers; sets MemoryPressure=True
  • D) Sends a SIGKILL to all containers on the node simultaneously
C) Evicts pods; sets MemoryPressure=True. Kubelet's eviction manager selects pods to evict based on QoS class (BestEffort first, then Burstable, Guaranteed last) and memory consumption. It sets the MemoryPressure node condition to True, which causes the scheduler to avoid scheduling new BestEffort/Burstable pods on that node.