Kubernetes 解体新書Part 5 パブリッククラウドとの関係

Service に type: LoadBalancer と書くと ELB が生えるのは誰の仕事か

Kubernetes の設定が変わると、誰がクラウドを操作するのでしょうか。ノードの初期化やロードバランサーの作成を追います。

  • L1 Web ターミナル
  • 更新 2026年10月6日

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 です。

cloud-controller-manager の 3 つの controllerkube-apiserver を watch する cloud-controller-manager の中に Node controller、Route controller、Service controller があり、それぞれ EC2、VPC のルートテーブル、ELB の API を呼ぶ。kube-apiserverNode / Servicewatchcloud-controller-managerNode controllerproviderID / addresses / label を埋めるRoute controllerPod CIDR の経路を route table に書くService controllerLB を作り、ターゲットを登録するクラウド APIAWS / GCP…EC2 API (インスタンスの照会)route table の経路ELB API (LB の作成と登録)実装はクラウドごと: cloud-provider-aws / cloud-provider-gcp / cloud-provider-azure / kubevirt-cloud-controller-manager…
図 1全員 kube-apiserver を watch し、差分を見つけるとクラウドの API を呼ぶ。実装はクラウドごとに別のバイナリ。
  • 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 がノードを登録した直後です。

外部 cloud provider でのノード初期化: uninitialized taint が外れるまでkubelet が --cloud-provider=external で起動し、uninitialized taint 付きで Node を登録する。cloud-controller-manager の Node controller がクラウドに問い合わせて providerID や addresses を埋め、taint を外すと scheduler が Pod を置けるようになる。1. kubelet 起動--cloud-provider=externalNode を自分で登録する2. Node 登録taint 付き:node.cloudprovider.kubernetes.io/uninitialized=true:NoSchedule3. CCM Node controllerクラウドに問い合わせて埋める:spec.providerIDstatus.addresses, zone label4. taint を外すscheduler が Pod を置けるようになるCCM が居ないと: Node は Ready でも taint が残り、Pod は Pending のまま。AWS なら providerID は aws:///ap-northeast-1a/i-0123…、この教材のクラスタでは kubevirt://… になる。kubelet が --cloud-provider を付けずに起動した場合は taint も付かず、CCM も何もしない。
図 2kubelet は taint 付きで Node を登録し、CCM の Node controller が taint を外すまで Pod は置かれない。

順を追うと、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 にトラフィックが流れるまでを、ステップで追います。

kubectl applykube-apiserverService webtype: LoadBalancerEXTERNAL-IP: <pending>watchService controllerCCM か AWS LB ControllerAPIELB APICreateLoadBalancerstatus を更新NLBtarget group: (空)作成クライアントnode-a10.0.1.10NodePort 31234kube-proxy / eBPFPod web10.244.1.5Pod api(別の Service)node-b10.0.2.11NodePort 31234kube-proxy / eBPFPod web10.244.2.3ターゲット = ノード (NodePort)

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 通りあります。

NLB のターゲット: instance モードと ip モード上段の instance モードでは NLB が各ノードの NodePort に送り、ノード上の kube-proxy / eBPF が別ノードの Pod へ転送するため 1 ホップ余計にかかる。下段の ip モードでは NLB が Pod の VPC IP に直接送る。instance モード: ターゲット = ノード (NodePort)NLBtarget: instancenode-a10.0.1.10NodePort 31234kube-proxy / eBPF が DNATnode-b10.0.2.11web10.244.2.3nginx別ノードへ 1 ホップ余計Pod が居ないノードもターゲットになる。externalTrafficPolicy: Local で Pod が居るノードだけに絞れる。ip モード: ターゲット = Pod の IPNLBtarget: ipnode-a10.0.1.10web10.0.1.23nginxnode-b10.0.2.11web10.0.2.31nginxPod へ直接 (EndpointSlice から登録)Pod の IP が VPC の IP (ENI のセカンダリ IP) だから成り立つ。Amazon VPC CNI の前提。Fargate はこのモードだけ。
図 3instance モードはノードの NodePort が入口、ip モードは Pod の IP が入口。後者は Pod の IP が VPC の IP であることが前提。

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 の仕事の跡から確かめます。

Node に CCM が書いた情報を見る
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"}]
null

providerID が kubevirt://… で埋まり、uninitialized taint が残っていません (出力が null か、taint の一覧に含まれない)。 EKS なら同じ場所に aws:///ap-northeast-1a/i-0123456789abcdef0 が入ります。

次に type: LoadBalancer の Service を作ります。

type: LoadBalancer を作って status を見る
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 のように働くと説明しています。

Service controller の痕跡を探す
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 だけです。

参考