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

ハンズオン: 自分のクラスタの「クラウド」を観察する

Node や Service の情報を読み、クラスタと基盤の接点を探します。外部アドレスが決まらない状態も観察します。

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

自分のクラスタのノードは、どの基盤の上で動いているのでしょうか。 Node の情報や、Service に記録された出来事から、その手がかりを探せます。

この章では Web ターミナルを使い、クラウド連携に関わる設定と状態を読みます。 外部アドレスが決まらない Service も作り、担当するプログラムがいないと何が起きるかを確かめます。

GOAL この章のゴール

  • Node に記録された、基盤に関する情報を読める
  • Service の状態とイベントから、外部公開の進み具合を確認できる
  • クラスタ自体を Kubernetes のリソースとして管理する仕組みが分かる
環境の状態を確認しています…

手元のクラスタの構造

実習用のノードは、運営側のクラスタ上で動く VM です。 自分のクラスタと、それを動かす管理側のクラスタの関係を図で見ます。

この教材のクラスタ: 管理クラスタの上に KubeVirt VM で建つ、あなた専用のクラスタ上段は管理クラスタで、Cluster API、KubeVirt、kubevirt-cloud-controller-manager、kubevirt-csi-driver が動く。下段はあなたのクラスタで、KubeVirt の VM が 1 台の Node になり、kubelet、Cilium、ワークロードが動く。CCM は管理クラスタ側から VM を見て Node の providerID を埋める。管理クラスタ (kube-classroom が運用。あなたからは見えない)Cluster APICluster / Machine の CRKubeVirtVirtualMachine → Pod 内の VMkubevirt-cloud-controller-managerNode controller のみ有効kubevirt-csi-driverPVC を管理側の PV に対応管理クラスタのノード (物理 / VM)VM はここの Pod として動く (KubeVirt の virt-launcher)VM の情報を見て Node を初期化あなたのクラスタ (handson-<user>、Kubernetes v1.37.1、1 ノード)Node = KubeVirt VM (Ubuntu 26.04)providerID: kubevirt://…kubeletcontainerdCilium 1.20CNI + kube-proxy 代替 + Gatewaycorednscorednsworkbenchzshあなたの Podappcontrol-plane兼 workerEKS に置き換えると: 管理クラスタ = AWS のアカウント、KubeVirt VM = EC2、kubevirt CCM = cloud-provider-aws。
図 1管理クラスタの上で KubeVirt が VM を動かし、その VM が手元のクラスタの Node になる。CCM と CSI は管理クラスタ側から VM を見ている。

この VM を動かしているのは KubeVirtキューブバートKubernetes の上で VM を動かす仕組み。用語集で見る です。 EKS に置き換えると、管理クラスタが「AWS 管理のアカウント」、KubeVirt の VM が「EC2」、kubevirt-cloud-controller-manager が「cloud-provider-aws」、kubevirt-csi-driver が「aws-ebs-csi-driver」に当たります。 EKS との違いはコントロールプレーンの置き場所で、ここではコントロールプレーンも VM の中 (kubeadm で構築、1 ノードで control-plane 兼 worker) にあります。 2 章目の ManagedBoundary で言う「自前 (EC2 + kubeadm)」に、CCM と CSI だけ管理側から供給されている構成です。

バージョンとノードを確認する
kubectl version
kubectl get nodes -o wide

出力例

Server Version: v1.37.1
NAME                               STATUS   ROLES           AGE   VERSION   INTERNAL-IP   OS-IMAGE             CONTAINER-RUNTIME
tenant-xxxx-control-plane-abcde    Ready    control-plane   3h    v1.37.1   10.0.1.11     Ubuntu 26.04.1 LTS   containerd://2.x

Node に残った CCM の指紋

3 章目で、Node controller が providerID と addresses を書き、uninitialized taint を外すと説明しました。 Kubernetes のドキュメントの言葉では、Node controller は「クラウドの API から得たサーバーの一意な ID で Node を更新し、リージョンやリソースのようなクラウド固有の情報を label と annotation に付け、ホスト名とネットワークアドレスを得る」部品です。 その跡を場所を決めて見に行きます。

この章で見る場所: CCM の痕跡は Node と Service のどこに残るかNode の spec.providerID、status.addresses、labels (topology.kubernetes.io/zone など)、spec.taints と、Service の status.loadBalancer、Events (EnsuringLoadBalancer) のうち、この教材のクラスタで埋まる場所と空のままの場所を示す。Nodespec.providerIDkubevirt://… (Node controller が書く)status.addressesInternalIP / Hostnamemetadata.labelstopology.kubernetes.io/zone は provider 次第spec.taintsuninitialized が無い = 初期化済みService (type: LoadBalancer)status.loadBalancer.ingress空のまま → EXTERNAL-IP <pending>EventsEnsuringLoadBalancer が出ないspec.clusterIP / nodePortapiserver が割り当てる。CCM は無関係EndpointSliceendpointslice controller が作る緑 = 埋まる (Node controller / 本体の controller の仕事)。赤 = 空のまま (Service controller が居ない)。
図 2緑の場所は埋まる。赤の場所は空のまま。この差が「Node controller は居て、Service controller は居ない」の証拠。
providerID / addresses / labels / taints を一度に見る
NODE=$(kubectl get node -o jsonpath='{.items[0].metadata.name}')
kubectl get node "$NODE" -o json | jq '{
  providerID: .spec.providerID,
  addresses: .status.addresses,
  taints: .spec.taints,
  labels: (.metadata.labels | with_entries(select(.key | test("topology|instance-type|kubernetes.io/(hostname|os|arch)"))))
}'

出力例

{
  "providerID": "kubevirt://tenant-xxxx-control-plane-abcde",
  "addresses": [
    { "address": "10.0.1.11", "type": "InternalIP" },
    { "address": "tenant-xxxx-control-plane-abcde", "type": "Hostname" }
  ],
  "taints": null,
  "labels": {
    "kubernetes.io/arch": "arm64",
    "kubernetes.io/hostname": "tenant-xxxx-control-plane-abcde",
    "kubernetes.io/os": "linux"
  }
}

出力を上から読みます。

  • providerID は kubevirt://<VM 名> で埋まっています。この値を書いたのは CCM の Node controller で、管理クラスタの VirtualMachine を見て書いています。EKS なら aws:///ap-northeast-1a/i-0123456789abcdef0 という形で、i-… の部分がそのまま EC2 のインスタンス ID です。
  • taints に node.cloudprovider.kubernetes.io/uninitialized がありません。CCM が初期化を終えて外した証拠です。
  • topology.kubernetes.io/zone や node.kubernetes.io/instance-type の label は、provider が埋める場合と埋めない場合があります。EKS では zone に ap-northeast-1a、instance-type に m6g.large のような値が入り、scheduler の topology spread や StorageClass の allowedTopologies がそれを使います。この環境で空なら、kubevirt CCM がそこまで埋めていないということです。
外部 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 も何もしない。
図 3もう一度、初期化の流れ。手元の Node は 4 番まで進んでいる。

CSI 側の登録も同じ場所で確かめられます。

CSI の Node plugin の登録を見る
kubectl get node "$NODE" -o jsonpath='{.metadata.annotations}' | jq 'with_entries(select(.key | test("cloud|kubevirt|csi")))'
kubectl get csinodes "$NODE" -o jsonpath='{.spec.drivers[*].name}' ; echo

出力例

{
  "csi.volume.kubernetes.io/nodeid": "{\"csi.kubevirt.io\":\"tenant-xxxx-control-plane-abcde\"}"
}
csi.kubevirt.io

CSI の Node plugin は csi.kubevirt.io として登録されています。 EKS なら ebs.csi.aws.com と efs.csi.aws.com が並ぶ場所です。

pending のままの LoadBalancer

3 章目で、Service controller の不在を <pending> で確かめました。 今度は時間を追って、「何が起きないか」を記録します。

Deployment と LoadBalancer Service を作る
kubectl create deployment hello --image=nginx:1.29 --port=80
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: hello
spec:
  type: LoadBalancer
  selector:
    app: hello
  ports:
    - port: 80
      targetPort: 80
EOF
kubectl get svc hello -w &
sleep 20; kill %1

出力例

NAME    TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
hello   LoadBalancer   10.96.45.67    <pending>     80:30987/TCP   0s

出力は、20 秒待っても 1 行だけです。 -w は status が更新されたときに新しい行を出すので、1 行だけということは status が一度も更新されていないということです。 EKS では同じ -w で、数十秒後に EXTERNAL-IP が DNS 名に変わる行がもう 1 行出ます。

Events と status、そして使える部分を確かめる
kubectl get events --field-selector involvedObject.name=hello,involvedObject.kind=Service
kubectl get svc hello -o jsonpath='{.status}' ; echo
kubectl get endpointslices -l kubernetes.io/service-name=hello
kubectl run -it --rm probe --image=curlimages/curl:8.11.0 --restart=Never -- curl -s -o /dev/null -w '%{http_code}\n' http://hello

出力例

No resources found in default namespace.
{"loadBalancer":{}}
NAME          ADDRESSTYPE   PORTS   ENDPOINTS    AGE
hello-abc12   IPv4          80      10.244.0.23  1m
200

この出力から 4 つの事実が読めます。

  1. Events が無い。Service controller が居れば EnsuringLoadBalancer → EnsuredLoadBalancer (または SyncLoadBalancerFailed) が記録されます。何も無いのは、誰もこの Service を reconcile していないからです。
  2. status.loadBalancer が空。だから <pending> です。
  3. EndpointSlice はある。これは kube-controller-manager の endpointslice controller の仕事で、CCM とは無関係です。
  4. ClusterIP で届く。ClusterIP 宛ての通信を Pod へ送り届ける仕組み (この環境では Cilium の eBPF) も CCM とは無関係です。

Service の転送はすべて動いていて、欠けているのは外からの入口を用意する Service controller だけです。 Kubernetes のドキュメントが「外部 LB の無い環境では type: LoadBalancer は NodePort と同じように働く」と書いている通りの状態です。

片付け
kubectl delete svc hello
kubectl delete deployment hello

クラスタ自体を宣言する Cluster API

最後に、手元のクラスタがどうやって建ったかを見ます。 管理クラスタ側の話なので、ここは図で追います。

Cluster API のリソース: クラスタ自体を Kubernetes のオブジェクトとして宣言する管理クラスタに Cluster、KubeadmControlPlane、MachineDeployment、Machine という CR があり、それぞれ infrastructure provider 固有の CR (KubevirtCluster、KubevirtMachine、または AWSCluster、AWSMachine) と対になる。controller がそれを見て VM や EC2 を作り、kubeadm でノードとして参加させる。汎用 (cluster.x-k8s.io)Clusterクラスタ 1 つKubeadmControlPlanecontrol-plane ノードの数と設定MachineDeploymentworker の Deployment 相当Machineノード 1 台infrastructure provider (この教材)KubevirtClusterKubevirtMachine→ VirtualMachine → VMinfrastructure provider (AWS)AWSClusterVPC / subnet / SGAWSMachine→ EC2 RunInstancesEKS を作りたいならAWSManagedControlPlane(コントロールプレーンを EKS に委ねる)eksctl も CloudFormation を使って同じことを宣言的にやる
図 4Cluster API: クラスタ 1 つ、control-plane、worker、ノード 1 台がそれぞれ CR。infrastructure provider の CR と対になり、controller が VM や EC2 を作る。

Cluster APIクラスターエーピーアイクラスタそのものを Kubernetes のリソース (Cluster、Machine など) として宣言し、controller が VM や EC2 を作る仕組み。用語集で見る のドキュメントは、関係する部品を次のように定義しています。 Cluster API とその provider が動き、他のクラスタの一生を管理するクラスタが management cluster (管理クラスタ)。 管理される側のクラスタが workload cluster で、Cluster という CR で表されます。 Machine は「Kubernetes の Node を載せるインフラ (たとえば VM) の宣言的な spec」で、MachineDeployment は Kubernetes の Deployment と同じ要領で Machine の集合を更新します。 VM やネットワークを実際に作るのが infrastructure provider、サーバーを Kubernetes のノードに仕立てるのが bootstrap provider (kubeadm)、コントロールプレーンの Machine を束ねるのが KubeadmControlPlane です。

kube-classroom は、利用者の handson-<user> namespace に Cluster を作ります。 Cluster API の controller がそれを見て KubeadmControlPlane と Machine を作り、infrastructure provider (この環境では cluster-api-provider-kubevirt) が KubevirtMachine から KubeVirt の VirtualMachine を作り、VM が起動すると cloud-init で kubeadm が走ってノードになります。 kubeconfig は <cluster>-kubeconfig という Secret に置かれ、Web ターミナル はそれを読んで kubectl を通しています。

Part 1 の reconciliation loop が、ここでは「クラスタそのもの」を対象に回っています。 VM を消せば Machine controller が気づいて作り直し、MachineDeployment の replicas を増やせばノードが増えます。 EKS を Cluster API で建てる場合は AWSManagedControlPlane という CR を使い、コントロールプレーンの部分を EKS に委ねます。 eksctl が CloudFormation で、Terraform が state ファイルでやっていることを、Kubernetes の CR と controller でやる、というのが Cluster API です。

Part 5 のまとめ

  • クラウドのサービスは「どの層を誰が管理するか」の物差しで並ぶ。マネージド Kubernetes の最低ラインはコントロールプレーンと etcd で、ノード / CSI / LB 連携の境界はサービスごとに違う。
  • type: LoadBalancer から LB が生えるのは Service controller の reconcile。EXTERNAL-IP は controller が status に書いた値で、書く人が居なければ pending のまま。
  • EKS は Kubernetes の拡張点に AWS の OSS を差し込んだもので、部品はリポジトリ名で呼べる。Auto Mode はその多くを AWS 側に移した。
  • Fargate は task / Pod ごとの microVM。その下の Firecracker と、Google の gVisor は、同じ「他人のコードを同居させる」問題への別の答えで、境界を置く層が違う。
  • 手元のクラスタも同じ構造を持っている。providerID、taint、Events、status を読めば、どのクラウドの上で、誰が何をしているかが分かる。

ふりかえり

Qこの教材のクラスタで providerID が kubevirt:// で埋まっているのは誰の仕事?

kubelet は –cloud-provider=external で起動し、uninitialized taint 付きで Node を登録するだけです。providerID と addresses を埋めて taint を外すのは CCM の Node controller で、この環境では管理クラスタ側で動いています。

Qtype: LoadBalancer の Service が pending のまま、しかし ClusterIP では届く。この状態から言えることは?

EndpointSlice は endpointslice controller、ClusterIP の転送は kube-proxy や Cilium の仕事で、どちらも CCM とは無関係に動きます。欠けているのは status.loadBalancer を書く Service controller だけです。

QEKS で kubectl get clusters が何も返さないのはなぜ?

クラスタ自体の定義は Kubernetes の外 (EKS の API、CloudFormation、または管理クラスタの Cluster API) にあります。自分のクラスタの kubeconfig からは見えません。Cluster API で EKS を建てることもできますが、その CR も管理クラスタ側にあります。

参考

読み切ると称号を獲得

雲の裏側を知る者

この Part を読み切りました。

称号一覧