CRI と Cgroup Driver をダウンタイム無しでまとめて変更する

執筆者 : 山下雅喜


はじめに

Kubernetes v1.20 のリリースノート において、CRI として Docker (dockershim) の利用が非推奨となり、v1.23 では dockershim が Kubernetes から除去されるとの予告がされています。

また、Kubernetes v1.21 のリリースノート には、kubeadm の標準の Cgroup Driver が cgroupfs から systemd に変更となり、v1.22 では既存クラスタを kubeadm upgrade した際に何も指定しないと systemd に変更されてしまうとの記載もあります。

当社内には Docker & cgroupfs の構成の社内システム用のクラスタがあり、その構成のままでは近い将来に問題が生じるため、containerd & systemd の構成に切り替える方法を検証してみました。

検証した環境

今回検証した環境のソフトウェアおよびバージョンは次の通りです。

  • OS : Ubuntu 20.04.2 LTS
  • Linux Kernel : 5.4.0-1043-aws
  • Kubernetes : 1.20.5
  • CRI
    • 変更前 : Docker CE 20.10.6 (Docker 公式の apt リポジトリから入手したもの)
    • 変更後 : containerd 1.4.4 (Docker 公式の apt リポジトリから入手したもの)
  • Cgroup Driver
    • 変更前 : cgroupfs
    • 変更後 : systemd

本クラスタは kubeadm を用いて構築し、コントロールプレーンノードは3台で構成しました。

CRI と Cgroup Driver の変更手順

CRI と Cgroup Driver はノードごとに異なっていても良いため、ノードごとに変更を行う想定で、大まかな手順は次のようになります。

  1. kubelet のデフォルト設定の Cgroup Driver を systemd に変更する
  2. ノードを drain する
  3. kubelet, Docker, containerd を停止する
  4. containerd を CRI 用にセットアップする
  5. kubelet に containerd を利用させる
  6. Pod ログディレクトリを退避させる
  7. Cgroup Driver を systemd に変更する
  8. containerd, kubelet を起動する
  9. ノードを uncordon する

1 を実行した後、ノードごとに 2~9 を実行するという流れです。 1,2,9 は kubectl の操作が可能なマシン上で行い、3~8 は変更対象のノード上で行います。

1. kubelet のデフォルト設定の Cgroup Driver を systemd に変更する

kubectl を用いて、kubelet のデフォルト設定 (名前空間 'kube-system' の ConfigMap 'kubelet-config-1.20') で、Cgroup Driver を systemd に変更します。 ConfigMap の名前は Kubernetes のバージョンによって異なるため、環境に合わせて適宜変更してください。

kubectl edit cm -n kube-system kubelet-config-1.20
=====
キー 'kubelet''cgroupDriver' の値を 'systemd' に変更する
=====

この ConfigMap は、kubeadm を用いてノードを初期セットアップ・アップグレードする際に、各ノードに kubelet 設定として配置されます。 既にセットアップ済のノードの kubelet 設定は変更されないため、後続の手順の中で変更が必要となります。

2. ノードを drain する

kubectl を用いて、CRI と Cgroup Driver の変更を行うノードを drain します。

kubectl drain ${ノード名} --ignore-daemonsets

必要なパラメータはノード上で実行しているワークロードによって変わってくるため、適宜変更してください。

3. kubelet, Docker, containerd を停止する

kubelet, Docker, containerd の順にサービスを停止します。

sudo systemctl stop kubelet.service
sudo systemctl stop docker.service
sudo systemctl stop docker.socket
sudo systemctl stop containerd.service

Docker サービスを停止した時点で、ノード上の全てのコンテナが停止されます。

4. containerd を CRI 用にセットアップする

本クラスタでは Docker 公式の apt リポジトリの docker-ce をインストールしているため、同リポジトリの containerd.io も既にインストールされています。 そのため、公式ドキュメント Container runtimes を参考に、containerd を CRI として動作させるために必要なセットアップのみ行います。

## モジュールのロード
cat <<EOF | sudo tee /etc/modules-load.d/containerd.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter


## カーネルパラメータの設定
cat <<EOF | sudo tee /etc/sysctl.d/99-kubernetes-cri.conf
net.bridge.bridge-nf-call-iptables  = 1
net.ipv4.ip_forward                 = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF

sudo sysctl --system


## CRI 用の設定の生成
containerd config default | sudo tee /etc/containerd/config.toml

Docker を利用していると上記のモジュールやカーネルパラメータは自動的に設定されますが、Docker を利用しなくなるので忘れずに設定する必要があります。

5. kubelet に containerd を利用させる

CRI として containerd を利用するよう、kubelet のパラメータを追加します。 必要なパラメータは次の2つで、このパラメータは kubelet の起動パラメータとして与える必要があります。

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

kubeadm を用いて構築したノードでは、/var/lib/kubelet/kubeadm-flags.env ファイルにパラメータを追加し、環境変数として kubelet に与えると良いでしょう。

sudo vim /var/lib/kubelet/kubeadm-flags.env
=====
KUBELET_KUBEADM_ARGS 変数に上記の2つのパラメータを追加する
=====

なお、kubelet は CRI の自動検出を行いません。 kubeadm init または join を用いてノードの初期セットアップを行う際は、kubeadm が CRI の自動検出を行います。

6. Pod ログディレクトリを退避させる

CRI として Docker を標準設定で利用している場合、Pod やコンテナのログは /var/lib/docker/containers/コンテナID ディレクトリ以下に出力され、ログファイルに対するシンボリックリンクが /var/log/pods ディレクトリ以下に作成されます。 一方、CRI として containerd を利用する場合、コンテナのログは /var/log/pods ディレクトリ以下に出力されます。

この違いに対応するため、/var/log/pods ディレクトリを退避させておく必要があります。

sudo mv /var/log/pods /var/log/pods.docker

/var/log/pods ディレクトリを退避させなかった場合、次のような問題が発生します。

  • ログが /var/lib/docker/containers/コンテナID ディレクトリ以下のファイルに出力され続ける
  • kubectl logs コマンドで CRI 変更後のログを参照できなくなる

どちらも Static Pod や drain 時に evict されなかった Pod において発生し、コンテナの再実行や Pod の再作成を行うと解消します。

なお、ログメッセージ形式の違いによる課題が運用上発生する場合もあり、これについては後述します。

7. Cgroup Driver を systemd に変更する

containerd が Cgroup Driver として systemd を利用するように、設定ファイル /etc/containerd/config.toml を変更します。

sudo vim /etc/containerd/config.toml
======
'plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options' の下に、SystemdCgroup = true を追加する

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = true
=====

また、kubelet も Cgroup Driver として systemd を利用するように、設定ファイル /var/lib/kubelet/config.yaml を変更します。

sudo sed -i -e "/^cgroupDriver:/c cgroupDriver: systemd" /var/lib/kubelet/config.yaml

8. containerd, kubelet を起動する

containerd, kubelet の順にサービスを起動します。

sudo systemctl start containerd.service
sudo systemctl start kubelet.service

設定変更を問題なく行えていれば、上記サービスを起動すると Static Pod や drain 時に evict されていない Pod が起動してきます。

なお、Docker サービスは利用しないため起動していませんが、起動して利用しても問題ありません。

9. ノードを uncordon する

Static Pod や drain 時に evict されていない Pod が起動したことが確認できたら、kubectl を用いてノードを uncordon します。

kubectl uncordon ${ノード名}

ワークロードのリバランスが必要であれば、適宜実行してください。

その他事項

コンテナログのメッセージ形式の違いの考慮

ノード上に保存されるコンテナのログのメッセージ形式は CRI によって異なり、Docker および containerd の標準のログ形式では次のようになっています。

ノード上に保存されるコンテナのログファイルを読み取ってログバックエンドで収集している場合、読み取る際のパスには変わりはないものの、メッセージ形式の違いに対応する必要が出てきます。

基本的にはログメッセージのパースを行う箇所で対応することになりますが、DaemonSet で各ノードに配置したエージェントでパースしている場合、バックエンドで収集するログに欠損が生じないようにするには少々工夫が必要です。 工夫の例として、各メッセージ形式に応じたパースを行う異なる DaemonSet を用意し、ノードに CRI を表すラベルを付加し、そのラベルの値に応じてノードでどちらの DaemonSet を実行するかを切り替える方法が楽そうでした。

ごみ掃除

Docker を停止する直前まで動いていた Pod のコンテナは、停止状態ではあるものの Docker や containerd (名前空間 moby) 上に残り続けています。 そのままにしておいても害はないと思いますが、不要なので削除してしまうと良いでしょう。

また、Docker 上に保存されたイメージのキャッシュももはや不要なので、削除して構いません。

まとめ

CRI や Cgroup Driver の変更は、Kubernetes のバージョンアップと同様に、ノード単位で行えることが確認できました。 クラスタ内で異なる CRI, Cgroup Driver を利用するノードが共存していても、クラスタは正常に動作することも確認できました。

ノードごとに変更作業を行えるため、コントロールプレーンノードを3台以上で構成し、ワークロードを複数レプリカで適切に実行していれば、サービスのダウンタイムは発生しないでしょう。

ただし、CRI を Docker から containerd に変更すると、ノード上に出力されるコンテナログのメッセージ形式が変わるため、ログを収集している場合には変更後の形式への対応が必要となります。