Kubelet & Container Runtime
The node agent — from scheduled pod to running containers
Kubelet Overview
The kubelet is the node-level agent that runs on every node in the cluster. It takes pod specs from the API server and ensures the described containers are running and healthy.
What It Does
- Watches for pods assigned to its node (via API server watch on
.spec.nodeName) - Manages the container runtime via CRI (Container Runtime Interface) over gRPC
- Reports node status (capacity, conditions, addresses) back to the API server
- Reports pod status and container states
- Runs probes (liveness, readiness, startup)
- Manages volumes — mounts them before containers start
- Manages container logs
What It Is Not
- Not a controller — it doesn't make global decisions, it executes locally
- Does NOT manage containers that were not created by Kubernetes
- Does NOT talk to etcd — only to the API server
- Does NOT handle networking rules — that's kube-proxy / CNI plugin
- Runs as a systemd service on the node (not in a container)
Pod Lifecycle: End-to-End
Follow a Deployment from kubectl apply all the way to a running container on a node. This shows how all control plane components collaborate.
kubectl apply
User submits Deployment
to API serverAPI Server
Validates, stores
Deployment in etcdDeployment Ctrl
Creates ReplicaSet
stored in etcdReplicaSet Ctrl
Creates Pod (unscheduled)
stored in etcd
Scheduler
Assigns node to Pod
sets .spec.nodeNameKubelet
Picks up Pod via watch
calls CRIContainer Runtime
Pulls image, creates
and starts containerRunning
Kubelet reports status
back to API server
Note
Key observation: No component in this chain "calls" the next step directly. Each writes to etcd (via the API server), and the next component picks it up via its own watch. This is the decoupled, event-driven architecture of Kubernetes.
CRI in Action
When the kubelet needs to start a pod, it makes a sequence of CRI (Container Runtime Interface) gRPC calls to the container runtime (containerd, CRI-O). Here is the exact sequence:
- RunPodSandbox — Creates the pod sandbox, which is the "pause" container. This tiny container (runs
/pause, an infinite sleep) exists solely to hold the pod's Linux namespaces (network, IPC, PID). All other containers in the pod join these namespaces.
The CNI plugin is invoked here to set up pod networking — assigns IP, creates veth pair, configures routes. - PullImage — Downloads the container image from the registry (if not already cached locally). Image pull policy:
Always,IfNotPresent, orNever. - CreateContainer — Creates the container within the sandbox. The container shares the network namespace (and optionally PID namespace) with the pause container. OCI runtime spec is generated here.
- StartContainer — Starts the container process. The OCI runtime (runc, crun, kata) is invoked to actually launch the process with the appropriate namespaces, cgroups, and seccomp profiles.
The Pause Container — Why It Exists
PID 1 in pod
holds namespaces
runs /pause (sleep)
Your application
joins pod NS
shares network with pause
e.g., Envoy proxy
joins pod NS
localhost comms with app
All containers in a pod share the same IP address and can communicate via localhost. If the app container crashes and restarts, the network namespace (and its IP) persists because the pause container is still running.
Init Containers
Init containers run before application containers, one at a time, in order. Each must complete successfully before the next starts. If an init container fails, the kubelet restarts it (subject to the pod's restart policy).
Init 1
e.g., wait for DB
to be reachableInit 2
e.g., clone config
from git repoInit 3
e.g., run DB
migrationsApp Containers
All start together
after inits complete
Common use cases:
- Wait for dependencies — poll until a database or service is ready
- Set up configuration — clone config from a repo, generate certs
- Run migrations — apply schema changes before the app starts
- Download artifacts — fetch data files, ML models
# Init container example in pod spec
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z postgres 5432; do sleep 2; done']
- name: run-migrations
image: myapp:latest
command: ['python', 'manage.py', 'migrate']
containers:
- name: web
image: myapp:latestSidecar Containers (K8s 1.28+)
Before K8s 1.28, sidecars were just regular containers in the pod — no guaranteed startup order, no graceful shutdown order. The native sidecar feature fixes this by using restartPolicy: Always on init containers.
How It Works
- Defined in
.spec.initContainers[]withrestartPolicy: Always - Starts before app containers (as part of init sequence)
- Runs for the entire lifetime of the pod
- Shuts down after all app containers have terminated
- Guaranteed startup order: sidecars first, then app containers
Use Cases
- Service mesh proxies — Istio/Envoy sidecar must be running before the app sends traffic
- Log collectors — Fluentd/Fluent Bit shipping logs from shared volumes
- Config watchers — detect config changes and signal the app
- Auth proxies — handle TLS termination or token refresh
# Native sidecar container (K8s 1.28+)
spec:
initContainers:
- name: envoy-proxy
image: envoyproxy/envoy:v1.28
restartPolicy: Always # ← this makes it a sidecar
ports:
- containerPort: 15001
containers:
- name: app
image: myapp:latest
# envoy-proxy is guaranteed running before app startsProbes
Probes let the kubelet monitor container health and readiness. They determine whether a container should be restarted, receive traffic, or keep starting up.
Startup Probe
- Runs once during startup
- Delays liveness & readiness probes until it succeeds
- For slow-starting apps (Java, large datasets, ML model loading)
- Failure: kubelet kills and restarts the container
- Max startup time =
failureThreshold × periodSeconds
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10
# 30 × 10 = 300s max startupLiveness Probe
- Runs continuously after startup
- Detects deadlocks, hung processes, broken state
- Failure: kubelet kills and restarts the container
- Types: HTTP GET, TCP socket, gRPC, exec command
- Be careful: overly aggressive liveness probes cause restart storms
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3Readiness Probe
- Runs continuously after startup
- Controls traffic routing — failing removes pod from Service endpoints
- Failure: pod stays running but gets no traffic
- Use for: warmup periods, dependency checks, backpressure
- Pod is not killed — just temporarily removed from load balancer
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
failureThreshold: 3Warning
Common mistake: Using the same endpoint for liveness and readiness probes. If a pod is temporarily overloaded, you want it to stop receiving traffic (readiness) but NOT get killed (liveness). Use separate endpoints: /healthz for liveness (is the process alive?) and /ready for readiness (can it handle requests?).
Termination Sequence
Graceful shutdown is critical for zero-downtime deployments. Understanding this exact sequence prevents dropped connections and data loss.
- Pod deletion requested — API server sets
deletionTimestampon the pod. The pod entersTerminatingstate. - Pod removed from Service endpoints — the Endpoint controller removes the pod's IP from all Services. No new traffic is routed to it. (This happens in parallel with step 3 — race condition risk!)
- preStop hook runs (if defined) — e.g., drain connections, deregister from service discovery, flush buffers. This is your chance for graceful cleanup.
- SIGTERM sent — the container's main process (PID 1) receives
SIGTERM. Your application should handle this signal: stop accepting new requests, finish in-flight work, close connections. - Grace period — default 30 seconds (configurable via
terminationGracePeriodSeconds). The kubelet waits for the process to exit cleanly. - SIGKILL sent — if the process is still running after the grace period, the kubelet sends
SIGKILL(forceful, uncatchable kill). - Cleanup — container is removed, pod status is updated to
SucceededorFailed, API server is notified.
Tip
Best practice: Add a preStop hook with a small sleep (e.g., 5s) to give the Endpoint controller time to propagate the removal before the app starts shutting down. This avoids the race condition where traffic is still being sent to a pod that has already started terminating.
# Graceful shutdown configuration
spec:
terminationGracePeriodSeconds: 60
containers:
- name: web
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5 && /app/graceful-shutdown.sh"]Device Plugins
Device plugins extend the kubelet to advertise and manage custom hardware resources — GPUs, FPGAs, InfiniBand NICs, and other accelerators.
How Device Plugins Work
Register
Plugin registers with
kubelet via gRPC
(Unix socket)Discover
Plugin reports available
devices to kubelet
(e.g., 4 GPUs)Advertise
Kubelet reports to
API server as node
allocatable resourcesAllocate
When pod is scheduled,
plugin allocates specific
device(s) to container
# Requesting a GPU in a pod spec
spec:
containers:
- name: training
image: pytorch/pytorch:2.1
resources:
limits:
nvidia.com/gpu: "1" # request 1 NVIDIA GPU| Device Plugin | Resource Name | Hardware |
|---|---|---|
| NVIDIA GPU | nvidia.com/gpu |
NVIDIA GPUs (Tesla, A100, H100) |
| AMD GPU | amd.com/gpu |
AMD Instinct GPUs |
| Intel FPGA | fpga.intel.com/af-* |
Intel FPGA accelerators |
| SR-IOV | intel.com/sriov-nic |
SR-IOV virtual functions |
| RDMA/InfiniBand | rdma/hca |
RDMA-capable NICs |
Cross-References
Note
- For CNI invocation during RunPodSandbox, see Net 12 — CNI
- For CRI implementation details (containerd, CRI-O), see Virt 06 — containerd & CRI
- For workload types (Deployments, StatefulSets, DaemonSets) and scheduling details, see Virt 08 — Workloads & Scheduling
- For control plane architecture, see Virt 08 — Control Plane