Kubernetes 解体新書Part 1 Kubernetes の全体像

頼んだ状態を保ち続ける仕組み

アプリの数が減ったら、代わりを作る。希望した状態を保つ仕組みと、Pod が動き始めるまでの担当を見ていきます。

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

Web アプリを 3 個動かしていて、1 個が止まったとします。 残りは 2 個です。 「3 個を保つ」と設定してあれば、Kubernetes は足りない分を作ろうとします。

この動きを支えるのが、希望した状態と現在の状態を繰り返し見比べる controller です。 この章では、数の不足に気付くところから、新しい Pod がノードで動き始めるまでを順に追います。

GOAL この章のゴール

  • Pod の数が減ったとき、代わりが作られる流れを説明できる
  • 配置を決める担当と、コンテナを起動する担当を区別できる
  • Pod を削除して、数が戻る様子を確かめられる

差を見張り続ける controller

エアコンの温度調節を考えます。 設定温度を 25 度にしておくと、エアコンは室温を測り、25 度より高ければ冷やし、低ければ止めます。 設定したあとは、人が何もしなくても、エアコンが差を見て動き続けます。

controllerコントローラー特定の種類のオブジェクトを apiserver 越しに見張り、あるべき状態 (spec) と現在の状態の差を見つけたら、それを埋める変更を apiserver に頼むプログラム。用語集で見る は、Kubernetes におけるこのサーモスタットです。 一度だけ指示して終わるのではなく、設定と現実を繰り返し見比べます。

Pod の数を保つ担当の例を見ます。 「同じ Pod を決まった数だけ保つ」ことを依頼するためのマニフェストの種類があり、それを ReplicaSetレプリカセット同じ Pod のひな形 (template) と台数 (replicas) と、自分の Pod を見分ける selector を持ち、その台数の Pod を保つリソース。用語集で見る と呼びます。 ドキュメントによると、ReplicaSet は「どの Pod を自分のものとみなすかを決める selector、保つべき Pod の数を示す replicas、数を満たすために作る新しい Pod のひな形 (template) で定義され、必要に応じて Pod を作ったり消したりして、望む数に到達させる」ものです。 次のマニフェストは、「このひな形の Pod を 3 つ」という依頼書です。

Pod を 3 つ保つ ReplicaSet を作る
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: web-rs
  labels: { app: web-rs }
spec:
  replicas: 3
  selector:
    matchLabels: { app: web-rs }
  template:
    metadata:
      labels: { app: web-rs }
    spec:
      containers:
        - name: nginx
          image: nginx:1.29.0
EOF
kubectl wait --for=jsonpath='{.status.readyReplicas}'=3 replicaset/web-rs --timeout=180s
kubectl get replicaset,pods -l app=web-rs

出力例

replicaset.apps/web-rs created
NAME                     DESIRED   CURRENT   READY   AGE
replicaset.apps/web-rs   3         3         3       5s
NAME               READY   STATUS    RESTARTS   AGE
pod/web-rs-abcde   1/1     Running   0          5s
pod/web-rs-fghij   1/1     Running   0          5s
pod/web-rs-klmno   1/1     Running   0          5s

書いたのは replicas: 3 と Pod のひな形だけです。 Pod を 3 つ作ったのは、ReplicaSet を担当する controller (ReplicaSet controller) です。 selector の app: web-rs は、「この ReplicaSet が数える Pod はラベル app=web-rs を持つもの」という目印で、ひな形に付けるラベルと一致している必要があります。 作られた Pod には、どの ReplicaSet のものかを示す ownerReferences という欄が付きます。

reconciliation loop を壊して確かめる

ReplicaSet controller が行っているのは、次の繰り返しです。 ラベルが一致する Pod の数 (現在の状態) を数え、replicas (あるべき状態) と比べ、足りなければ Pod を作り、多ければ消します。 一致していれば何もしません。 これを、関係するオブジェクトが変わるたびに繰り返します。 この繰り返しを reconciliation loopリコンシリエーションループあるべき状態 (spec) と現在の状態を見比べて、差分を埋める動作を繰り返す仕組み。用語集で見る と呼びます。

reconciliation loop: あるべき状態 (spec) と現在の状態 (status) を見比べ、差分があれば埋める動作を繰り返す左に spec (replicas: 3)、右に現在の状態 (ラベルの一致する Pod が 2 つ)。中央の controller が両方を apiserver から読み、差分 1 を見つけて Pod を 1 つ作るよう apiserver に頼む矢印を出す。下にループを示す矢印。あるべき状態 (spec)replicas: 3利用者が YAML で宣言ReplicaSet controller両方を apiserver から読んで比べる差分: 1 足りない→ Pod を 1 つ作るよう頼む現在の状態app=web の Pod: 2apiserver にある Pod を数える読む読むPOST Pod状態が変わったらまた比べる (永遠に)一致していれば何もしない。これを全リソースについて並行して回している。
図 1reconciliation loop。あるべき状態 (spec) と現在の状態を見比べ、差分があれば埋める。一致していれば何もしない。

言葉で聞くより、壊したほうが早いです。 下のシミュレータで Pod を kill してみてください。 少し遅れて controller が作り直します。 スライダで replicas を減らすと、今度は多すぎる分を消します。 controller のトグルを切ると、kill した Pod は戻ってきません。

desired 3observed 3一致
  • web-00000Running
  • web-00001Running
  • web-00002Running
  1. controller: 監視を開始しました

controller を止めると、kill した Pod は戻ってきません。これが「宣言した状態を誰かが見張り続ける」ことの意味です。

同じことを実機でやります。 1 つ消して、-w (変化が起きるたびに行を足して表示するオプション) で戻ってくるのを見ます。 -w は止まらないので、新しい Pod が Running になったら Ctrl-C で抜けてください。

Pod を消して、戻ってくるのを見る
kubectl delete pod $(kubectl get pod -l app=web-rs -o jsonpath='{.items[0].metadata.name}') --wait=false
kubectl get pods -l app=web-rs -w

出力例

pod "web-rs-abcde" deleted
NAME             READY   STATUS              RESTARTS   AGE
web-rs-abcde     1/1     Terminating         0          2m
web-rs-xyz12     0/1     Pending             0          0s
web-rs-xyz12     0/1     ContainerCreating   0          0s
web-rs-xyz12     1/1     Running             0          2s

名前が 1 つ変わって、数は 3 のままです。 ReplicaSet には触っていません。 ReplicaSet controller が「3 に対して、消えていない Pod は 2」という差を見て、1 つ作っただけです。 誰が消したかは関係ありません。 手で消しても、ノードが落ちても、同じループが回ります。

この controller は、Pod を自分で起動しているわけではありません。 apiserver に「Pod を 1 つ作ってほしい」と頼み、Pod オブジェクトが保存されるところまでが仕事です。 Kubernetes のドキュメントは Job controller を例に、「Job controller は自分では Pod もコンテナも動かさない。代わりに、API サーバーに Pod の作成や削除を頼む。コントロールプレーンの他のコンポーネントがその新しい情報に反応し (スケジュールして動かすべき新しい Pod がある)、やがて仕事が終わる」と説明しています。 Kubernetes には、こうした小さな controller が、Deployment や ReplicaSet、Node、Job など、種類ごとに多数あります。 ドキュメントは「1 つの大きな制御ループより、それぞれがクラスタの状態の一面だけを扱う単純な controller をたくさん持つ」のが設計の原則で、「controller は失敗しうるので、Kubernetes はそれを許容するように設計されている」と書いています。 組み込みの controller は、まとめて kube-controller-manager という 1 つのプログラムの中で動いています (「論理的には別々のプロセスだが、複雑さを減らすため 1 つのバイナリにまとめて 1 つのプロセスで動かす」)。

では、Pod オブジェクトが保存されたあと、実際にコンテナを起動するのは誰でしょうか。

コンテナを起動する担当

Pod オブジェクトが apiserver に保存されただけでは、まだコンテナは 1 つも動いていません。 保存されたのは「この Pod が居てほしい」という記録であり、しかも、どのノードで動かすかはまだ決まっていません。 そこから実際の起動までに、2 つの担当が働きます。

1 つ目は kube-scheduler です。 ドキュメントの定義では「まだノードが割り当てられていない新しい Pod を見張り、見つけた Pod ごとに、動かすのに最適なノードを探す責任を持つ」コンポーネントです。 選び方は 2 段階で、まず資源の要求や制約を満たすノードだけに絞り (filtering)、残ったノードに点数を付けて最高点のノードを選びます (scoring)。 選んだ結果は「binding と呼ばれる手続きで API サーバーに通知する」だけで、scheduler 自身は起動しません。 条件を満たすノードが 1 つも無ければ、「その Pod は、scheduler が置けるようになるまでスケジュールされないまま」になります。

2 つ目が kubeletキューブレット各ノードで動くエージェント。自分のノードに割り当てられた Pod を apiserver から見つけ、コンテナランタイム (containerd など) にコンテナの起動を頼み、結果を Pod の status に書き戻す。用語集で見る です。 ドキュメントの定義では「クラスタの各ノードで動くエージェントで、Pod の中でコンテナが動いていることを確実にする」もので、「渡された PodSpec に書かれたコンテナが動いていて健全であることを保証する。Kubernetes が作っていないコンテナは管理しない」とあります。 kubelet は、自分のノードに割り当てられた Pod を見つけると、1 章に出てきた containerd にイメージの取得とコンテナの作成を頼みます。 コンテナが動き出したら、その様子を Pod の status に書き戻します。 前の章で読んだ status.phase と status.podIP を書いたのも、kubelet です。

ノード自身も、kubelet が apiserver に登録します。 ドキュメントによると、既定では「kubelet が自分自身を API サーバーに登録する」方式で、登録後は、Node オブジェクトの status の更新と Lease オブジェクトの更新 (既定で 10 秒ごと) という 2 種類のハートビートを送り続けます。 ハートビートが途絶えると、kube-controller-manager の中の node controller がその Node の Ready 条件を Unknown にし、既定ではその 5 分後から、そのノード上の Pod の立ち退きを始めます。 2 章で「落ちたホストに気付くのもプログラムの仕事」と書いたのは、この仕組みのことです。

登場人物を並べる

Kubernetes のアーキテクチャ: コントロールプレーンとノードkube-apiserver を中心に etcd、scheduler、controller-manager、cloud-controller-manager が並び、各ノードの kubelet と kube-proxy が apiserver と通信する。コントロールプレーンkube-apiserver唯一の入口 (REST / watch)etcd状態の保存schedulerPod → Nodecontroller-managerreconcile 群cloud-controller-managerクラウド連携読み書きscheduler / controller も apiserver を watch し、結果を書き戻すクラウド APINode A10.0.1.10kubeletkube-proxy任意containerdPod10.244.1.5appPod10.244.1.6weblogCRI で Pod を起動Node B10.0.1.11kubeletkube-proxy任意containerdPod10.244.2.3dbPod10.244.2.4?watch / status を書くkubelet / kube-proxy は apiserver としか話さない
図 2管理側 (コントロールプレーン) とノード。kubelet も kube-proxy も、クラスタの管理情報は kube-apiserver から受け取る。kube-proxy と cloud-controller-manager は無くても動く。

ここまでで、Kubernetes を動かしているプログラムが出揃いました。 クラスタの全体を管理する側を コントロールプレーンコントロールプレーンクラスタ全体の状態を保存し、何をどこで動かすかを決めるプログラムの集まり。apiserver、etcd、scheduler、controller-manager など。用語集で見る と呼びます。 ドキュメントによると、コントロールプレーンは「クラスタ全体に関わる決定 (たとえばスケジューリング) を行い、クラスタのイベント (たとえば Deployment の replicas が満たされていないときに新しい Pod を起動する) を検出して応答する」側です。 Kubernetes のドキュメントの「Kubernetes Components」のページに沿って並べます。

  • kube-apiserver: 「Kubernetes の HTTP API を公開する中核のサーバー」。kubectl も kubelet も、ここに話しかけます。
  • etcd: 「API サーバーの全データのための、一貫性があり高可用な key-value ストア」。
  • kube-scheduler: 「まだノードに紐付いていない Pod を探し、それぞれに適したノードを割り当てる」。
  • kube-controller-manager: 「Kubernetes API の振る舞いを実装する controller 群を動かす」。
  • cloud-controller-manager (任意): 「基盤となるクラウドプロバイダーと統合する」。ノードの情報取得、ロードバランサーの作成、経路の設定を担当します。ドキュメントは「自前の設備や学習用の PC の上で動かす場合、クラスタには cloud-controller-manager は無い」と書いています。この教材のクラスタでは KubeVirt 用のものが管理クラスタ側で動いています。
  • kubelet: 「Pod とそのコンテナが動いていることを確実にする」。ノードごとに 1 つ。
  • kube-proxy (任意): 「Service を実装するためのネットワークルールをノード上で維持する」。Service は次の章で導入します。ドキュメントは「Service のパケット転送を自前で実装するネットワークプラグインを使う場合、kube-proxy を動かす必要は無い」と書いていて、この教材のクラスタでは Cilium がその役を担い、kube-proxy は居ません。
  • コンテナランタイム: 「コンテナを動かす責任を持つソフトウェア」。containerd、CRI-O など。

AWS の語彙に寄せると、apiserver と etcd が「API と状態」、scheduler が「配置の判断」、kube-controller-manager が「Auto Scaling グループのような見張り役の集合」、kubelet が「各インスタンスのエージェント」です。 EKS は管理側を AWS が運用し、ノード側は利用者が (あるいは Auto Mode で AWS が) 運用します。

実運用で書く Deployment

ReplicaSet は、Pod の数を保つ担当です。 ただし、ひな形の image を nginx:1.29.0 から 1.30 に変えたい場合に、Pod を入れ替える手順までは持っていません。 イメージの更新は、実運用で必ず起きます。 そこで、ReplicaSet の 1 つ上に、世代を管理するリソースが置かれています。 それが DeploymentデプロイメントReplicaSet を世代ごとに作り、更新のときに新旧を切り替えるリソース。実運用でコンテナを動かすときに書く基本のリソース。用語集で見る です。 ドキュメントは「Deployment にあるべき状態を書くと、Deployment controller が実際の状態を制御された速さであるべき状態に変える」と説明し、ReplicaSet については「ReplicaSet を直接使うのではなく Deployment を使うことを勧める」「ReplicaSet オブジェクトを自分で操作する必要は無いかもしれない」と書いています。 Deployment を書くと、Deployment を担当する controller が ReplicaSet を作り、その ReplicaSet を担当する controller が Pod を作ります。 入れ子の中身は、次の章で見ます。 この章では、これまでと同じ流れが 1 段増えるだけだと考えれば十分です。

先に作った ReplicaSet を片付け、Deployment を作ります。

ReplicaSet を消して、Deployment を作る
kubectl delete replicaset web-rs
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels: { app: web }
  template:
    metadata:
      labels: { app: web }
    spec:
      containers:
        - name: nginx
          image: nginx:1.29.0
EOF

ここで書いたのは replicas: 3 だけです。 どのノードで、いつ、どうやって起動するかは書いていません。 それを決めるのが、上に並べたプログラムたちです。 kubectl apply は差分を送るので、同じ YAML を何度流しても結果は同じです (replicas を 5 に変えて流せば、5 になります)。

apply から Pod が走るまで

全員が kube-apiserver を見ている: scheduler も controller も kubelet も、互いには話さず apiserver を watch する中央に kube-apiserver と etcd。周囲に 6 つのコンポーネントが並び、それぞれから apiserver へ両方向の矢印が出る。コンポーネント同士を結ぶ線は無い。kube-apiserverREST + watchetcdkubectlあなたschedulernodeName 空の Podcontroller-managerDeployment / RS / …kubelet自分宛の Podcloud-controller-managerNode などCilium agent通信の宛先横の線が 1 本も無い。だから 1 つが落ちても他は動き続け、再起動すれば続きから動ける。
図 3全員が apiserver を見ている。コンポーネント同士を結ぶ線は 1 本も無い。

kubectl apply から Pod が Running になるまでを、メッセージ単位で追います。 下のシーケンス図を 1 つずつ進めてください。

kubectl apply から Pod 起動までのシーケンス。現在: POST Deploymentkubectlあなたapiserver入口etcd保存Deploy ctrlcontroller-mgrRS ctrlcontroller-mgrscheduler置き場所kubeletノードcontainerdCRIPOST Deployment1write2watch: Deployment added3POST ReplicaSet (replicas: 3)4watch: ReplicaSet added5POST Pod ×3 (nodeName: 空)6watch: Pod (nodeName 空)7POST Binding (nodeName)8watch: 自分宛の Pod9CRI: RunPodSandbox / PullImag…10PATCH Pod status: Running11

Deployment: 無し → 作成要求

kubectl が YAML を JSON にして apiserver に送る。認証、認可、admission を通る。

1/11POST Deployment

点線は watch による通知。実線は apiserver への書き込みか、kubelet から containerd への CRI 呼び出し。controller 同士は一度も直接話していない。

文章でも並べておきます。

  1. kubectl が apiserver に Deployment を POST する

    認証、認可、admission (保存前にオブジェクトを検査し、必要なら書き換える段階) を通って etcd に保存されます。この時点では Pod は 1 つもありません。
  2. Deployment controller が ReplicaSet を作る

    「Deployment があるのに ReplicaSet が無い」という差分を埋めます。Pod は作りません。
  3. ReplicaSet controller が Pod を 3 つ作る

    Pod オブジェクトができますが、nodeName は空です。
  4. scheduler がノードを決める

    nodeName が空の Pod を見つけ、filtering と scoring でノードを選び、binding で Pod の nodeName に書き込みます。
  5. kubelet が自分宛の Pod を見つけて起動する

    CRI 経由で containerd にイメージの取得とコンテナ作成を頼み、結果を Pod の status に書き戻します。

5 つのステップのどこにも、「次のコンポーネントを呼ぶ」処理が無いことに注目してください。 全員が apiserver を watch して、自分の担当が変わったら動きます。 watch は Kubernetes API の機能で、ドキュメントの説明では「クライアントは最初にオブジェクトや一覧を取得し、それ以降の変更を追跡できる。watch 要求を送ると、API サーバーは変更の流れを返し続ける」というものです。 ドキュメントは通信の形を「ハブアンドスポーク」と呼び、「ノード (とその上の Pod) からの API 利用はすべて API サーバーで終端する。他のコントロールプレーンのコンポーネントは、リモートサービスを公開するようには設計されていない」と書いています。 controller 同士は互いの存在を知りません。 直接呼び合わないので、どれかが落ちても、再起動して apiserver を見直せば続きから動けます。 scheduler が 1 分止まっていても、その間に作られた Pod は Pending のまま待っていて、scheduler が戻れば配置されます。

「Docker 非推奨」騒動の中身

kubelet の説明に出てきた「コンテナランタイムに起動を頼む」部分には、歴史があります。 3 章で予告した、2020 年の「Kubernetes が Docker を非推奨にした」騒動の中身を、ここで見ます。

Kubernetes は Docker で動くことを前提に公開されました。 2016 年 7 月の 1.3 で CoreOS の rkt が使えるようになり、「ランタイムは差し替えられる」ことが前提になります。 同年 12 月の 1.5 で、kubelet とランタイムの間に gRPC のインターフェース (CRI、Container Runtime Interface) が alpha として定義されました。 Kubernetes のドキュメントの言葉では「Docker Engine はそのインターフェース (CRI) を実装していないので、Kubernetes プロジェクトは移行を助ける特別なコードを作り、その dockershim というコードを Kubernetes 自身の一部にした」のです。

一方 Docker 側は、2017 年 3 月に中核のランタイム containerd を CNCF に寄贈し、翌月、Docker 自体を部品の集合 (Moby Project) として再編しました。 containerd は CRI を直接話せます。 この時点で、kubelet から見れば「containerd と直接話せばよく、dockerd は間に挟まっているだけ」という状態になっています。 Kubernetes のブログは 2020 年 12 月に「クラスタは本当に必要な containerd に辿り着くために dockershim という別の道具を使わなければならない。それは保守すべきもの、壊れうるものがもう 1 つ増えるということで、よくない」と説明しています。

dockershim が消えて何が変わったか: 1.23 まで kubelet は dockershim 経由で dockerd と話し、その先で containerd が動いていた。1.24 からは kubelet が containerd と直接話す左は 1.23 までのスタック (kubelet → dockershim → dockerd → containerd → runc)。右は 1.24 以降 (kubelet → containerd → runc)。イメージは両方とも OCI で同じ。〜1.23 (dockershim あり)kubeletdockershim (CRI → Docker API)dockerdcontainerdrunc1.24〜 (CRI 直結)kubeletCRI (gRPC)containerd (または CRI-O)runc消えたのは中間の 2 層。イメージは OCI 形式で共通なので、docker build したものはそのまま動く。
図 41.23 までと 1.24 以降のスタック。消えたのは kubelet と containerd の間の 2 層で、イメージは OCI 形式のまま。

2020 年 12 月の 1.20 で dockershim が非推奨になり、「Kubernetes が Docker を非推奨にした」と騒がれました。 実際に非推奨になったのは kubelet 内の変換層です。 2022 年 5 月の 1.24 で削除され、以降 kubelet は CRI 経由で containerd や CRI-O と直接話します。 Kubernetes の FAQ は「docker build で作ったイメージは、すべての CRI 実装で動く。既存のイメージはすべて、まったく同じように動く」「自分の PC で Docker を使ってコンテナを開発、テストしているなら、何も変わらない」と明言しています。 変わったのはノード上で動くデーモンの構成で、開発者のワークフローは変わっていません。 Docker Engine をどうしてもノードで使いたい場合は、Mirantis と Docker が保守する cri-dockerd を外付けします。

後片付け

次の章でも Deployment を使うので、消さずに残しておいて構いません。 消す場合は次の通りです。

Deployment を消す
kubectl delete deployment web

ふりかえり

Qkubelet が直接通信する相手はどれ?

選択肢の中では apiserver です。kubelet はほかに、ノード上のコンテナランタイムとも通信します。etcd に触れるのは apiserver です。

QReplicaSet が管理する Pod を手で消したとき、作り直すのは誰?

ReplicaSet controller が「Pod が足りない」差分を見つけて新しい Pod を作ります。scheduler はその Pod の置き場所を決めるだけです。

Qscheduler が 1 分間止まっていたら、その間に apply された Deployment はどうなる?

controller は apiserver を watch しているだけなので、scheduler が居なくても ReplicaSet controller は Pod を作ります。nodeName が空のまま待ち、scheduler が戻れば続きから処理されます。

Q1.24 で dockershim が削除されて、変わらなかったものは?

イメージは OCI 形式で共通なので、そのまま動きます。変わったのは kubelet が containerd と直接話すようになったことです。

参考