TT Lab
はじめる
学ぶ 学習パス コース

CKA — Kubernetes管理者

kubeadm がやってくれないこと — 準備、スキュー、インターフェース

TT Labで続きを見る

一言でいうと

kubeadmは、コントロールプレーンを組み立てるツールであって、ノードを準備するツールではありません。swap・IPフォワーディング・コンテナランタイム・cgroupドライバー・ポートは人が合わせておく必要があり、アップグレードは、バージョンスキューポリシーが定めた順序(kube-apiserverが先)に従う必要があり、CNI・CSI・CRIは、ネットワーク・ストレージ・ランタイムをコアから切り離して、交換可能にしたインターフェースです。

なぜ必要なのか

kubeadm initは、開始するとすぐにpreflight検査を実行します。kubeadmのインストールのドキュメントは、要件を次のように書いています。マシン1台あたり2GB以上のRAM、コントロールプレーンのマシンは2 CPU以上、すべてのマシン間の完全なネットワーク接続、ノードごとに固有のhostname・MACアドレス・product_uuid、そして特定のポートの開放です。バイナリはglibcに動的リンクされているため、Alpineのようにglibcがないディストリビューションでは互換レイヤーが必要で、kubeadmは、サポートするカーネルバージョンかどうかを、SystemVerification検査で確認します。

この検査が失敗すると、kubeadmは停止しますが、通過するように直してはくれません。そのため、準備項目を知らないと、「なぜ動かないのか」から始めて、検索で1つずつ埋めていくことになり、試験会場では、その時間がありません。アップグレードも同様です。kubeadmはスキューポリシーを強制しますが、どの順序で、どのノードを上げるべきかは、人が知っている必要があります。

どう動くのか

インストール前のインフラ準備

swap: kubeletのデフォルトの動作は、ノードでswapが検知されると、起動を拒否することです。そのため、swapoff -aで無効にし、再起動後も維持されるように、/etc/fstabやsystemd.swapの設定から外す必要があります。あえてswapを置くなら、kubeletの設定にfailSwapOn: falseを指定する必要があり、それでも、ワークロードはデフォルトのswapBehaviorであるNoSwapのため、swapを使いません。

カーネルのネットワーク設定: コンテナランタイムのドキュメントによると、Linuxカーネルは、デフォルトでは、インターフェース間のIPv4パケットのルーティングを許可しません。そのため、/etc/sysctl.d/k8s.confにnet.ipv4.ip_forward = 1を書き、sysctl --systemで適用します。ドキュメントは、ほとんどのネットワーク実装が、必要ならこの値を自分で変更するものの、一部は管理者に任せていると述べ、「ほかのsysctlの値やカーネルモジュールのロードを期待する実装もあるので、ネットワーク実装のドキュメントを見るように」と付け加えています。以前の手引きにあったoverlay・br_netfilterモジュールのロードと、bridge-nf-call系のsysctlは、現在の公式ページの必須リストにはなく、CNI実装が要求するかどうかによって決まります。試験や現場で、古い手順をそのまま暗記するよりも、今のドキュメントが要求するもの(ip_forward)と、実装が要求するものを区別しておくほうが、安全です。

コンテナランタイム: KubernetesはCRIでランタイムと対話します。ランタイムを指定しないと、kubeadmが既知のソケットのパスを調べて自動検出し、複数あるか、1つもなければ、エラーを出して指定を要求します。Docker EngineはCRIを実装していないので、cri-dockerdを別にインストールする必要があり、kubeletのDocker内蔵サポート(dockershim)は、1.24で削除されました。

ランタイム Unixソケット
containerd unix:///var/run/containerd/containerd.sock
CRI-O unix:///var/run/crio/crio.sock
Docker Engine (cri-dockerd) unix:///var/run/cri-dockerd.sock

cgroupドライバー: kubeletとランタイムは、どちらもcgroupでリソースを制限し、そのときに使うドライバーが、cgroupfsとsystemdの2つです。ドキュメントが「critical」とまで言うルールは、kubeletとランタイムが同じドライバーを使う必要があるということです。kubeletのデフォルトはcgroupfsですが、initシステムがsystemdのディストリビューションでは、cgroupの管理者が2つになり、リソース逼迫の下でノードが不安定になることがあるので、systemdドライバーを使います。cgroup v2を使うなら、systemdドライバーが答えです。kubelet側はKubeletConfigurationのcgroupDriver: systemd、ランタイム側は、各ランタイムのドキュメントの設定です。Kubernetes 1.37では、KubeletCgroupDriverFromCRIフィーチャーゲートが有効で、ランタイムがRuntimeConfigのCRI RPCをサポートしていれば、kubeletがランタイムからドライバーを自動で把握しますが、containerd 1.yのように、そのRPCがないランタイムでは、依然としてkubelet自身の設定を使います。

ポート: ポートとプロトコルのドキュメントの表をそのまま移すと、次のとおりです。

場所 ポート 用途
コントロールプレーン 6443 kube-apiserver
コントロールプレーン 2379-2380 etcdクライアントAPI
コントロールプレーン 10250 kubelet API
コントロールプレーン 10259 kube-scheduler
コントロールプレーン 10257 kube-controller-manager
ワーカー 10250 kubelet API
ワーカー 10256 kube-proxy
ワーカー 30000-32767 NodePort Service (TCP・UDP)

クラスターのライフサイクル: スキューが順序を決める

バージョンスキューポリシーは、プロジェクトが最近の3つのminorリリースブランチを維持し、1.19以降のバージョンは約1年のパッチサポートを受けると書いています。コンポーネント間の許容される差は、次のとおりです。

この規則から、アップグレードの順序が出てきます。kube-apiserverを先に上げ(minorを飛ばせません)、次にcontroller-managerとscheduler、最後にkubeletとkube-proxyを上げます。逆にすると、「kubeletがapiserverより新しい」という禁止状態を経ることになります。ドキュメントは、アップグレードの前に、現在のminorの最新パッチに上げ、目標のminorの最新パッチに進むようにと勧めています。

kubeadmクラスターのアップグレードのドキュメントが定めた手順は、3段階です。最初のコントロールプレーンノード、追加のコントロールプレーンノード、ワーカーノードです。

  1. 最初のコントロールプレーンで、kubeadm upgrade planで上げられるバージョンとコンポーネントの設定状態を確認したあと、kubeadm upgrade apply v1.X.yを実行します。このコマンドは、APIサーバーへの到達可否・すべてのノードのReady・コントロールプレーンの状態を検査し、スキューポリシーを強制し、イメージを準備し、コントロールプレーンのコンポーネントを入れ替えますが、1つでも起動しなければ元に戻し、新しいCoreDNS・kube-proxyのマニフェストを適用します。kubeadmが管理する証明書も、このときに更新されます。
  2. 追加のコントロールプレーンノードでは、kubeadm upgrade nodeを使います。クラスターのClusterConfigurationを取得して、static Podのマニフェストとkubeletの設定を上げます。
  3. 各ノードのkubeletをminorアップグレードするときは、先にdrainします。そのあと、kubelet・kubectlのパッケージを上げ、ワーカーではkubeadm upgrade nodeでkubeletの設定を更新してから、kubeletを再起動し、kubectl uncordonで元に戻します。

ドキュメントは、コンテナスペックのハッシュが変わるため、アップグレード後にすべてのコンテナが再起動されると、あらかじめ知らせています。失敗したときは、同じコマンドを再実行してもよく(冪等)、バージョンを変えないまま、kubeadm upgrade apply --forceで状態を合わせることもでき、/etc/kubernetes/tmpの下に、etcdとマニフェストのバックアップが残ります。

ノードを安全に空にするのドキュメントによると、kubectl drainはPodを丁寧に終了させ、PodDisruptionBudgetを尊重し、DaemonSetのPodがあれば--ignore-daemonsetsが必要です。drainが正常に終わったということは、(除外されたシステムPodを除いて)すべてのPodが安全に追い出されたという意味で、そのあと、kubectl uncordonでスケジューリングを再び開きます。1回に1ノードずつ実行しますが、複数のターミナルで並列に実行しても、PDBは一緒に守られます。

CNI・CSI・CRI: 何を切り離したのか

3つのインターフェースは、名前が似ていますが、切り離した層が違います。

インターフェース 切り離したもの 誰が誰を呼び出すか
CRI コンテナランタイム kubeletがgRPCクライアントとして、ランタイムを呼び出します
CNI Podネットワークの実装 コンテナランタイムがCNIプラグインをロードして、Podのネットワークモデルを実装します
CSI ストレージシステム CSIドライバーがストレージを標準インターフェースとして公開し、Podはcsiボリュームタイプで使います

CRIのドキュメントは、CRIがkubeletとランタイムの間の主なgRPCプロトコルで、v1.23で安定化され、1.26からkubeletがv1のCRI APIを要求するため、サポートしていないランタイムでは、ノードが登録されないと書いています。エンドポイントは、kubeletの--container-runtime-endpointで指定します。ネットワークプラグインのドキュメントは、CNIプラグインがKubernetesのネットワークモデルを実装するのに必須で、CNIスペックv0.4.0以上(推奨v1.0.0)と互換である必要があり、1.24でkubeletのcni-bin-dir・network-pluginフラグが削除されて、CNIの管理はkubeletではなく、ランタイムの役割になったと説明しています。ボリュームのドキュメントのCSIの節は、CSIが任意のストレージシステムをコンテナワークロードに公開する標準インターフェースで、PVCの参照・generic ephemeralボリューム・CSI ephemeralボリュームの3つの方式で使え、以前の方式であるFlexVolumeは、1.23から廃止されたと書いています。

現場での姿

リソース逼迫のときにだけ、ノードが不安定になります。普段は問題ないのに、メモリが埋まるとkubeletが再起動したり、Podが奇妙に死んだりするノードは、cgroupドライバーの不一致を疑います。ドキュメントが、まさにこの症状を書いています。systemdがinitなのに、kubeletとランタイムがcgroupfsを使うと、cgroupの管理者が2つになり、リソースのビューが分かれます。ドキュメントは、すでにクラスターに参加したノードのドライバーを変更するのも、慎重を要する作業だと警告しているので、参加前に合わせるほうが、はるかに安上がりです。

drainなしでkubeletを上げてしまいました。パッケージだけを入れ替えてkubeletを再起動すると、そのノードのコンテナがすべて再起動しますが、drainをしていないので、PDBも丁寧な終了も守られません。ドキュメントが「minorアップグレードは先にdrain」と明記している理由です。

次のクイズで確認すること

クイズでは、swapに対するkubeletのデフォルトの動作、cgroupドライバーをsystemdに合わせる理由、現在の公式ドキュメントが要求するカーネル設定、アップグレードの順序、kubeletの許容スキュー、そしてCNIプラグインをロードする主体が誰なのかを問います。