Kubernetes Multi-Runtime Cluster Playground
A multi-node, multi-runtime Kubernetes cluster provisioned with kubeadm.
- eth0
- /
- oci://ghcr.io/sagikazarmark/iximiuz-labs/playgrounds/kubeadm-cluster/dev-machine:live@sha256:686acd9a10da117be78b736b6cbba8d231da71f3b9152386c1952807ea0a6e62
- eth0
- /
- oci://ghcr.io/sagikazarmark/iximiuz-labs/playgrounds/kubernetes-multi-runtime/control-plane:live@sha256:380f3c104fb416203b6b919666a559af4f983af83607dd527e4d8a8ff0f01e89
- eth0
- /
- oci://ghcr.io/sagikazarmark/iximiuz-labs/playgrounds/kubernetes-multi-runtime/worker-containerd:live@sha256:72cc296fb5d22e32148fa2ef8e9e20af297deefb365f230ddc8712ff109ecf35
- eth0
- /
- oci://ghcr.io/sagikazarmark/iximiuz-labs/playgrounds/kubernetes-multi-runtime/worker-cri-o:live@sha256:9ba4206e77d6161c5861a3139266d582735c6af60aae9bfd38d5db09df26fd66
- eth0
- /
- oci://ghcr.io/sagikazarmark/iximiuz-labs/playgrounds/kubernetes-multi-runtime/worker-docker:live@sha256:ca8cbf18e887f69406c717132dd57d57fb0043e7ed203f4f3f2338484316d8e3
๐ Overview
This playground provides a multi-node Kubernetes cluster with different container runtimes and OCI runtime implementations:
- 1 Control Plane Node:
control-plane - 3 Worker Nodes: Each configured with a different container runtime:
worker-containerd: containerd with multiple OCI runtimesworker-cri-o: CRI-O with runc and crun supportworker-docker: Docker runtime through cri-dockerd
- 1 Development Machine:
dev-machine(pre-configured with kubectl access)
๐ง Runtime Compatibility Matrix
runc | crun | runsc | |
|---|---|---|---|
| containerd | โ | โ | โ |
| CRI-O | โ | โ | โ |
| Docker | โ | โ | โ |
๐ ๏ธ Tools
kubectl(alias:k): Kubernetes cluster management and debuggingnerdctl: Docker-compatible CLI forcontainerdkrew:kubectlplugin manager for extending functionality
Why Different OCI Runtimes Matter
Different OCI (Open Container Initiative) runtimes provide varying levels of security, performance, and compatibility:
runc: The standard OCI runtime, providing good performance and compatibilitycrun: A faster, more memory-efficient runtime written in Crunsc(gVisor): Provides enhanced security through application kernel isolation
Using different OCI runtimes allows you to optimize workloads based on specific requirements - performance-critical applications might use crun, while security-sensitive workloads benefit from runsc.
RuntimeClasses in Kubernetes
Kubernetes supports multiple OCI runtimes through RuntimeClasses, which provide a mechanism to select different runtime implementations for your pods. Each container runtime (containerd, CRI-O, Docker) can support multiple OCI runtimes.
By default, each container runtime uses its configured default OCI runtime, but you can override this by specifying the runtimeClassName field in your Pod specification.
Checking Available Runtime Handlers
Each node registers its available runtime handlers in the node status. Let's examine what's available:
# Check runtime handlers available on each node
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{range .status.nodeInfo.containerRuntimeVersion}{.}{"\n"}{end}{"\n"}{end}'
# More detailed view of runtime handlers per node
for node in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
echo "Node: $node"
kubectl get node $node -o jsonpath='{.status.nodeInfo.containerRuntimeVersion}{"\n"}'
echo "---"
done
๐ฏ Runtime Examples and Scheduling
Example 1: Universal Runtime (runc)
First, let's create a RuntimeClass for runc and demonstrate that it works across all nodes:
# Create RuntimeClass for runc
cat <<EOF | kubectl apply -f -
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: runc
handler: runc
EOF
# Deploy podinfo with runc across multiple nodes
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo-runc
spec:
replicas: 3
selector:
matchLabels:
app: podinfo-runc
template:
metadata:
labels:
app: podinfo-runc
spec:
runtimeClassName: runc
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- podinfo-runc
topologyKey: kubernetes.io/hostname
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo
ports:
- containerPort: 9898
EOF
# Verify pods are scheduled across different nodes
kubectl get pods -l app=podinfo-runc -o wide
Example 2: Heterogeneous Node Configuration with Labels
RuntimeClasses assume homogeneous node configuration by default, but you can support heterogeneous configurations using node scheduling.
Let's use node labels to target nodes that support crun:
# Label nodes that support crun runtime
kubectl label node worker-containerd crun-support=enabled
kubectl label node worker-cri-o crun-support=enabled
# Create RuntimeClass for crun
cat <<EOF | kubectl apply -f -
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: crun
handler: crun
scheduling:
nodeClassMap:
crun: crun-support=enabled
EOF
# Deploy podinfo with crun using node affinity
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo-crun
spec:
replicas: 2
selector:
matchLabels:
app: podinfo-crun
template:
metadata:
labels:
app: podinfo-crun
spec:
runtimeClassName: crun
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: crun-support
operator: In
values:
- enabled
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- podinfo-crun
topologyKey: kubernetes.io/hostname
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo
ports:
- containerPort: 9898
EOF
# Verify pods are only scheduled on nodes with crun support
kubectl get pods -l app=podinfo-crun -o wide
Example 3: Security Isolation with Taints and Tolerations
For security-sensitive workloads using runsc (gVisor), we can use taints and tolerations to ensure exclusive scheduling:
# Taint the containerd node for runsc workloads
kubectl taint node worker-containerd runsc=enabled:NoSchedule
# Create RuntimeClass for runsc with toleration
cat <<EOF | kubectl apply -f -
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: runsc
handler: runsc
scheduling:
tolerations:
- key: runsc
operator: Equal
value: enabled
effect: NoSchedule
nodeClassMap:
runsc: kubernetes.io/hostname=worker-containerd
EOF
# Deploy security-sensitive application with runsc
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo-secure
spec:
replicas: 2
selector:
matchLabels:
app: podinfo-secure
template:
metadata:
labels:
app: podinfo-secure
spec:
runtimeClassName: runsc
nodeSelector:
kubernetes.io/hostname: worker-containerd
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo
ports:
- containerPort: 9898
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
EOF
# All pods will be scheduled on the containerd node due to runsc requirement
kubectl get pods -l app=podinfo-secure -o wide
๐ Runtime Scheduling Rules
When deploying pods with specific RuntimeClasses, ensure proper node targeting based on the compatibility matrix:
โ Recommended Node Selectors:
runtimeClassName: runcโ Any worker node (all support runc)runtimeClassName: crunโ Onlyworker-containerdorworker-cri-onodesruntimeClassName: runscโ Onlyworker-containerdnode
โ Invalid Combinations to Avoid:
runscruntime onworker-cri-oorworker-docker(not supported)crunruntime onworker-docker(not supported)
๐ก Node Selector Examples:
# For runc (works everywhere)
nodeSelector:
kubernetes.io/os: linux
# For crun (containerd or CRI-O only)
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: runtime
operator: In
values: ["containerd", "cri-o"]
# For runsc (containerd only)
nodeSelector:
runtime: containerd
๐ฏ Getting Started
The cluster is automatically provisioned and ready to use.
Start by verifying the cluster status:
# Check cluster nodes and their container runtime
kubectl get nodes -o wide
# View cluster information
kubectl cluster-info
# Verify node labels for runtime scheduling
kubectl get nodes --show-labels | grep runtime
# Check which runtimes are available on each node
kubectl describe nodes | grep -A 5 -B 5 "Container Runtime"
Create RuntimeClasses
RuntimeClasses allow you to select different OCI runtimes for your pods. First, let's create the RuntimeClass definitions:
# Create RuntimeClass for runc (default)
cat <<EOF | kubectl apply -f -
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: runc
handler: runc
EOF
# Create RuntimeClass for crun
cat <<EOF | kubectl apply -f -
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: crun
handler: crun
EOF
# Create RuntimeClass for runsc (gVisor)
cat <<EOF | kubectl apply -f -
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: runsc
handler: runsc
EOF
# List available RuntimeClasses
kubectl get runtimeclasses
# Describe a specific RuntimeClass
kubectl describe runtimeclass runc
kubectl describe runtimeclass crun
kubectl describe runtimeclass runsc
# Verify node capabilities before deploying workloads
kubectl get nodes -l runtime=containerd -o name # Should show worker-containerd
kubectl get nodes -l runtime=cri-o -o name # Should show worker-cri-o
kubectl get nodes -l runtime=docker -o name # Should show worker-docker
๐งช Testing Different Runtimes
Deploy Test Applications with Different Runtimes
Default Runtime (runc via containerd)
# Create a simple deployment with default runtime
kubectl create deployment podinfo-default --image=ghcr.io/stefanprodan/podinfo --port=9898
# Check which node and runtime it's using
kubectl get pods -o wide
kubectl describe pod $(kubectl get pods -l app=podinfo-default -o jsonpath='{.items[0].metadata.name}')
Using crun Runtime
# Create deployment with crun runtime (compatible with containerd and CRI-O)
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo-crun
spec:
replicas: 1
selector:
matchLabels:
app: podinfo-crun
template:
metadata:
labels:
app: podinfo-crun
spec:
runtimeClassName: crun
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: runtime
operator: In
values: ["containerd", "cri-o"] # crun works with containerd and CRI-O, but NOT Docker
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo
ports:
- containerPort: 9898
EOF
# Verify the pod is using crun
kubectl get pods -l app=podinfo-crun -o wide
kubectl describe pod $(kubectl get pods -l app=podinfo-crun -o jsonpath='{.items[0].metadata.name}')
Using runsc (gVisor) Runtime
# Create deployment with runsc runtime for enhanced security (only compatible with containerd)
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo-runsc
spec:
replicas: 1
selector:
matchLabels:
app: podinfo-runsc
template:
metadata:
labels:
app: podinfo-runsc
spec:
runtimeClassName: runsc
nodeSelector:
runtime: containerd # runsc only works with containerd, NOT with CRI-O or Docker
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo
ports:
- containerPort: 9898
EOF
# Verify the pod is using runsc
kubectl get pods -l app=podinfo-runsc -o wide
kubectl describe pod $(kubectl get pods -l app=podinfo-runsc -o jsonpath='{.items[0].metadata.name}')
CRI-O with crun
# Create deployment targeting CRI-O node with crun
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo-crio-crun
spec:
replicas: 1
selector:
matchLabels:
app: podinfo-crio-crun
template:
metadata:
labels:
app: podinfo-crio-crun
spec:
runtimeClassName: crun
nodeSelector:
runtime: cri-o # Ensure it runs on CRI-O node
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo
ports:
- containerPort: 9898
EOF
Docker Runtime (via cri-dockerd)
# Create deployment on Docker node (using cri-dockerd with runc only)
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo-docker
spec:
replicas: 1
selector:
matchLabels:
app: podinfo-docker
template:
metadata:
labels:
app: podinfo-docker
spec:
runtimeClassName: runc # Docker only supports runc, not crun or runsc
nodeSelector:
runtime: docker # Ensure it runs on cri-dockerd node
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo
ports:
- containerPort: 9898
EOF
Expose and Test Applications
# Expose all deployments
kubectl expose deployment podinfo-default --port=9898 --name=podinfo-default-svc
kubectl expose deployment podinfo-crun --port=9898 --name=podinfo-crun-svc
kubectl expose deployment podinfo-runsc --port=9898 --name=podinfo-runsc-svc
kubectl expose deployment podinfo-crio-crun --port=9898 --name=podinfo-crio-crun-svc
kubectl expose deployment podinfo-docker --port=9898 --name=podinfo-docker-svc
# Wait for all pods to be ready
kubectl wait --for=condition=Ready pods --all --timeout=300s
# Test each application
kubectl get pods,services -o wide
# Test default runtime
curl http://$(kubectl get svc podinfo-default-svc -o jsonpath='{.spec.clusterIP}:{.spec.ports[0].port}')
# Test crun runtime
curl http://$(kubectl get svc podinfo-crun-svc -o jsonpath='{.spec.clusterIP}:{.spec.ports[0].port}')
# Test runsc runtime (gVisor)
curl http://$(kubectl get svc podinfo-runsc-svc -o jsonpath='{.spec.clusterIP}:{.spec.ports[0].port}')
# Test CRI-O with crun
curl http://$(kubectl get svc podinfo-crio-crun-svc -o jsonpath='{.spec.clusterIP}:{.spec.ports[0].port}')
# Test Docker runtime (via cri-dockerd)
curl http://$(kubectl get svc podinfo-docker-svc -o jsonpath='{.spec.clusterIP}:{.spec.ports[0].port}')
๐ Runtime Inspection and Debugging
Check Container Runtime on Each Node
# Check runtime information on each worker node
kubectl get nodes -o custom-columns=NAME:.metadata.name,RUNTIME:.status.nodeInfo.containerRuntimeVersion
# Get detailed node information
kubectl describe node worker-containerd
kubectl describe node worker-cri-o
kubectl describe node worker-docker
Inspect Running Containers
On the containerd node:
# Switch to worker-containerd node terminal
# List containers with nerdctl
nerdctl --namespace k8s.io ps
# List containers with ctr
ctr --namespace k8s.io containers ls
# Check which runtime a container is using
ctr --namespace k8s.io info
On the CRI-O node:
# Switch to worker-cri-o node terminal
# List containers with crictl
crictl ps
# Get container runtime info
crictl info
On the Docker node (via cri-dockerd):
# Switch to worker-docker node terminal
# List containers with docker
docker ps
# Get docker system info
docker system info
# Check cri-dockerd status
systemctl status cri-docker
# Check cri-dockerd configuration
cat /etc/systemd/system/cri-docker.service
Performance and Security Testing
Test gVisor Security Isolation
# Create a test pod with runsc to verify gVisor isolation (only works on containerd)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: gvisor-test
spec:
runtimeClassName: runsc
nodeSelector:
runtime: containerd # runsc only supported on containerd nodes
containers:
- name: test
image: busybox
command: ["sleep", "3600"]
EOF
# Exec into the pod and try to access host information
kubectl exec -it gvisor-test -- dmesg
kubectl exec -it gvisor-test -- cat /proc/version
kubectl exec -it gvisor-test -- ps aux
Compare Runtime Performance
# Create identical workloads with different runtimes (all on containerd for fair comparison)
for runtime in runc crun runsc; do
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: perf-test-${runtime}
spec:
template:
metadata:
labels:
runtime-test: ${runtime}
spec:
runtimeClassName: ${runtime}
nodeSelector:
runtime: containerd # Use containerd node for all tests since it supports all runtimes
restartPolicy: Never
containers:
- name: perf-test
image: busybox
command: ["sh", "-c", "time dd if=/dev/zero of=/tmp/test bs=1M count=100; echo 'Runtime: ${runtime} completed'"]
EOF
done
# Monitor job completion times
kubectl get jobs -w
kubectl logs job/perf-test-runc
kubectl logs job/perf-test-crun
kubectl logs job/perf-test-runsc
๐งฉ RuntimeClass Configuration Examples
Custom RuntimeClass Definition
# Example: Create a custom RuntimeClass for debugging
cat <<EOF | kubectl apply -f -
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: debug-runtime
handler: runc
overhead:
podFixed:
cpu: 10m
memory: 10Mi
scheduling:
nodeClassForScheduling: runtime-debug
tolerations:
- effect: NoSchedule
key: runtime
value: debug
EOF
๐ Monitoring and Observability
Check Resource Usage by Runtime
# Get resource usage across different runtimes
kubectl top pods --all-namespaces
# Get detailed metrics for runtime comparison
kubectl get pods -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName,RUNTIME:.spec.runtimeClassName,STATUS:.status.phase
# Monitor pod distribution across runtimes
kubectl get pods --all-namespaces -o wide | grep -E "worker-(containerd|cri-o|docker)"
๐จ Troubleshooting Runtime Compatibility
Common Issues and Solutions
Pod Stuck in Pending State
If a pod remains in Pending state, check for runtime compatibility issues:
# Check pod events for scheduling errors
kubectl describe pod <pod-name>
# Look for events like:
# "0/4 nodes are available: 1 node(s) didn't match Pod's node affinity/selector"
# "RuntimeClass not supported by node"
Common causes:
- Using
runscRuntimeClass on CRI-O or Docker nodes - Using
crunRuntimeClass on Docker nodes - Incorrect node selectors
RuntimeClass Handler Not Found
# Check if RuntimeClass exists
kubectl get runtimeclasses
# Verify handler is supported on target nodes
kubectl describe node <node-name> | grep "Container Runtime"
Pod Fails to Start with Runtime Errors
# Check container logs for runtime-specific errors
kubectl logs <pod-name>
# Check node logs for runtime issues
kubectl describe node <node-name>
# On the node itself, check runtime status:
# For containerd nodes:
systemctl status containerd
# For CRI-O nodes:
systemctl status crio
# For Docker nodes:
systemctl status docker
systemctl status cri-docker
Verification Commands
# Test runtime compatibility before deploying
kubectl run test-runc --image=busybox --rm -it --restart=Never \
--overrides='{"spec":{"runtimeClassName":"runc","nodeSelector":{"runtime":"containerd"}}}' \
-- echo "runc on containerd works"
kubectl run test-crun --image=busybox --rm -it --restart=Never \
--overrides='{"spec":{"runtimeClassName":"crun","nodeSelector":{"runtime":"containerd"}}}' \
-- echo "crun on containerd works"
kubectl run test-runsc --image=busybox --rm -it --restart=Never \
--overrides='{"spec":{"runtimeClassName":"runsc","nodeSelector":{"runtime":"containerd"}}}' \
-- echo "runsc on containerd works"
๐งน Cleanup
# Clean up test deployments
kubectl delete deployment podinfo-default podinfo-crun podinfo-runsc podinfo-crio-crun podinfo-docker
# Clean up services
kubectl delete service podinfo-default-svc podinfo-crun-svc podinfo-runsc-svc podinfo-crio-crun-svc podinfo-docker-svc
# Clean up test pods and jobs
kubectl delete pod gvisor-test
kubectl delete job perf-test-runc perf-test-crun perf-test-runsc
๐ Learn More
- Kubernetes RuntimeClass Documentation
- containerd Documentation
- CRI-O Documentation
- gVisor Documentation
- runc Documentation
- crun Documentation
- cri-dockerd Documentation
๐งฉ Related Content
๐งช Playgrounds
- Kubernetes Playgrounds (official)
- containerd
- CRI-O
- Kubernetes Cluster (official)
๐ Courses
๐ Articles
๐ Challenges
- Install and Configure containerd on a Linux Host (official)
- Working With containerd Namespaces (official)
Happy learning! ๐
A real VM, not a container
Get root on a VM with its own kernel, so Docker, Kubernetes, and systemd just work.
Read the docs โ
SSH from browser or CLI
Use the built-in web terminal, or connect with labctl ssh, plain ssh, scp, or rsync.
Read the docs โ
Drive it with AI
Let Claude, Codex, or any MCP client start this playground and run commands in it.
Read the docs โ
Expose HTTP(S) ports
Give any web app running inside the VM a public URL - for yourself or to share with others.
Read the docs โ
Share terminals
Invite others into your terminal session, or open it yourself from another device.
Read the docs โ
Customize with init scripts
Add shell scripts that run at boot to install packages, clone repos, or seed test data.
Read the docs โ
Private networking
VMs reach the internet via NAT with no public IP; several VMs share bridge networks.
Read the docs โ
Ephemeral or persistent
By default, VMs are destroyed when the session ends; enable persistence to keep the disks for next time.
Read the docs โ
Built-in container registry
A private registry.iximiuz.com for every playground to share images across services and VMs.
Read the docs โ