kubectl apply を叩いてから、Pod が起動するまでに何が起きているのか?
昨日も kubectl apply -f deployment.yaml を打ち、少し待って kubectl get pods で Running を確認したはずだ。コマンドの応答が返ってきた瞬間、もう Pod は動き始めていると思ってはいないだろうか。実際には apply の応答は「予約を受け付けた」ことしか意味しておらず、そこから複数のプロセスが互いに素性を知らないまま、バケツリレーのように処理を引き継いでいく。
kubectl applyは Pod を直接作らない。kube-apiserver経由で「望ましい状態」をetcdに書き込むだけ。- スケジューラやコントローラーは
etcdの変化を watch し、実際の状態を望ましい状態に近づけ続ける「コントロールループ」で動く。 - 実際にコンテナを起動するのはノード上の
kubeletで、apply の応答が返った時点ではまだ何も起動していないことが多い。
見た目は同期処理、中身は非同期
kubectl apply を実行すると、手元の YAML がまず現在のクラスタ上のオブジェクトとマージされ、HTTPS で kube-apiserver に送られる。apiserver はリクエストを認証(誰が送ったか)・認可(その操作が許されているか、RBAC で判定)・admission(ポリシー的に妥当か)の順に検証し、問題がなければ「望ましい状態(spec)」をオブジェクトとして保存する。
ここで押さえておきたいのは、apiserver 自身はコンテナについて何も知らないし、何も起動しないということだ。単にマニフェストを検証して保存するだけの窓口であり、kubectl コマンドが応答を受け取って終了するのはこの保存が完了した瞬間にすぎない。Pod が実際に動き出すのはここから先の話になる。
kube-apiserver と etcd — 唯一の入口と唯一の真実
Kubernetes の全コンポーネントのうち、直接 etcd に読み書きするのは kube-apiserver だけだ。scheduler・controller-manager・kubelet・kubectl はいずれも apiserver を経由してしかクラスタの状態に触れない。etcd は分散 KVS で、望ましい状態と、各ノードから報告された実際の状態(status)の両方を保持する唯一の場所になる。
各コンポーネントは etcd の変化を list-watch という API で購読(watch)している。watch がなければ「変わったか」と定期的に問い合わせる(ポーリング)しかなく、クラスタが大きくなるほど apiserver の負荷が跳ね上がる。watch であれば変更があった瞬間だけ通知が飛ぶので、負荷はクラスタの規模ではなく変更の頻度に比例する。
大きな駅の伝言板を思い浮かべてほしい。あなたは「3 番線に人を 10 人配置する」と伝言板に書き込むが、自分で誰かを呼びに行くわけではない。改札係・清掃係・警備係はそれぞれ独立して伝言板を見張っており(watch)、自分の担当に関係する書き込みを見つけたときだけ、自分の持ち場で動き出す。kubectl apply は伝言板に書き込む行為であり、etcd がその伝言板そのもの、scheduler や kubelet は独立して張り付いている担当者たちだ。
誰が Pod を作り、どのノードで動かすかを決めるのか
Deployment に apply すると、まず kube-controller-manager の中の Deployment コントローラーが反応する。「望ましい replicas 数」と「今存在する Pod 数」を比較し、足りなければ Pod オブジェクトを新規作成して apiserver に送る。この時点の Pod はまだ nodeName が空で、どのノードで動くかは決まっていない。
次に kube-scheduler が nodeName の空いた Pod を watch していて見つけると、要求リソースを満たさないノードを除外する filtering、残ったノードに得点をつける scoring を行い、最も高いスコアのノードへ nodeName を書き戻す(バインディング)。ここまでの処理はすべて etcd 上のオブジェクトを書き換えているだけで、まだどのノードでもコンテナは起動していない。
┌─────────┐
│ kubectl │ apply
└────┬────┘
▼ 認証・検証・保存
┌──────────────────┐
│ kube-apiserver │ ← 唯一の入口
└─────────┬─────────┘
▼ 書き込み
┌─────────┐
│ etcd │ ← 唯一の真実
└────┬────┘
│ watch通知(非同期)
┌──────┼───────┐
▼ ▼ ▼
sched ctrl-mgr kubelet
(bind) (Pod作成) (起動)
最後に手を動かすのは kubelet
各ノードで動く kubelet は「自分のノード名が nodeName に入っている Pod」を watch している。該当の Pod を見つけると、コンテナランタイムに CRI(Container Runtime Interface)経由でイメージの pull とコンテナ起動を指示する。起動後は kubelet がノード上の Pod の実際の状態(Running・CrashLoopBackOff 等)を定期的に apiserver へ書き戻し、これが Pod の status フィールドになる。
ここで初めて「望ましい状態(spec)」と「実際の状態(status)」が一致し、コントロールループ(望ましい状態と実際の状態の差分を検知して埋め続ける処理)が一周する。Deployment コントローラーが監視しているのは replicas 数なので、Pod が落ちたりノードが失われたりして数が合わなくなれば、誰かが手動で気づかなくても同じループが自動的にまた回り出す。中央で全部を采配する司令塔がいないからこそ、一部のコンポーネントが再起動しても他は watch を張り直すだけで自己修復できる。
apply したのに Pod が Running にならないとき、この役割分担を知っていると切り分けが速くなる。kubectl describe pod の Events で Reason: FailedScheduling が出ていれば scheduler の filtering で弾かれている(大抵はリソース不足やアフィニティ条件の不一致)。一方 Reason: Pulling や ImagePullBackOff まで進んでいれば scheduler の仕事は終わっており、詰まっているのは対象ノードの kubelet 側(イメージ名やレジストリ認証の問題)だと分かる。
scheduler の仕事(バインディング)と kubelet の仕事(起動)が別々のイベントとして記録されることを、実際のイベントログで確認する。
kind create cluster --name demo kubectl create deployment nginx --image=nginx --replicas=1 kubectl get events --field-selector involvedObject.kind=Pod --watch
数秒のうちに Reason: Scheduled(Source: default-scheduler)が先に現れ、続けて Pulling → Pulled → Created → Started(いずれも Source: kubelet)が順番に流れるはずだ。前半が scheduler、後半が対象ノードの kubelet の仕事であることが、ログの発行元(Source)の違いからそのまま見て取れる。
- kubectl apply がコンテナを直接起動している — apply は API サーバー経由で望ましい状態を etcd に保存するだけで、実際に Pod オブジェクトを作るのは Deployment コントローラー、コンテナを起動するのは対象ノードの kubelet であり、kubectl 自体は保存が終わった時点で処理を終える。
- kube-scheduler がコンテナを起動している — scheduler は「どのノードで動かすか」を決めて Pod オブジェクトの nodeName を埋める(バインディング)だけで、コンテナランタイムを呼び出すのはそのノードの kubelet の役目である。
- apply の応答が正常に返れば、もう Pod は動いている — 応答は「望ましい状態を受理した」ことを意味するだけで、実際に Running になるまでは非同期に進む。収束を確認するには
kubectl get podsやkubectl rollout statusで実際の状態を見に行く必要がある。
- kube-apiserver
- クラスタの唯一の入口。全リクエストの認証・認可・検証を行い、etcd に直接読み書きする唯一のコンポーネント。
- etcd
- 望ましい状態と実際の状態を保持する分散 KVS。クラスタの状態に関する唯一の真実の源。
- watch(list-watch)
- ポーリングせずに、対象の変更があった瞬間だけ通知を受け取る購読の仕組み。
- kube-scheduler
- nodeName が未設定の Pod を見つけ、filtering と scoring を経てどのノードで動かすかを決める(バインディング)コンポーネント。
- kubelet
- 各ノードで動くエージェント。自ノードに割り当てられた Pod のコンテナを実際に起動し、状態を apiserver に報告する。
- コントロールループ
- 望ましい状態と実際の状態の差分を検知し、それを埋め続ける非終了の処理。
- Kubernetes Component | Kubernetes — kube-apiserver・etcd・scheduler・controller-manager・kubelet それぞれの役割を公式ドキュメントで俯瞰できる。
- Controllers | Kubernetes — コントロールループの考え方と、非終了ループがなぜ「安定しなくてもよい」設計なのかが分かる。
- kube-scheduler | Kubernetes — filtering と scoring の 2 段階でノードを選ぶ具体的な流れ。