Certified Kubernetes Administrator (CKA) #3 クラスターアーキテクチャ 2: Node (kubelet/kube-proxy/CRI)、Pod ネットワーキングモデル

#2 クラスターアーキテクチャ 1 では、control plane の 4 つのコンポーネントがどのようにクラスターの決定を下すのかを見ました。apiserver が通信の関門となり、etcd が状態を保存し、scheduler が Pod の配置を決め、controller-manager が reconciliation loop を回します。ところが、これらの決定はすべて 決定にすぎません。実際にコンテナを起動し、トラフィックを流し、ディスクとネットワークをつなぐ作業は、すべて ワーカーノード で起こります。

この記事はそのノードを覗き込みます。ノードの上で動く 3 つのコンポーネント (kubelet、kube-proxy、コンテナランタイム) がそれぞれ何をするのか、kubelet とランタイムをつなぐ CRI という標準とは何か、そしてすべての Pod が互いを直接呼べるようにする Pod ネットワーキングモデル と、それを実際に実装する CNI プラグインまで、運用の観点から整理します。

ノードは control plane が下した決定を実行する #

Kubernetes を 2 つの層に分けて見ると理解が早いです。control plane は「何をどこに起動するか」を決める 頭脳 であり、ワーカーノードはその決定を受けて「実際にコンテナを起動する」 手足 です。scheduler が「この Pod を node01 に配置する」と決めると、その決定は etcd に保存されるだけで、まだ何のコンテナも起動していません。node01 の kubelet がその決定を読み、コンテナランタイムにコンテナを起動するよう指示した瞬間に、ようやくワークロードが実行されます。

そのためノードコンポーネントを理解すると、トラブルシューティングの半分が解けます。Pod が Pending で止まっているなら scheduler 側の問題かもしれませんが、ContainerCreating で止まっているなら kubelet やランタイム、あるいは CNI 側を見る必要があります。ノードが NotReady になると、そのノードにあった Pod 全体が影響を受けます。ノードの 3 つのコンポーネントは次のとおりです。

コンポーネント役割どこで動くか
kubeletノードエージェント。Pod を実際に実行し状態を報告すべてのノード (control plane ノードを含む)
kube-proxyService の仮想 IP をノードのルーティング規則として実装すべてのノード (通常は DaemonSet)
コンテナランタイムコンテナイメージを受け取り実際のコンテナを起動すべてのノード

control plane ノードも、実はワーカーノードです。control plane コンポーネント自体が static Pod として起動しているため、control plane ノードの上でも kubelet とランタイムが動きます。#2 で見た apiserver と etcd が static Pod として起動するのも、結局はそのノードの kubelet が起動してくれるものです。

kubelet: ノードのエージェント #

kubelet は すべてのノードで動くただ 1 つのエージェント であり、ノードコンポーネントの中で最も中核です。他のコンポーネントが Pod として起動しているのとは違い、kubelet は ノードの systemd サービス として直接動きます。kubelet が Pod ではなくサービスである理由は明確です。kubelet が Pod を起動する主体なのに、それ自身が Pod だとしたら、鶏が先か卵が先かの問題に陥ってしまうからです。

kubelet がする仕事を整理すると次のとおりです。

  • Pod の実行。apiserver を watch していて、自分のノードに配置された Pod ができると、コンテナランタイムにコンテナを起動するよう指示します。PodSpec に書かれたイメージ、ボリューム、環境変数、probe 設定をすべてランタイムに渡します。
  • 状態の報告。自分のノードの状態 (Ready かどうか、リソースの空き) と各 Pod の状態を、定期的に apiserver に報告します。k get nodesk get pods に見える状態は、結局 kubelet が上げた報告です。
  • probe の実行。livenessProbe、readinessProbe、startupProbe を実際に実行する主体が kubelet です。liveness が失敗すると、kubelet が直接コンテナを再起動します。
  • static Pod の管理。kubelet は apiserver なしでも /etc/kubernetes/manifests に置かれたマニフェストを読んで Pod を起動します。これが static Pod であり、control plane コンポーネントはまさにこの方式でブートストラップされます。

static Pod は CKA で特に重要です。apiserver が死んでいても kubelet はこのディレクトリだけを見て Pod を起動できるので、control plane が「自分自身を起動する」ブートストラップが可能になります。apiserver のマニフェストを誤って直して apiserver が起動しない状況は、#24 で扱う代表的なトラブルシューティングシナリオです。

# kubelet は systemd サービスとして動く
systemctl status kubelet

# kubelet ログ (NotReady の原因追跡の第 1 順位)
journalctl -u kubelet -f

# static Pod マニフェストディレクトリ
ls /etc/kubernetes/manifests/

kubelet が死ぬと、そのノードはもう apiserver に状態を報告できなくなるので、しばらく後にノードが NotReady に変わります。ただし、すでに起動していた Pod は、kubelet が死んだからといってすぐに消えるわけではありません。kubelet が再び生き返ると、それらの Pod を再び管理し始めます。NotReady の原因追跡は #23 で本格的に扱います。

kube-proxy: Service をノードの上で実装する #

kube-proxy は Service の仮想 IP を実際のルーティングに変換してくれる コンポーネントです。Service が何かは #18 で詳しく扱いますが、ここで核心だけ押さえるとこうです。ClusterIP Service は 10.96.0.10 のような 仮想 IP を持ちますが、この IP を持つネットワークインターフェースはクラスターのどこにもありません。ただの約束されたアドレスにすぎません。

では、その仮想 IP に送ったトラフィックはどうやって実際の Pod に届くのでしょうか。まさに kube-proxy が各ノードに ルーティング規則 を敷いておくからです。kube-proxy は apiserver を watch していて、Service とその背後の Pod (endpoints) が変わると、ノードのカーネル規則を更新します。結果として「この仮想 IP に向かうパケットは実際の Pod IP のいずれかに送る」という規則が、すべてのノードに敷かれます。

kube-proxy は通常 DaemonSet としてデプロイされ、すべてのノードに 1 つずつ動きます。どのノードから送ったトラフィックでも、そのノードの規則を経て宛先 Pod に行く必要があるので、規則はすべてのノードになければなりません。

iptables モードと IPVS モード #

kube-proxy が規則を敷く方式には、大きく 2 つがあります。

モード動作特徴
iptablesカーネルの iptables 規則で DNAT を処理デフォルト。安定。Service が増えると規則の評価が線形に増えて性能が落ちる
IPVSカーネルの IPVS (ハッシュテーブル) で処理Service 数が多い大規模クラスターに有利。多様なロードバランシングアルゴリズムをサポート

デフォルトは iptables モードです。Service が数千個規模に増える大規模クラスターでは、iptables 規則を順次評価するコストが大きくなるので、ハッシュベースの IPVS モードがより良い性能を出します。CKA では、2 つのモードの違いとどちらが大規模に有利かという程度を知っておけば十分です。どのモードかは kube-proxy の ConfigMap やログで確認できます。

コンテナランタイムと CRI #

kubelet はコンテナを 直接 起動しません。kubelet は「このイメージでコンテナを 1 つ起動しろ」と指示するだけで、実際にイメージを受け取ってネームスペースと cgroup を作りプロセスを起動する作業は コンテナランタイム がします。今日最も広く使われるランタイムは containerd であり、CRI-O もよく使われます。

CRI: kubelet とランタイムの間の標準インターフェース #

kubelet がどのランタイムとも同じ方式で対話できる理由は、CRI (Container Runtime Interface) という標準があるからです。CRI は kubelet とランタイムの間に置かれた gRPC ベースの標準インターフェースです。kubelet は CRI 規約どおりに「コンテナを起動しろ」「イメージを受け取れ」といったリクエストを送り、その規約を実装したランタイムなら何であれそのリクエストを処理します。

過去には kubelet の中に Docker を直接呼び出す dockershim というアダプターが入っていました。ところが Docker は CRI を直接実装しておらず別のアダプターが必要で、この dockershim は Kubernetes 1.24 で削除されました。その結果、今の標準的な経路は kubelet → CRI → containerd (または CRI-O) → コンテナ です。Docker で作ったイメージ (OCI イメージ) はそのまま使えるので、イメージの互換性は問題ありません。ただ、ノードでコンテナを起動する主体が Docker デーモンではなく containerd になっただけです。

crictl: CRI レベルでコンテナを覗き込む #

ランタイムが containerd に変わったことで、ノードでコンテナを直接確認するときは docker の代わりに crictl を使います。crictl は CRI を通じてランタイムと対話するデバッグツールです。

# ノードで動くコンテナを確認 (docker ps の CRI 版)
crictl ps

# コンテナイメージの一覧
crictl images

kubelet が報告する Pod と crictl が見せるコンテナが食い違うとき (例: kubelet は生きていると言うのにコンテナが起動しない)、ランタイムレベルの問題を疑うことになります。トラブルシューティングでノードの中へもう一段下りるツールです。

ノードの登録と状態確認 #

ノードがクラスターに合流すると (この join プロセスは #4 で kubeadm を使って直接やってみます)、そのノードの kubelet が apiserver に自分自身を登録し、以後は定期的に状態を報告します。管理者が真っ先に見るコマンドは k get nodes です。

# ノードの一覧と状態
k get nodes

# ノードの IP、OS、カーネル、コンテナランタイムまで一目で
k get nodes -o wide

-o wide を付けると、内部 IP、オペレーティングシステム、カーネルバージョン、そして コンテナランタイムバージョン (例: containerd://1.7.x) まで見えます。どのノードがどのランタイムを使うかを一度に確認できます。

ノードの STATUS は通常 Ready ですが、次のような理由で NotReady になることがあります。

  • kubelet が死んだか起動できない。最も多い原因。systemctl status kubeletjournalctl -u kubelet が最初の確認ポイント
  • コンテナランタイムが死んだ。kubelet が CRI でランタイムに届かないと、ノードを正常と報告できない
  • CNI が準備できていない。ネットワークプラグインがまだ入っていないか壊れると、ノードが NotReady にとどまる
  • リソース圧迫。disk pressure、memory pressure のような condition が付いて、正常なスケジューリングを妨げる

ここでは「NotReady が見えたらどこから見るか」の大きな絵だけをつかみ、具体的な原因別の復旧は #23 で段階的に扱います。

Pod ネットワーキングモデル #

Kubernetes ネットワーキングの出発点はただ 1 つの約束です。すべての Pod は NAT なしで互いの IP へ直接通信できなければならない ということです。このモデルを噛み砕くと次のとおりです。

  • すべての Pod は 固有の IP を持ちます。同じノードにあろうと別のノードにあろうと同じです。
  • ある Pod は別の Pod の IP へ NAT なしで 直接パケットを送れます。
  • ノード上のエージェント (kubelet など) も、そのノードの Pod と直接通信できます。

この約束のおかげで、開発者は Pod がどのノードにあるかを気にせず IP だけで通信を設計できます。従来の仮想化環境でよく見たポートマッピングや NAT は、Pod の間にはありません。

Pod CIDR とノード別の分割 #

このモデルを実装するには IP が重なってはいけません。そこでクラスターには全 Pod に使う大きな IP 帯域である Pod CIDR (例: 10.244.0.0/16) を定めておき、この帯域をノード別に細かく分けます。例えば node01 は 10.244.1.0/24、node02 は 10.244.2.0/24 を受け取るという具合です。各ノードは自分に割り当てられたサブネットの中だけで Pod に IP を割り当てるので、クラスター全体で Pod IP が重なりません。

では、node01 の Pod が node02 の Pod に送ったパケットは、どうやってノードの境界を越えるのでしょうか。これを ノード間ルーティング で解いてくれるのが、次に見る CNI プラグインです。

pause コンテナ #

Pod の中の複数のコンテナが 同じ IP と同じネットワークネームスペース を共有する理由は、Pod ごとに pause コンテナ という見えないコンテナが先に起動するからです。pause コンテナがネットワークネームスペースを押さえておき、同じ Pod の他のコンテナがそのネームスペースに合流する構造です。だから 1 つの Pod の中のコンテナ同士は localhost で互いを呼べます。

CNI プラグインが実際のネットワークを実装する #

Kubernetes 自体は「Pod ネットワーキングモデル」という 規則だけを定め、その規則を実際のネットワークとして実装する作業は CNI (Container Network Interface) プラグイン に任せます。kubelet は Pod を起動するとき CNI プラグインを呼んで、その Pod に IP を付けてネットワークに接続します。CNI が入っていないノードは Pod にネットワークを与えられないので、NotReady にとどまります。

代表的な CNI プラグインは次のとおりです。

プラグイン特徴
CalicoBGP ベースのルーティング、豊富な NetworkPolicy サポートで広く使われる
CiliumeBPF ベース。高性能ときめ細かいセキュリティポリシー
Flannelシンプルな overlay ネットワーク。設定が簡単で学習と小規模に適する

CNI プラグインの構造と NetworkPolicy の動作原理をさらに深く見たいなら、Kubernetes 深掘りシリーズの CNI 編 を一緒に読むことをおすすめします。CKA の範囲では「CNI が Pod ネットワーキングモデルを実装し、これがないとノードが NotReady になる」という因果をつかむことが優先です。

ノードを確認する核心コマンド #

この記事で扱ったコンポーネントを実際に確認するコマンドを一か所にまとめると次のとおりです。

# ノードの状態とランタイムを一目で
k get nodes -o wide

# kubelet の状態 (NotReady の第 1 確認)
systemctl status kubelet
journalctl -u kubelet -f

# ノードで動くコンテナ (CRI レベル)
crictl ps

# kube-proxy と CNI は通常 kube-system ネームスペースの Pod で確認
k get pods -n kube-system -o wide

最後のコマンドで、kube-proxy と CNI プラグイン (例: calico-node) が各ノードに正常に起動しているかを確認できます。ノード 1 つだけが NotReady なら、そのノードの kube-proxy や CNI Pod が起動しているかを見ることが手がかりになります。

試験ポイント #

  • kubelet は systemd サービス として動きます。Pod ではありません。NotReady 追跡の第 1 順位は systemctl status kubeletjournalctl -u kubelet です。
  • static Pod は kubelet が /etc/kubernetes/manifests を見て apiserver なしで起動します。control plane コンポーネントがこの方式でブートストラップされます。
  • kube-proxy は Service の仮想 IP をノードのルーティング規則として実装します。iptables (デフォルト) と IPVS (大規模に有利) の 2 つのモードを区別します。
  • CRI は kubelet とランタイムの間の標準インターフェースです。dockershim が 1.24 で削除された後は containerd/CRI-O が標準であり、ノードでは docker ではなく crictl ps で確認します。
  • Pod ネットワーキングモデル は「すべての Pod が NAT なしで直接通信」です。Pod CIDR をノード別に分割し、実際の実装は CNI プラグイン (Calico/Cilium/Flannel) が担います。CNI がないとノードが NotReady にとどまります。
  • k get nodes -o wide でノード別のコンテナランタイムバージョンまで確認できます。

まとめ #

この記事で押さえたこと:

  • ノードは control plane の決定を実行する手足。scheduler が決めた配置を kubelet が受けて、ランタイムにコンテナを起動させる。
  • kubelet。ノードエージェント。Pod の実行、状態の報告、probe の実行、static Pod の管理。systemd サービスとして動く。
  • kube-proxy。Service の仮想 IP を iptables/IPVS 規則として実装。通常は DaemonSet。
  • コンテナランタイムと CRI。kubelet は CRI を通じて containerd/CRI-O と対話します。dockershim は 1.24 で削除。確認は crictl
  • Pod ネットワーキングモデル。NAT なしの Pod 間通信、ノード別の Pod CIDR、pause コンテナ、CNI プラグインが実際に実装。

次へ — kubeadm クラスター構築 #

アーキテクチャを control plane (#2) とノード (この記事) の 2 つの層ともに見渡しました。いよいよこれらのコンポーネントを 自分の手で立てる 番です。

#4 kubeadm クラスター構築 では、空の Linux マシンにコンテナランタイムと kubeadm を入れ、kubeadm init で単一の control plane をブートストラップした後、CNI プラグインを適用し、ワーカーノードを kubeadm join で合流させる過程を最初から最後まで追います。この記事で見た kubelet、CRI、CNI がどのように一か所で噛み合うかを手で確認する記事です。

X