🏊

Kubernetes deep dive - Nodeの登録と削除

2022/12/11に公開

背景

Kubernetesを構成する各種コンポーネントの中でも、Worker Nodeに常駐しPodを作成したりする仕事などを担っているKubeletですが、kubelet get nodesをしたときにNodeが表示されるのもKubeletが人知れず仕事をしてくれているおかげです。
今回はKubeletが起動したときにどのようにNodeを登録しているのか、Nodeが終了するときはどのようにNodeを削除しているのかKubernetesのコードから追っていきたいと思います。

今回はKubernetes v1.25.5のコードを対象とし、AWS上のLinuxで動作することを前提としています。

Nodeの登録

Kubeletのエントリポイントはcmd/kubelet/kubelet.goです。
https://github.com/kubernetes/kubernetes/blob/842abe9dae1de3f908262b1ed0c621606ae653a2/cmd/kubelet/kubelet.go#L34-L38

35行目で生成した*cobra.Commandを引数に取りcli.Runで起動しています。

このcommandを追ってみると、cli.Runしたときに呼ばれるRunE関数にたどり着きます。

https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/cmd/kubelet/app/server.go#L165-L274

cli.Runしたとき内部的には(*cobra.Command).Executeが呼ばれており、cobra.CommandRunEフィールドに入っている関数を呼び出しています。

ここからRun, run, RunKubeletと飛んで、
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/cmd/kubelet/app/server.go#L272-L273
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/cmd/kubelet/app/server.go#L420
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/cmd/kubelet/app/server.go#L772

RunKubelet関数の中のcreateAndInitKubeletでKubeletの実体を生成しています。
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/cmd/kubelet/app/server.go#L1146-L1151

createAndInitKubeletで生成したKubeletの実体を持って、startKubelet関数を呼び出しKubeletの開始処理へと入っていきます。
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/cmd/kubelet/app/server.go#L1174

ここまではcmd/kubeletがメインでしたが、この(*kubelet.Kubelet).Runからpkg/kubelet側がメインになっていきます。
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/cmd/kubelet/app/server.go#L1182

(*Kubelet).Runです。おそらくここが実質Kubeletのエントリポイントでしょう。
https://github.com/kubernetes/kubernetes/blob/442574f3a7321ff0f34dec9bae67f28651dbabd2/pkg/kubelet/kubelet.go#L1421-L1422

(*Kubelet).Runの中で呼ばれる(*Kubelet).fastStatusUpdateOnceはKubeletの起動時にのみ呼ばれる関数で、pod CIDR, ランタイムのステータス、Nodeのステータスをなるべく早く更新するために置かれているようです。
https://github.com/kubernetes/kubernetes/blob/442574f3a7321ff0f34dec9bae67f28651dbabd2/pkg/kubelet/kubelet.go#L1449
https://github.com/kubernetes/kubernetes/blob/442574f3a7321ff0f34dec9bae67f28651dbabd2/pkg/kubelet/kubelet.go#L2438-L2443

(*Kubelet).fastStatusUpdateOnceの中で呼ばれているSyncNodeStatusを見てみると、(*Kubelet).registerWithAPIServerといういかにもNodeを登録していそうな関数が見つかります。
https://github.com/kubernetes/kubernetes/blob/442574f3a7321ff0f34dec9bae67f28651dbabd2/pkg/kubelet/kubelet.go#L2458
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/pkg/kubelet/kubelet_node_status.go#L432-L435
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/pkg/kubelet/kubelet_node_status.go#L444

(*Kubelet).initialNodeで、登録するNodeのデータであるv1.Nodeオブジェクトを生成し、
(*Kubelet).tryRegisterWithAPIServerでAPI serverにNodeの登録リクエストをしています。
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/pkg/kubelet/kubelet_node_status.go#L48-L51
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/pkg/kubelet/kubelet_node_status.go#L64
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/pkg/kubelet/kubelet_node_status.go#L71
https://github.com/kubernetes/kubernetes/blob/0064010cddfa009fe16ae23fcd0c57f4f15d227c/pkg/kubelet/kubelet_node_status.go#L86

このリクエストが成功すればNodeが登録され、kubectl get nodesで見えるようになるということになります。

Nodeの削除

ここまでNodeの登録の流れを追ってきましたが、Nodeの削除はどのように行っているのでしょうか。
もし登録時のようにKubeletが自分がいるNodeの削除リクエストをしているのだとすると、KubeletやNodeが異常終了したり、削除リクエストをしたときにAPIサーバーが不調なときにいつまでもNodeオブジェクトが残ったままになってしまいそうです。
そうなるとkubectl get nodesをしたときに古いNodeがいつまでも残ったままになってしまい見にくそうですね。

Kubernetesの宣言型APIなどに見られる冪等性の観点からしてもそのアプローチはあまりあっていないように見えます。

それではNodeの削除の流れを追ってみましょう。

先に種明かしをしてしまうと、Node削除時に仕事をしているのはkube-controller-manager, cloud-controller-managerです。

まずはエントリポイントから見ていきます。
Kubeletのときと同じくcobraを使っているので、RunE関数までスキップします。
https://github.com/kubernetes/kubernetes/blob/98837de446fa4fbf441b69eb04da1ca5b66c2095/cmd/kube-controller-manager/controller-manager.go#L34-L39
https://github.com/kubernetes/kubernetes/blob/94f1a197e4bd2368262d586189c4b956b56e16c8/cmd/kube-controller-manager/app/controllermanager.go#L123-L139

RunEの中のRun関数を辿ってみると、run関数の第3引数に渡しているNewControllerInitializersが見つかります。
https://github.com/kubernetes/kubernetes/blob/94f1a197e4bd2368262d586189c4b956b56e16c8/cmd/kube-controller-manager/app/controllermanager.go#L138
https://github.com/kubernetes/kubernetes/blob/94f1a197e4bd2368262d586189c4b956b56e16c8/cmd/kube-controller-manager/app/controllermanager.go#L238

このNewControllerInitializersは各種リソースのコントローラーの開始処理を持っていて、Nodeに関するコントローラーはstartNodeLifecycleController, startCloudNodeLifecycleControllerで定義されています。
https://github.com/kubernetes/kubernetes/blob/94f1a197e4bd2368262d586189c4b956b56e16c8/cmd/kube-controller-manager/app/controllermanager.go#L414-L464

このうち、NodeLifecycleControllerはHealth status, Taintの管理などKubernetesレイヤーのNodeのライフサイクル、CloudNodeLifecycleControllerは各種クラウドプロバイダー(AWS, GCPなど)との繋ぎこみといったKubernetesを構成するのに必要な比較的下側のNodeのライフサイクルを管理している印象です。

ということでNodeの削除は下側の世界にかかわるのでstartCloudNodeLifecycleControllerを追っていきます。
https://github.com/kubernetes/kubernetes/blob/759785ea147bc13945d521eaba4a6592cbc0675f/cmd/kube-controller-manager/app/core.go#L207
https://github.com/kubernetes/kubernetes/blob/759785ea147bc13945d521eaba4a6592cbc0675f/cmd/kube-controller-manager/app/core.go#L222

startCloudNodeLifecycleControllerから(*cloudnodelifecyclecontroller.CloudNodeLifecycleController).Runに飛ぶと、
(*CloudNodeLifecycleController).MonitorNodesCloudNodeLifecycleController.nodeMonitorPeriodで設定された間隔で実行されるようになっていることがわかります。
https://github.com/kubernetes/kubernetes/blob/f3996039ef13ef75b0c91a7329f5098851e5ec34/staging/src/k8s.io/cloud-provider/controllers/nodelifecycle/node_lifecycle_controller.go#L105-L107
https://github.com/kubernetes/kubernetes/blob/f3996039ef13ef75b0c91a7329f5098851e5ec34/staging/src/k8s.io/cloud-provider/controllers/nodelifecycle/node_lifecycle_controller.go#L124

ちなみにデフォルトだと5秒ごとのようです。
https://github.com/kubernetes/kubernetes/blob/b1e0decce19563fc6c9aa188105b7c03ab5ee358/staging/src/k8s.io/cloud-provider/config/v1alpha1/defaults.go#L56-L58

(*CloudNodeLifecycleController).MonitorNodesの中でReady=TrueのNodeはスキップしつつ、ensureNodeExistsByProviderIDでNodeオブジェクトに書かれているproviderIDをもとに各種クラウドプロバイダーにインスタンスがまだ存在しているかを問い合わせます。
https://github.com/kubernetes/kubernetes/blob/f3996039ef13ef75b0c91a7329f5098851e5ec34/staging/src/k8s.io/cloud-provider/controllers/nodelifecycle/node_lifecycle_controller.go#L127-L130
https://github.com/kubernetes/kubernetes/blob/f3996039ef13ef75b0c91a7329f5098851e5ec34/staging/src/k8s.io/cloud-provider/controllers/nodelifecycle/node_lifecycle_controller.go#L138-L151
https://github.com/kubernetes/kubernetes/blob/f3996039ef13ef75b0c91a7329f5098851e5ec34/staging/src/k8s.io/cloud-provider/controllers/nodelifecycle/node_lifecycle_controller.go#L153-L155

問い合わせの結果、インスタンスがすでに存在していないことがわかったときNodeオブジェクトを削除しています。
https://github.com/kubernetes/kubernetes/blob/f3996039ef13ef75b0c91a7329f5098851e5ec34/staging/src/k8s.io/cloud-provider/controllers/nodelifecycle/node_lifecycle_controller.go#L161
https://github.com/kubernetes/kubernetes/blob/f3996039ef13ef75b0c91a7329f5098851e5ec34/staging/src/k8s.io/cloud-provider/controllers/nodelifecycle/node_lifecycle_controller.go#L176

この削除リクエストが成功すればNodeが削除され、kubectl get nodesの結果から外されることになります。

まとめ

Nodeの登録と削除がどのように行われるかを見てきました。
登録時はNodeにいるKubeletがNodeを登録していて、削除時はControl Planeにいるcloud-controller-managerのCloudNodeLifecycleControllerがReadyではないNodeがまだ存在しているかを照会し、削除済であればKubernetesの世界からも削除を行うという仕組みになっていることがわかりました。

途中で端折った箇所もありますが、大まかな流れは追えているかと思います。

そして最後に、この記事を書くにあたって改めて公式のドキュメントに立ち返ってみたところちゃんと書いてありました。
この2つの違いがわかっていればもう少し早く理解できたのに……(Node削除時の処理を探してkube-controller-managerの周りをずっとぐるぐる回っていた)

kube-controller-manager
コントロールプレーン上で動作するコンポーネントで、複数のコントローラープロセスを実行します。
(中略)
ノードコントローラー:ノードがダウンした場合の通知と対応を担当します。

cloud-controller-manager
クラウド特有の制御ロジックを組み込むKubernetesのcontrol planeコンポーネントです。クラウドコントロールマネージャーは、クラスターをクラウドプロバイダーAPIをリンクし、クラスタのみで相互作用するコンポーネントからクラウドプラットフォームで相互作用するコンポーネントを分離します。
(中略)
ノードコントローラー:ノードが応答を停止した後、クラウドで削除されたかどうかを判断するため、クラウドプロバイダーをチェックします。

引用:https://kubernetes.io/ja/docs/concepts/overview/components/

kube-controller-managerのNode ControllerがNodeLifecycleController、cloud-controller-managerのNode ControllerがCloudNodeLifecycleControllerに該当します。

また次の機会があれば、先にドキュメントを読むことを忘れないようにしたいと思います。

Discussion