Kubernetes に「このアプリを外部へ公開したい」と設定すると、クラウドにロードバランサーが作られる構成があります。 利用者が操作したのは Kubernetes ですが、実際にはクラウド側の API を呼ぶ担当も動いています。
この章では、その担当が設定の変化を見つけ、クラウドを操作し、結果を Kubernetes に戻す流れを追います。 ノードの初期化も例にして、クラウドとの連携が進む仕組みを確かめます。
GOAL この章のゴール
- クラウド連携の各 controller が担当する仕事を説明できる
- ノードの初期化とロードバランサーの作成の流れを追える
- 外部アドレスが決まらない理由を、担当の有無から調べられる
ELB を作る部品、cloud-controller-manager
Part 4 では、kube-controller-manager からクラウド固有の処理だけを切り出した別のプロセスがあると紹介しました。
これが cloud-controller-managerクラウドコントローラーマネージャーkube-controller-manager からクラウド固有の処理だけを切り出したプロセス。Node / Route / Service の 3 つの controller を持つ。略して CCM。用語集で見る (以下 CCM) です。
Kubernetes のドキュメントによると、CCM はプラグインの仕組みで作られていて、クラウド会社は CloudProvider という Go のインターフェースを実装して自社のクラウドを繋ぎます。
CCM の中身は 3 つの controller です。
- Node controller:新しい VM が起動して kubelet が Node を登録すると、クラウドの API でその VM を調べ、クラウド側の一意な ID (
providerID)、ホスト名とアドレス (addresses)、リージョンやインスタンスタイプの label を Node に書き込む。Node が応答しなくなったときはクラウドの API で VM が削除されていないかを確かめ、消えていれば Node オブジェクトも削除する。 - Route controller:別のノードにあるコンテナ同士が通信できるように、クラウド側に経路を設定する (AWS なら VPC の route table に
10.244.1.0/24 → eni-…のような行)。クラウドによっては Pod ネットワーク用の IP ブロックの割り当ても行う。VPC CNI のように Pod に VPC の IP を直接配る方式では使わない。 - Service controller:
type: LoadBalancerの Service を見て、クラウドの LB を作り、ターゲットを登録し、ヘルスチェックを設定し、status.loadBalancer.ingressに結果を書き戻す。Service が消えれば LB も消す。
AWS の API に置き換えると、Node controller は EC2、Route controller は VPC の route table、Service controller は ELB の API を叩いています。 IAM ロールの権限が足りないときに最初に止まるのもこの 3 つです。 クラウドの API 呼び出しの大半が CCM に集まるので、Kubernetes のドキュメントも、大きなクラスタではクラウド側のレート制限に影響すると注意しています。
ノードが初期化待ちから抜けるまで
EKS のノードを kubectl describe node すると、ProviderID に aws:///ap-northeast-1a/i-0123456789abcdef0 という値が入っています。
この値は、その Node がどのクラウドのどのインスタンス (ここでは EC2 の i-…) であるかを示す ID です。
この値を書くのは Node controller で、書くのは kubelet がノードを登録した直後です。
順を追うと、kubelet が --cloud-provider=external で起動すると、Node を登録するときに node.cloudprovider.kubernetes.io/uninitialized=true:NoSchedule という taint を自分で付けます。
この taint は「クラウドの情報をまだ誰も埋めていないので、Pod を置かないでくれ」という印です。
続いて Node controller がクラウドに問い合わせて providerID と addresses を埋め、この taint を外します。
kubelet だけでなく kube-controller-manager にも --cloud-provider=external を渡して、クラウド連携の loop を CCM だけが回すようにします。
CCM の Pod 自身は、この taint を許容 (toleration) して起動します。
taint の付いたノードに置けなければ CCM が起動できず、初期化が永久に終わらなくなるからです。
この順序のため、CCM が居ないクラスタで kubelet にこの設定をすると、Node は Ready なのに taint が残り、Pod は Pending のままになります。
in-tree から外部へ
ここまでの説明では CCM を独立したプロセスとして扱いましたが、最初からそうだったわけではありません。
同じ方針は CSI にも適用されていて、in-tree の EBS ボリュームプラグインは CSI driver への移行が済み、新機能は aws-ebs-csi-driver 側にしか入りません。 「クラウド固有のものは本体の外へ」が Kubernetes の一貫した方針です。
Service controller が LB を作る流れ
3 つの controller のうち、利用者が一番よく目にするのは Service controller の仕事です。
kubectl apply から ELB にトラフィックが流れるまでを、ステップで追います。
1/6kubectl apply: type: LoadBalancer の Service が apiserver に保存される。status は空なので EXTERNAL-IP は <pending>。
EXTERNAL-IP は「LB が存在する」事実そのものではなく、controller が status に書き戻した値です。書く人が居なければ空のままです。
ステップ 5 で初めて EXTERNAL-IP が埋まることに注目してください。
kubectl get svc の <pending> は status.loadBalancer.ingress が空であることの表示で、その欄を書くのは Service controller です。
書く人が居なければ、LB の有無に関係なく空のままです。
書かれる値は、IP アドレス (ip) の場合と DNS 名 (hostname) の場合があり、AWS では DNS 名です。
もう一つ、ステップ 4 のターゲットの登録先には 2 通りあります。
cloud-provider-aws の Service controller が作るのは instance モードです。 LB のターゲットは「全ノードの NodePort」で、ノードに着いたパケットは、kube-proxy が作った規則か eBPF のプログラムによって、宛先アドレスが Pod の IP に書き換えられ (DNAT)、Pod へ送られます。 Pod が別ノードに居れば 1 ホップ余計にかかります (Part 2 の packet journey で見た経路です)。
type: LoadBalancer の Service には、ClusterIP に加えて NodePort が自動で割り当てられ、クラウドの LB はこのポートを宛先にします。
externalTrafficPolicy: Local を指定すると、ノードに着いたパケットを別ノードの Pod へ回さず、そのノードの Pod にだけ渡します。
余計なホップが無くなって送信元 IP も保たれますが、Pod の配置が偏ると負荷も偏ります。
AWS Load Balancer Controller は ip モードも作れます。 EndpointSlice を見て Pod の IP をターゲットに登録し、NLB から Pod へ直接届けます。 この controller のドキュメントは、ip モードの前提として「Pod が VPC ネイティブのネットワークを持つこと」、つまり Amazon VPC CNI が Pod に VPC のサブネットの IP (ENI のセカンダリ IP) を配っていることを挙げています。 Fargate の Pod はこのモードでしか受けられません。
手元のクラスタで Service controller の不在を見る
この教材のクラスタには kubevirt-cloud-controller-manager が居て、管理クラスタ側で動いています。 この CCM は Node controller として働き、LB 機能は無効化されています。 つまり「Node controller は居るが Service controller は居ない」世界です。 まず Node controller の仕事の跡から確かめます。
kubectl get node -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.providerID}{"\n"}{end}'
kubectl get node -o jsonpath='{.items[0].status.addresses}' | jq .
kubectl get node -o jsonpath='{.items[0].spec.taints}' ; echo出力例
tenant-xxxx-control-plane-abcde kubevirt://tenant-xxxx-control-plane-abcde
[{"address":"10.0.1.11","type":"InternalIP"},{"address":"tenant-xxxx-control-plane-abcde","type":"Hostname"}]
nullproviderID が kubevirt://… で埋まり、uninitialized taint が残っていません (出力が null か、taint の一覧に含まれない)。
EKS なら同じ場所に aws:///ap-northeast-1a/i-0123456789abcdef0 が入ります。
次に type: LoadBalancer の Service を作ります。
kubectl create deployment web --image=nginx:1.29 --port=80
kubectl expose deployment web --type=LoadBalancer --port=80
sleep 10
kubectl get svc web
kubectl get svc web -o jsonpath='{.status}' ; echo出力例
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web LoadBalancer 10.96.123.45 <pending> 80:31234/TCP 10s
{"loadBalancer":{}}<pending> のまま、status.loadBalancer は空です。
いくら待っても変わりません。
ClusterIP と NodePort は割り当てられているので、クラスタ内からの curl は普通に通ります。
Kubernetes のドキュメントも、外部 LB の無い環境では type: LoadBalancer の Service が NodePort の Service のように働くと説明しています。
kubectl get events -A --field-selector reason=EnsuringLoadBalancer
kubectl get events -A | grep -i -E 'loadbalancer|cloud-controller' || echo "(controller のイベントは無い)"
kubectl describe svc web | tail -5出力例
No resources found
(controller のイベントは無い)
Events: <none>Service controller が動いているクラスタなら、EnsuringLoadBalancer → EnsuredLoadBalancer というイベントが Service に付き、失敗すれば SyncLoadBalancerFailed に IAM のエラーなどが載ります。
ここでは何も無い。
「誰もこの Service を見ていない」状態です。
EKS で同じことをすると、コントロールプレーン側の cloud-provider-aws が数十秒で Classic LB を作り、EXTERNAL-IP に a1b2c3….ap-northeast-1.elb.amazonaws.com という DNS 名が入ります。
AWS Load Balancer Controller を入れて annotation を付ければ NLB になり、aws-load-balancer-nlb-target-type: ip なら VPC CNI が配った Pod の IP がそのままターゲットグループに並びます。
kubectl delete svc web
kubectl delete deployment webふりかえり
QEXTERNAL-IP が <pending> のとき、確実に言えることは?
<pending> は status が空であることの表示にすぎません。原因は controller が居ない、loadBalancerClass が一致しない、クラウド API がエラーを返した、など複数あり、kubectl describe svc のイベントで切り分けます。
Qnode.cloudprovider.kubernetes.io/uninitialized taint を外すのは誰?
kubelet は –cloud-provider=external のときに taint を付けて Node を登録します。CCM の Node controller がクラウドの情報を埋めて taint を外します。
QKubernetes 1.31 で起きたことは?
1.26 から feature gate で段階的に進めていた in-tree プロバイダーの分離が 1.31 で完了しました。kubelet の –cloud-provider に渡せる値は external だけです。
参考
- Cloud Controller Manager (Kubernetes Documentation, Concepts)
- Cloud Controller Manager Administration (Kubernetes Documentation, Tasks)
- Kubernetes Removals and Major Changes In v1.31 (Kubernetes Blog)
- KEP-2395: Removing In-Tree Cloud Providers (kubernetes/enhancements)
- Service (Kubernetes Documentation, Concepts)
- Service controller (cloud-provider-aws)
- Route TCP and UDP traffic with Network Load Balancers (Amazon EKS User Guide)
- Network Load Balancer (AWS Load Balancer Controller)