CertGrid CertGrid
Concepts·Kubernetes and Cloud Native Associate

Cloud Native Standards: CRI, CNI, CSI and OCI

CRI, CNI, CSI and OCI are not trivia - they are why you can swap Docker for containerd, Flannel for Calico, or one storage backend for another without Kubernetes noticing. Each one is visible on a running cluster in a single command.

Cloud Native Architecture Guide 29 of 46 Beginner

Written against the versions above. The interfaces below are versioned standards and change slowly; the implementations plugged into them change constantly. That is the point of them.

A four-node kubeadm cluster with Calico for CNI and two CSI drivers installed.
Server NameIP AddressOSRolesCPURAMHDD
CKA1001192.168.0.175Ubuntu 26.04 LTSControl Plane Node2 Core4 GB50 GB
CKA1001-NODE01192.168.0.176Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE02192.168.0.177Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE03192.168.0.178Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. CRI: the runtime is replaceable

    The Container Runtime Interface is the gRPC API the kubelet speaks to whatever runs containers. RuntimeName: containerd with RuntimeApiVersion: v1 is two facts: which implementation is plugged in, and which version of the contract it honours.

    This interface is why the dockershim removal in 1.24 was survivable. Kubernetes did not learn to speak to containerd - it already only spoke CRI, and Docker was the thing needing a shim to fit.

    bash Example session
    sudo -n crictl version | grep -E 'RuntimeApiVersion|RuntimeName'RuntimeName:  containerdRuntimeApiVersion:  v1

    Expected resultThe runtime name and the CRI API version it implements.

    Success conditionYou can name your runtime and the interface version it satisfies.

  2. CNI: the network is replaceable

    The Container Network Interface is a much smaller contract: a binary, a JSON config, and two verbs - ADD and DEL. When a sandbox is created, the runtime invokes the CNI plugin to give it an interface and an IP.

    Two networks are configured here. k8s-pod-network is Calico, doing the real work. cni-loopback is the trivial plugin that gives every sandbox its lo interface.

    This is the layer that makes "my Pods have no IP" a CNI question rather than a Kubernetes question - and why the answer usually lives in /etc/cni/net.d/ rather than in any Kubernetes object.

    bash Example session
    sudo -n crictl info | python3 -c "import json,sys; d=json.load(sys.stdin); print('CNI networks:', [n['Config']['Name'] for n in d['cniconfig']['Networks']])"CNI networks: ['cni-loopback', 'k8s-pod-network']

    Expected resultThe configured CNI network names, Calico's among them.

    Success conditionYou can list the CNI networks the runtime knows about.

  3. CSI: storage is replaceable, and pluggable at runtime

    The Container Storage Interface is the newest of the four and the most visible inside Kubernetes, because a CSI driver registers itself as an object you can list. Two are installed here: hostpath.csi.k8s.io for local volumes and csi.tigera.io, which Calico uses for its own needs.

    StorageClasses are where CSI meets everyday use. csi-hostpath-sc names a CSI provisioner; local-path names rancher.io/local-path, which is not CSI at all. Both work, and the difference matters when you ask for a feature - snapshots, expansion, topology - that only the CSI path implements.

    Note ALLOWVOLUMEEXPANSION: true for the CSI class, false for the other. That is the standard buying you a capability.

    bash Example session
    kubectl --context cka1001 get csidriversNAME                  ATTACHREQUIRED   PODINFOONMOUNT   STORAGECAPACITY   TOKENREQUESTS   REQUIRESREPUBLISH   MODES                  AGEcsi.tigera.io         true             true             false             <unset>         false               Ephemeral              16hhostpath.csi.k8s.io   true             true             false             <unset>         false               Persistent,Ephemeral   8hkubectl --context cka1001 get storageclassNAME                   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGEcsi-hostpath-sc        hostpath.csi.k8s.io     Delete          Immediate              true                   8hlocal-path (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  14h

    Expected resultTwo CSI drivers, and two StorageClasses with different provisioners and different capabilities.

    Success conditionYou can tell which of your StorageClasses is CSI-backed.

  4. OCI: the image is portable

    The Open Container Initiative standardises two things: the image format and the runtime spec. runc --version reports spec: 1.2.1 - that is the OCI runtime spec, not runc's own version.

    The image side is why one tag serves every architecture. The pause image's manifest is a manifest list: one document naming per-platform manifests, and the runtime picks the entry matching the node. Five architectures here, from a single registry.k8s.io/pause:3.10.1.

    So "it works on my machine" has a precise meaning: the same digest, resolved on a different platform, is a different set of bytes - deliberately.

    bash Example session
    sudo -n ctr -n k8s.io content get $(sudo -n ctr -n k8s.io images ls name==registry.k8s.io/pause:3.10.1 | awk 'NR==2 {print $3}') | head -c 520{   "schemaVersion": 2,   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",   "manifests": [      {         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",         "size": 501,         "digest": "sha256:e5b941ef8f71de54dc3a13398226c269ba217d06650a21bd3afcf9d890cf1f41",         "platform": {            "architecture": "amd64",            "os": "linux"         }      },      {         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",sudo -n ctr -n k8s.io content get $(sudo -n ctr -n k8s.io images ls name==registry.k8s.io/pause:3.10.1 | awk 'NR==2 {print $3}') | python3 -c "import json,sys; d=json.load(sys.stdin); print('mediaType:', d['mediaType']); print('platforms:', [m['platform']['architecture'] for m in d['manifests']][:8])"mediaType: application/vnd.docker.distribution.manifest.list.v2+jsonplatforms: ['amd64', 'arm', 'arm64', 'ppc64le', 's390x', 'amd64', 'amd64']

    Expected resultA manifest list media type, and the architectures it covers.

    Success conditionYou can show one tag resolving to several platform manifests.

Troubleshooting

Official sources