Home About

Published

- 8 min read

Kubernetes - How to Replace Docker Runtime with Containerd

kubernetes
img of Kubernetes - How to Replace Docker Runtime with Containerd

If you have not already seen the blog post by Kubernetes, they are removing dockershim in the upcoming v1.24 release

Why is Kubernetes moving away from dockershim?

Docker was the first container runtime used by Kubernetes. This is one of the reasons why Docker is so familiar to many Kubernetes users and enthusiasts. Docker support was hardcoded into Kubernetes – a component the project refers to as dockershim. As containerization became an industry standard, the Kubernetes project added support for additional runtimes. This culminated in the implementation of the container runtime interface (CRI), letting system components (like the kubelet) talk to container runtimes in a standardized way. As a result, dockershim became an anomaly in the Kubernetes project. Dependencies on Docker and dockershim have crept into various tools and projects in the CNCF ecosystem, resulting in fragile code.

https://kubernetes.io/blog/2022/01/07/kubernetes-is-moving-on-from-dockershim/

How to Replace Docker with Containerd

Here I will show how I’ve replaced Docker runtime with containerd in my K8s cluster.

Pre-Checks

First begin by confirming the container runtime which your K8s cluster is running on. In this example, you can see that all 3 nodes are using “docker://20.10.12” under the “CONTAINER-RUNTIME” column.

   alex@masternode:~$ kubectl get nodes -o wide
NAME          STATUS   ROLES                  AGE     VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
masternode    Ready    control-plane,master   3d16h   v1.22.4   192.168.0.10   <none>        Ubuntu 20.04.3 LTS   5.4.0-91-generic   docker://20.10.12
workernode1   Ready    <none>                 3d16h   v1.22.4   192.168.0.11   <none>        Ubuntu 20.04.3 LTS   5.4.0-91-generic   docker://20.10.12
workernode2   Ready    <none>                 4m20s   v1.22.4   192.168.0.13   <none>        Ubuntu 20.04.3 LTS   5.4.0-94-generic   docker://20.10.12

Begin with first worker node

On a machine with kubectl, cordon and drain the first worker node.

   alex@masternode:~$ kubectl cordon workernode1
node/workernode1 cordoned

alex@masternode:~$ kubectl drain workernode1 --ignore-daemonsets --force
node/workernode1 already cordoned
WARNING: deleting Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet: default/tools; ignoring DaemonSet-managed Pods: ingress/ingress-ingress-nginx-controller-4cxzc, kube-system/kube-proxy-qrmfk, kube-system/weave-net-4nxzm
evicting pod dev/wordpress-69ddf5f8c7-r8xn9
evicting pod default/tools
evicting pod default/nfs-subdir-external-provisioner-77cd49877f-dkdwk
evicting pod dev/mysql-wordpress-db69dd79-b7bhq
evicting pod dev/wordpress-69ddf5f8c7-lz4tt
pod/nfs-subdir-external-provisioner-77cd49877f-dkdwk evicted
pod/wordpress-69ddf5f8c7-r8xn9 evicted
pod/wordpress-69ddf5f8c7-lz4tt evicted
pod/mysql-wordpress-db69dd79-b7bhq evicted
pod/tools evicted
node/workernode1 evicted

Confirm pods are now rescheduled to another node. Here you can see that all the pods are now running on workernode2.

   alex@masternode:~$ kubectl get pods -n dev -o wide
NAME                             READY   STATUS    RESTARTS   AGE    IP            NODE          NOMINATED NODE   READINESS GATES
mysql-wordpress-db69dd79-8tbkx   1/1     Running   0          80s    172.16.96.3   workernode2   <none>           <none>
wordpress-69ddf5f8c7-26c5x       1/1     Running   0          80s    172.16.96.4   workernode2   <none>           <none>
wordpress-69ddf5f8c7-stt7b       1/1     Running   0          5m2s   172.16.96.2   workernode2   <none>           <none>
wordpress-69ddf5f8c7-tx58w       1/1     Running   0          80s    172.16.96.6   workernode2   <none>           <none>
wordpress-69ddf5f8c7-vxdw7       1/1     Running   0          5m2s   172.16.96.1   workernode2   <none>           <none>

Note: The tasks below will be executed directly on the worker node itself.

Stop and disable the kubelet service on the worker node.

   alex@workernode1:~$ sudo systemctl disable kubelet && sudo systemctl stop kubelet
Removed /etc/systemd/system/multi-user.target.wants/kubelet.service.

alex@workernode1:~$ sudo systemctl status kubelet.service | head -5
● kubelet.service - kubelet: The Kubernetes Node Agent
     Loaded: loaded (/lib/systemd/system/kubelet.service; disabled; vendor preset: enabled)
    Drop-In: /etc/systemd/system/kubelet.service.d
             └─10-kubeadm.conf
     Active: inactive (dead)

Remove all existing docker packages (I am also purging all configs to be clean)

   alex@workernode1:~$ sudo apt-get remove --purge containerd.io docker-ce docker-ce-cli
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
  docker-ce-rootless-extras docker-scan-plugin pigz slirp4netns
Use 'sudo apt autoremove' to remove them.
The following packages will be REMOVED:
  containerd.io* docker-ce* docker-ce-cli*
0 upgraded, 0 newly installed, 3 to remove and 44 not upgraded.
After this operation, 375 MB disk space will be freed.
Do you want to continue? [Y/n] y
(Reading database ... 114615 files and directories currently installed.)
Removing docker-ce (5:20.10.12~3-0~ubuntu-focal) ...
Warning: Stopping docker.service, but it can still be activated by:
  docker.socket
Removing containerd.io (1.4.12-1) ...
Removing docker-ce-cli (5:20.10.12~3-0~ubuntu-focal) ...
Processing triggers for man-db (2.9.1-1) ...
(Reading database ... 114397 files and directories currently installed.)
Purging configuration files for docker-ce (5:20.10.12~3-0~ubuntu-focal) ...
Purging configuration files for containerd.io (1.4.12-1) ...
Processing triggers for systemd (245.4-4ubuntu3.15) ...

Note: The following modules-load-d and systctl.d config is based on the official Kubernetes documentation for containerd. I already had the other configs when I configured Docker, so I only needed to append these additional configurations.

Note 2: Your filename may be different so please check before running these commands!.

   # Double check your filename!
echo "overlay" | sudo tee -a /etc/modules-load.d/k8s.conf
sudo modprobe overlay

# Double check your filename!
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.d/k8s.conf
sudo sysctl --system

Note: FYI, the next couple steps are referenced from the official containerd documentation.

Download the containerd tarball.

   alex@workernode1:~$ wget https://github.com/containerd/containerd/releases/download/v1.5.9/cri-containerd-cni-1.5.9-linux-amd64.tar.gz
--2022-01-17 12:09:17-- https://github.com/containerd/containerd/releases/download/v1.5.9/cri-containerd-cni-1.5.9-linux-amd64.tar.gz
Resolving github.com (github.com)... 13.237.44.5
Connecting to github.com (github.com)|13.237.44.5|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://objects.githubusercontent.com/github-production-release-asset-2e65be/46089560/e6ef531b-7944-43e0-bbd3-032590a4d8bb?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20220117%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220117T010753Z&X-Amz-Expires=300&X-Amz-Signature=137e650f59a0b171ee5d4eae8f906cf6ecee55572c2b93f48e330b03d604bc09&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=46089560&response-content-disposition=attachment%3B%20filename%3Dcri-containerd-cni-1.5.9-linux-amd64.tar.gz&response-content-type=application%2Foctet-stream [following]
--2022-01-17 12:09:17-- https://objects.githubusercontent.com/github-production-release-asset-2e65be/46089560/e6ef531b-7944-43e0-bbd3-032590a4d8bb?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20220117%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220117T010753Z&X-Amz-Expires=300&X-Amz-Signature=137e650f59a0b171ee5d4eae8f906cf6ecee55572c2b93f48e330b03d604bc09&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=46089560&response-content-disposition=attachment%3B%20filename%3Dcri-containerd-cni-1.5.9-linux-amd64.tar.gz&response-content-type=application%2Foctet-stream
Resolving objects.githubusercontent.com (objects.githubusercontent.com)... 185.199.108.133, 185.199.111.133, 185.199.110.133, ...
Connecting to objects.githubusercontent.com (objects.githubusercontent.com)|185.199.108.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 127092433 (121M) [application/octet-stream]
Saving to: ‘cri-containerd-cni-1.5.9-linux-amd64.tar.gz’

cri-containerd-cni-1.5.9-linux-amd64.tar.gz           100%[========================================================================================================================>] 121.20M  36.8MB/s    in 4.6s

2022-01-17 12:09:22 (26.2 MB/s) - ‘cri-containerd-cni-1.5.9-linux-amd64.tar.gz’ saved [127092433/127092433]

Extract the tarball.

   alex@workernode1:~$ sudo tar --no-overwrite-dir -C / -xzf cri-containerd-cni-1.5.9-linux-amd64.tar.gz

If you don’t already have an existing /etc/containerd/config.toml file, then generate one by running:

   alex@workernode1:~$ sudo mkdir /etc/containerd
alex@workernode1:~$ sudo containerd config default | sudo tee /etc/containerd/config.toml

# Note: If you already have an existing file then make sure disabled_plugins is not disabling CRI
# e.g. disabled_plugins = [""]

Delete the extracted CNI config that came with the tarball (this can interfere with an existing CNI)

   alex@workernode1:~$ sudo rm -f /etc/cni/net.d/10-containerd-net.conflist

Add the following args to the kubelet service.

   --container-runtime=remote --container-runtime-endpoint=/run/containerd/containerd.sock

Example:

   # BEFORE
alex@workernode1:~$ sudo cat /var/lib/kubelet/kubeadm-flags.env
KUBELET_KUBEADM_ARGS="--network-plugin=cni --pod-infra-container-image=k8s.gcr.io/pause:3.5"

alex@workernode1:~$ sudo vi /var/lib/kubelet/kubeadm-flags.env

# AFTER
alex@workernode1:~$ cat /var/lib/kubelet/kubeadm-flags.env
KUBELET_KUBEADM_ARGS="--network-plugin=cni --pod-infra-container-image=k8s.gcr.io/pause:3.5 --container-runtime=remote --container-runtime-endpoint=/run/containerd/containerd.sock"

Start and enable containerd service.

   alex@workernode1:~$ sudo systemctl daemon-reload && sudo systemctl enable containerd && sudo systemctl start containerd

alex@workernode1:~$ sudo systemctl status containerd.service | head -5
● containerd.service - containerd container runtime
     Loaded: loaded (/etc/systemd/system/containerd.service; disabled; vendor preset: enabled)
     Active: active (running) since Mon 2022-01-17 12:10:40 AEDT; 51s ago
       Docs: https://containerd.io
    Process: 74410 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS)

Start and enable the kubelet service.

   alex@workernode1:~$ sudo systemctl enable kubelet && sudo systemctl restart kubelet
Created symlink /etc/systemd/system/multi-user.target.wants/kubelet.service → /lib/systemd/system/kubelet.service.

# Check the status of kubelet
alex@workernode1:~$ sudo systemctl status kubelet.service | head -5
● kubelet.service - kubelet: The Kubernetes Node Agent
     Loaded: loaded (/lib/systemd/system/kubelet.service; enabled; vendor preset: enabled)
    Drop-In: /etc/systemd/system/kubelet.service.d
             └─10-kubeadm.conf
     Active: active (running) since Mon 2022-01-17 12:14:13 AEDT; 4s ago

Note: Go back on the machine with kubectl for the next 3 tasks.

Update the worker node annotation so it knows to use containerd instead of docker.

   alex@masternode:~$ kubectl annotate node workernode1 --overwrite kubeadm.alpha.kubernetes.io/cri-socket=/var/run/containerd/containerd.sock
node/workernode1 annotated

Uncordon the worker node.

   alex@masternode:~$ kubectl uncordon workernode1
node/workernode1 uncordoned

Confirm that the node is now using containerd as the container runtime. Here you can mine is using “containerd://1.5.9”

   alex@masternode:~$ kubectl get nodes -o wide | grep workernode1
workernode1   Ready    <none>                 3d17h   v1.22.4   192.168.0.11   <none>        Ubuntu 20.04.3 LTS   5.4.0-94-generic   containerd://1.5.9

Remaining Worker Nodes

For the remaining worker nodes, repeat the steps above for each node at a time.

Master Node

The master node is also very similar to a standard worker node. Here I will only show the commands which I’ve executed without the actual STDOUT.

   # Cordon the masternode
alex@masternode:~$ kubectl cordon masternode

# Drain the masternode
alex@masternode:~$ kubectl drain masternode --ignore-daemonsets

# Stop and disable kubelet service
alex@masternode:~$ sudo systemctl stop kubelet; sudo systemctl disable kubelet

# Confirm kubelet status
alex@masternode:~$ sudo systemctl status kubelet.service | head -5

# Uninstall Docker packages
alex@masternode:~$ sudo apt-get remove --purge containerd.io docker-ce docker-ce-cli

# Install prereq
alex@masternode:~$ sudo apt-get update && sudo apt-get install -y libseccomp2

# Download and extract containerd tarball.
alex@masternode:~$ wget https://github.com/containerd/containerd/releases/download/v1.5.9/cri-containerd-cni-1.5.9-linux-amd64.tar.gz
alex@masternode:~$ sudo tar --no-overwrite-dir -C / -xzf cri-containerd-cni-1.5.9-linux-amd64.tar.gz

# Generate default config for containerd
alex@masternode:~$ sudo mkdir /etc/containerd
alex@masternode:~$ sudo containerd config default | sudo tee /etc/containerd/config.toml

# Delete the default CNI that comes with the tarball
alex@masternode:~$ sudo rm -f /etc/cni/net.d/10-containerd-net.conflist

# Add the following args to KUBELET_KUBEADM_ARGS in /var/lib/kubelet/kubeadm-flags.env
# --container-runtime=remote --container-runtime-endpoint=/run/containerd/containerd.sock
alex@masternode:~$ sudo vi /var/lib/kubelet/kubeadm-flags.env
alex@masternode:~$ cat /var/lib/kubelet/kubeadm-flags.env
KUBELET_KUBEADM_ARGS="--network-plugin=cni --pod-infra-container-image=k8s.gcr.io/pause:3.5 --container-runtime=remote --container-runtime-endpoint=/run/containerd/containerd.sock"

# Start and enable containerd service
alex@masternode:~$ sudo systemctl daemon-reload && sudo systemctl enable containerd && sudo systemctl start containerd

# Confirm containerd is started
alex@masternode:~$ sudo systemctl status containerd.service | head -5

# Start and enable kubelet service
alex@masternode:~$ sudo systemctl enable kubelet && sudo systemctl restart kubelet
Created symlink /etc/systemd/system/multi-user.target.wants/kubelet.service → /lib/systemd/system/kubelet.service.

# Check the kubelet service
alex@masternode:~$ sudo systemctl status kubelet.service | head -5

# Use crictl to check and wait until all the static pods are running
alex@masternode:~$ sudo crictl ps
CONTAINER           IMAGE               CREATED             STATE               NAME                      ATTEMPT             POD ID
d98403829fa6c       7f92d556d4ffe       22 seconds ago      Running             weave-npc                 2                   c3f3bf10c313b
7a95100ebdc74       df29c0a4002c0       27 seconds ago      Running             weave                     3                   c3f3bf10c313b
868fe4a138238       8f8fdd6672d48       35 seconds ago      Running             kube-proxy                2                   1557e3a21b0c4
47a8a203c98a3       0048118155842       46 seconds ago      Running             etcd                      2                   b73ad0aa19901
eb1985e59d015       059e6cd8cf78e       52 seconds ago      Running             kube-apiserver            2                   51009f2162ea4
6f086b353506d       935d8fdc2d521       56 seconds ago      Running             kube-scheduler            2                   ccfce96435ba1
4eadd77a4590d       04185bc88e08d       58 seconds ago      Running             kube-controller-manager   2                   6c46620c55863

# Update the annotation on the master node
alex@masternode:~$ kubectl annotate node masternode --overwrite kubeadm.alpha.kubernetes.io/cri-socket=/var/run/containerd/containerd.sock

# Uncordon the master node
alex@masternode:~$ kubectl uncordon masternode

Final Check

Use the “kubectl get nodes” command to ensure all nodes are now using containerd as the container runtime.

   alex@masternode:~$ kubectl get nodes -o wide
NAME          STATUS   ROLES                  AGE     VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
masternode    Ready    control-plane,master   3d17h   v1.22.4   192.168.0.10   <none>        Ubuntu 20.04.3 LTS   5.4.0-91-generic   containerd://1.5.9
workernode1   Ready    <none>                 3d17h   v1.22.4   192.168.0.11   <none>        Ubuntu 20.04.3 LTS   5.4.0-94-generic   containerd://1.5.9
workernode2   Ready    <none>                 67m     v1.22.4   192.168.0.13   <none>        Ubuntu 20.04.3 LTS   5.4.0-94-generic   containerd://1.5.9

Conclusion

As everything I’ve built for my blog here is based on IaC, I have also updated the Ansible playbook to install and configure containerd instead of using Docker.

I hope you find this post useful and as always, please leave a comment below if you come across any issues.