The Container Runtime Interface (CRI) is the thin, standardized layer that lets Kubernetes talk to any container runtime, which is a surprisingly loose concept.
Let’s see it in action. Imagine you have a pod definition, like this simple nginx pod:
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
When Kubernetes decides to run this pod on a node, it doesn’t directly tell Docker or containerd "start this container." Instead, it communicates with the kubelet on that node. The kubelet, in turn, uses the CRI to interact with the configured container runtime. This interaction involves two main CRI services:
- RuntimeService: This is responsible for managing the lifecycle of containers and pods. Think of it as the "create, start, stop, delete" part.
- ImageService: This handles pulling and managing container images.
The kubelet, acting as the CRI client, makes gRPC calls to the container runtime’s CRI endpoint (typically a Unix domain socket). For our nginx-pod, the kubelet would:
- Call
ImageService.PullImageto fetch thenginx:latestimage. - Call
RuntimeService.RunPodSandboxto create the network namespace and other pod-level resources. - Call
RuntimeService.CreateContainerto set up the container’s filesystem, environment variables, and command. - Call
RuntimeService.StartContainerto actually launch the container process.
This abstraction means you could theoretically swap out your container runtime (e.g., from containerd to CRI-O) without changing Kubernetes itself, as long as the new runtime implements the CRI spec.
The problem CRI solves is standardizing the interface between Kubernetes and the myriad of container runtimes that exist. Before CRI, Kubernetes had tight integrations with specific runtimes like Docker. This made it difficult to adopt new runtimes or for runtimes to gain broad Kubernetes support. CRI defines a set of gRPC APIs that any compliant runtime must expose, allowing Kubernetes to interact with them in a consistent way. It separates the concerns: Kubernetes manages orchestration, and the runtime manages containers.
The most surprising thing about CRI is how it fundamentally shifted the responsibility for container lifecycle management away from the orchestrator and towards specialized runtimes. Kubernetes doesn’t "know" how to start a container process; it delegates that entirely. The container runtime is the actual entity that interacts with the OS kernel’s namespaces and cgroups, sets up the filesystem, and executes the container’s entrypoint. Kubernetes simply orchestrates these runtime operations.
Here’s a peek into what a RunPodSandbox request might look like conceptually (this isn’t actual wire format, but the intent):
{
"podSandboxConfig": {
"metadata": {
"name": "nginx-pod-sandbox",
"namespace": "default",
"uid": "a1b2c3d4-e5f6-7890-1234-567890abcdef"
},
"linux": {
"cgroupParent": "kubepods/besteffort/pod-a1b2c3d4-e5f6-7890-1234-567890abcdef"
},
"dnsConfig": {
"nameservers": ["10.0.0.10"],
"searches": ["default.svc.cluster.local", "svc.cluster.local", "cluster.local"]
}
}
}
And a CreateContainer request, building on the sandbox:
{
"podSandboxId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"config": {
"metadata": {
"name": "nginx-container",
"uid": "f0e1d2c3-b4a5-6789-0123-456789abcdef"
},
"image": {
"image": "nginx:latest"
},
"command": ["nginx", "-g", "daemon off;"],
"args": [],
"workingDir": "/",
"envs": [
{"name": "NGINX_PORT", "value": "80"}
]
},
"sandboxConfig": {
// ... same as RunPodSandbox ...
}
}
The runtime receives these requests, translates them into low-level OS calls (like clone for namespaces, mount for filesystems, and execve for the process), and reports back success or failure.
The one thing that trips many people up is that CRI doesn’t define how the runtime should implement these operations, only what operations it must provide. This means the underlying implementation details can vary significantly between runtimes. For instance, containerd might use its own internal state management and direct interaction with runc or crun, while CRI-O is built around OCI runtimes and systemd. Kubernetes doesn’t care; it just needs the gRPC endpoints to respond correctly.
The next layer of complexity you’ll encounter is how Kubernetes handles networking and storage through the Container Network Interface (CNI) and Container Storage Interface (CSI) respectively, which are also abstractions managed by the kubelet.