クラウド上のノードには、どのリージョンやゾーンにあるかを示すラベルが付きます。 Kubernetes 本体だけでは分からない情報を、クラウドに問い合わせる担当がいるためです。
この章では、その連携を担う cloud-controller-manager (CCM) を見ます。 ノードの準備やロードバランサーの作成を例に、Kubernetes の依頼がクラウドの操作につながる流れを追います。
GOAL この章のゴール
- クラウド連携が担当する仕事を説明できる
- ノードの初期化時に、情報が設定される流れを追える
- ロードバランサーを作る担当がいない場合の表示を確認できる
CPI と CCM
ノードがどのゾーンにあるかを調べるには、そのノードを動かしているクラウドに問い合わせます。 ロードバランサーを作るときも、クラウドごとの API を呼ぶ必要があります。 こうしたクラウド固有の仕事をまとめて動かすプログラムが CCM です。
以前は Kubernetes 本体に含まれていた処理を分けたことで、クラウド側の対応を Kubernetes 本体とは別に更新しやすくなりました。 CCM の中からクラウドごとの処理を呼ぶための取り決めを、CPI と呼びます。
CPI は Go の interface (k8s.io/cloud-provider の cloudprovider.Interface) で、LoadBalancer / InstancesV2 / Routes などのメソッドを持ちます。
CRI や CSI が socket 越しの gRPC で別プロセスと話すのに対し、CPI は同じプロセスの中で呼ばれる Go の関数です。
kubernetes.io の「Developing Cloud Controller Manager」によると、各クラウドはこの interface を満たす Go のパッケージを書き、Kubernetes 本体の cloud-controller-manager の main.go を雛形にして自分のパッケージを import したバイナリをビルドします。
Node / Route / Service の controller の実装は本体側にあり、クラウドごとに違うのはそこから呼ばれるパッケージだけです。
Go の interface で済む理由は、呼ぶ側の位置にあります。 CRI や CSI は各ノードの kubelet が呼ぶので、kubelet を変えずにランタイムや driver を差し替えるには別プロセスと socket で話す必要がありました。 CPI はクラスタ側の controller が呼ぶもので、controller 自体をクラウドごとに別のバイナリとしてビルドして配ればよいのです。
3 つの controller
- Node controller:Node を watch し、クラウドの API から取った情報を Node に書き込みます。kubernetes.io が挙げる仕事は、インスタンスの一意な ID (
providerID)、リージョンや zone のラベル、ホスト名とネットワークアドレス、そしてインスタンスがクラウド側で削除されていたら Node オブジェクトを消すこと、の 4 つです。 - Route controller:ノード間で Pod が通信できるよう、クラウドの経路表に「この Node の
podCIDRはこのインスタンスへ」という経路を書きます。クラウドによっては Pod ネットワークのアドレスブロックの割り当ても担当します。Cilium のようにオーバーレイを使う構成では不要です。 - Service controller:
type: LoadBalancerの Service を watch し、クラウドのロードバランサーを作って設定します。kubernetes.io は、対象をロードバランサーのほか IP アドレス、パケットフィルタ、ヘルスチェックのようなクラウドの部品だとしています。
AWS で言えば、Node controller は EC2 の DescribeInstances、Route controller は VPC の CreateRoute、Service controller は ELB の CreateLoadBalancer にあたる API を呼ぶプロセスです。
uninitialized taint
Node controller の仕事は、ノードが参加した直後から始まります。
kubernetes.io の「Cloud Controller Manager Administration」によると、--cloud-provider=external を指定したコンポーネントは、初期化のときに node.cloudprovider.kubernetes.io/uninitialized という taint を NoSchedule の効果で Node に付けます。
これは「外部の controller による 2 回目の初期化が済むまで、Pod を乗せるな」という印です。
CCM の Node controller は taint 付きの Node を見つけると、クラウドの API からインスタンス情報を取得して providerID と addresses とラベルを書き込み、最後に taint を外します。
この動きを使うには、kubelet だけでなく kube-controller-manager にも --cloud-provider=external を指定します。
CCM 自身の Pod はこの taint が付いたノードにも乗れるよう、toleration (taint を許容する指定) を持ちます。
同じドキュメントは、この順序の理由も書いています。 scheduler は Node のリージョンやインスタンスの種類 (GPU や spot など) を見て Pod の置き場所を決めることがあるので、その情報が無いうちに Pod を乗せるわけにはいかない、という理由です。 裏返すと、CCM が動いていないクラスタでは新しいノードがいつまでも schedulable にならない、と同じドキュメントは注意しています。
in-tree の削除
このクラスタの CCM
あなたの tenant クラスタにも CCM が居ます。 ただし少し変わった置き方です。
kubevirt-cloud-controller-manager の README によると、この CCM は tenant クラスタの kubeconfig を --kubeconfig で、管理クラスタ (README では infrastructure cluster) の kubeconfig を cloud-config で受け取り、2 つのクラスタをまたいで動きます。
この環境では管理クラスタ側に置かれています。
「クラウドの API」にあたるのは KubeVirt の VirtualMachineInstance で、そこから VM の名前と IP を読んで Node に書き込み、VM の zone と region を Node のラベルに写します。
Node controller としては完全に動いています。
一方で、この環境では LoadBalancer 機能を無効にしてあります。
README によると、有効なら tenant の type: LoadBalancer に対して管理クラスタ側に Service を作り、tenant の VM をその宛先にする動きをしますが、ここではその controller が何もしません。
「Service controller が居ない世界」を、安全に観察できる環境です。
実機: CCM が書いたものを読む
kubectl get node -o json | jq '.items[0] | {providerID: .spec.providerID, taints: .spec.taints, addresses: .status.addresses, zone: .metadata.labels["topology.kubernetes.io/zone"]}'出力例
{
"providerID": "kubevirt://<vm 名>",
"taints": null,
"addresses": [
{ "type": "InternalIP", "address": "10.0.x.x" },
{ "type": "Hostname", "address": "<node>" }
],
"zone": "..."
}providerID の kubevirt:// という接頭辞は、CPI の実装 (プロバイダー) の名前です。
AWS なら aws:///ap-northeast-1a/i-0abc... の形になります。
taints が無いのは、Node controller が初期化を終えて uninitialized を外した後だからです。
kubelet 側の指定は、ノードの中で確かめられます。
kubeadm はこのフラグを /var/lib/kubelet/kubeadm-flags.env に書くので、config.yaml の方には出てきません。
cat /var/lib/kubelet/kubeadm-flags.env出力例
KUBELET_KUBEADM_ARGS="--cloud-provider=external --container-runtime-endpoint=unix:///run/containerd/containerd.sock ..."次に、Service controller が居ないことを確かめます。
kubectl create deployment hello --image=nginx:1.29 --port=80
kubectl expose deployment hello --type=LoadBalancer --port=80
kubectl get svc hello -w出力例
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
hello LoadBalancer 10.96.45.12 <pending> 80:31234/TCP 5s
hello LoadBalancer 10.96.45.12 <pending> 80:31234/TCP 60sいつまで待っても <pending> のままです。
Ctrl-C で止めてください。
kubernetes.io によると、ロードバランサーの作成は非同期に行われ、できあがった情報は Service の .status.loadBalancer に書かれます。
それを書くのは Service controller で、居ないので誰も書かない。
ClusterIP と NodePort (各ノードの固定ポートで受ける入口。出力の 31234) は kube-apiserver が割り当てるので、その 2 つは埋まっています。
type: LoadBalancer は ClusterIP、NodePort、クラウドのロードバランサーの 3 つでできていて、最後の 1 つだけが欠けている状態です。
kubectl describe svc hello | sed -n '/Events/,$p'
kubectl delete svc,deployment hello出力例
Events: <none>Service controller が居て失敗したなら、ロードバランサーの同期に失敗したというイベントが出ます。
何も出ないのは、watch している者が居ないからです。
EKS で同じ <pending> を見たら、まず CCM (cloud-provider-aws、または AWS Load Balancer Controller) のログを見る、という判断がここから導けます。
ふりかえり
Qnode.cloudprovider.kubernetes.io/uninitialized の taint を外すのは誰?
kubelet が –cloud-provider=external で起動時に付け、CCM の Node controller がクラウドの情報を書き込んだ後に外します。CCM が居ないと永遠に残ります。
QCPI が Go の interface として定義されていて、gRPC を使わないのはなぜ?
CRI / CSI は kubelet が別プロセスを呼ぶので socket が要ります。CPI は kube-controller-manager から抜き出した controller 群 (CCM) が直接使う interface で、クラウドごとに別のバイナリをビルドします。
Qこのクラスタで type: LoadBalancer の EXTERNAL-IP が pending のままになる理由は?
ClusterIP と NodePort は apiserver が割り当てるので埋まります。クラウドのロードバランサーを作って status に書くのは CCM の Service controller で、この環境では無効化されています。
参考
- Cloud Controller Manager, Kubernetes Documentation: https://kubernetes.io/docs/concepts/architecture/cloud-controller/
- Cloud Controller Manager Administration, Kubernetes Documentation: https://kubernetes.io/docs/tasks/administer-cluster/running-cloud-controller/
- Developing Cloud Controller Manager, Kubernetes Documentation: https://kubernetes.io/docs/tasks/administer-cluster/developing-cloud-controller-manager/
cloudprovider.Interface(cloud.go), kubernetes/cloud-provider: https://github.com/kubernetes/cloud-provider/blob/master/cloud.go- Completing the largest migration in Kubernetes history (in-tree cloud provider の削除), Kubernetes Blog: https://kubernetes.io/blog/2024/05/20/completing-cloud-provider-migration/
- KEP-2395: Removing In-Tree Cloud Providers: https://github.com/kubernetes/enhancements/blob/master/keps/sig-cloud-provider/2395-removing-in-tree-cloud-providers/README.md
- Kubernetes の変更履歴 (CHANGELOG-1.26 / 1.27 / 1.30 / 1.31): https://github.com/kubernetes/kubernetes/tree/master/CHANGELOG
- Service (type: LoadBalancer), Kubernetes Documentation: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer
- kubevirt/cloud-provider-kubevirt README: https://github.com/kubevirt/cloud-provider-kubevirt/blob/main/README.md