はじめに
Deployment を apply してから Pod が Ready になるまで という記事で、kube-apiserver・各コントローラー・kube-scheduler・kubelet・kube-proxy を跨いだ全体の流れを整理しました。その中の「kubelet が Pod を検知してから Ready になるまで」の部分は、実際には initContainer の実行と3種類の probe (startupProbe/readinessProbe/livenessProbe) が絡み合っています。もう少し細かく理解しておかないと、起動に失敗したときに原因を特定できないケースがあるため、改めて記事としてまとめます。
この記事では前回の続編として、次の4点を整理します。
- initContainer がどういう順序で実行され、失敗するとどうなるか(通常の init container と、
restartPolicy: Alwaysを指定したサイドカー形式の init container の違いを含む) - postStart Lifecycle Hook が、後続のコンテナの起動や Pod Status の更新にどう影響するか
- startupProbe が readinessProbe/livenessProbe をどうゲートしているか
-
PodScheduled/PodReadyToStartContainers/Initialized/ContainersReady/Readyという5つの Pod Condition が、実際にはどういう順序・タイミングでTrueになるか
対象コンポーネントとバージョン
| コンポーネント | バージョン | リポジトリ | 本記事での役割 |
|---|---|---|---|
| Kubernetes | v1.37.1 | kubernetes/kubernetes | kubelet による init container の起動制御・postStart Hook の実行・probe 実行・PodCondition の計算、通常の Bind 処理での PodScheduled=True の設定 |
| kind | v0.33.0 | kubernetes-sigs/kind | 検証環境。ノードイメージは kindest/node:v1.37.0
|
実装説明は Kubernetes v1.37.1、掲載する kind の実行結果は v1.37.0 を対象にしています。通常の Bind API を使った初回起動を中心に扱い、コンテナ単位の再起動ルールや kubelet 再起動後の復旧は対象外です。実行結果は既存の検証記録で、今回の添削ではクラスタを再作成していません。
TL;DR
-
通常の Init Container は正常終了を待ちます。サイドカー形式 (
restartPolicy: Always) は、startupProbe の成功などによりstarted=trueになると次へ進みます。 -
通常の Init Container には Hook・Probe を設定できません。
postStartや3種類の Probe は、本体コンテナとサイドカー形式の Init Container で利用できます。 -
本体コンテナ同士は startupProbe の成功を待ち合わせません。v1.37.1 の起動処理では
postStartを同期的に待ちますが、アプリケーションの準備完了を順番に保証するものではありません。 - startupProbe は同じコンテナの readiness/liveness を待機させます。成功後にそれぞれの判定が進み、readinessProbe は Pod の Ready、livenessProbe はコンテナの再起動判断に関わります。
- Condition は厳密な起動イベント履歴ではありません。init container の有無や readinessGates により成立条件が変わり、API 上の更新は非同期に反映されます。
第1章: 全体の流れ
この章では、通常の Init Container、サイドカー形式の Init Container、本体コンテナの違いを整理します。以下では .spec.containers に指定するコンテナを「本体コンテナ」と呼びます。
| 種類 | 後続コンテナへ進む条件 | Hook・Probe |
|---|---|---|
| 通常の Init Container | exit code 0 で正常終了 |
lifecycle と3種類の Probe は指定不可 |
| サイドカー形式の Init Container |
started=true。startupProbe があれば成功を待つ |
postStart と3種類の Probe を指定可能 |
| 本体コンテナ | v1.37.1 の同じ起動ループでは startContainer の返却後。postStart があれば実行を待つ |
前の本体コンテナの startupProbe / readinessProbe の成功は待たない |
(参考: Init Containers、Sidecar Containers)
通常の Init Container → サイドカー → 本体コンテナという構成例です。全種類のコンテナを必ず含むわけではありません。Probe の矢印は指定されている場合を表します。
本文中の Started は ContainerStatus の started フィールドを指し、Event の Started とは別です。同様に、コンテナの ready と Pod Condition の Ready も区別します。
通常の Bind API を使う場合、kube-apiserver が spec.nodeName と PodScheduled=True を同じ更新で設定します。kubelet はボリュームの準備、sandbox 作成・ネットワーク設定などを経て、コンテナの起動へ進みます。
Initialized は通常の Init Container の正常終了と、サイドカー形式の起動を反映します。ContainersReady は本体コンテナとサイドカーの Ready を、Ready はさらに readinessGates を確認します。Condition の詳細は第5章で説明します。
本体コンテナの記述順だけで、サービス間の依存関係や準備完了の順序を保証することはできません。起動後の通信にはリトライを用意し、起動前に待つ必要がある処理は Init Container などで明示するのが良いと思います。
第2章: initContainer の実行順序
この章では、通常の Init Container とサイドカー形式で、次のコンテナへ進む判定がどう異なるかを確認します。
initContainer が「spec の順に直列実行される」ことは公式ドキュメントにも明記されていますが、実際に kubelet がどうやってそれを実現しているかを見ます。
通常の Init Container はネットワークとストレージの準備後、spec に記載した順に実行されます。次へ進むには正常終了が必要です。
(参考: Init Containers)
2.1 次に起動する init container を「末尾から逆順」に探す
実際に次に起動すべき init container を決めているのは kuberuntime_container.go の computeInitContainerActions です。コメントにある通り、spec の先頭からではなく末尾から逆順に走査します。
// pkg/kubelet/kuberuntime/kuberuntime_container.go
// Note that we iterate through the init containers in reverse order to find
// the next init container to run, as the completed init containers may get
// removed from container runtime for various reasons. Therefore the kubelet
// should rely on the minimal number of init containers - the last one.
for i := len(pod.Spec.InitContainers) - 1; i >= 0; i-- {
container := &pod.Spec.InitContainers[i]
status := podStatus.FindActiveContainerStatusByName(container.Name)
...
}
(参考: kuberuntime_container.go#L1089-L1100)
完了済みの init container のコンテナステータスはランタイム側のガベージコレクションで消えることがあります。先頭から順に「このコンテナは完了しているか」を調べる実装だと、途中のコンテナのステータスが消えていた場合に誤判定しかねません。末尾から見ていって最初に見つかった「状態が分かるコンテナ」を基準にすることで、この問題を避けています。
通常の(非サイドカー)init container が ContainerStateExited かつ exit code 0 であれば「初期化済み」と判断し、最後の init container でなければ次の index (i+1) を起動対象に積みます。
// pkg/kubelet/kuberuntime/kuberuntime_container.go
case kubecontainer.ContainerStateExited:
...
logger.V(4).Info("Init container has been initialized", "pod", klog.KObj(pod), "container", container.Name)
if i == (len(pod.Spec.InitContainers) - 1) {
podHasInitialized = true
} else {
// this init container is initialized for the first time, start the next one
changes.InitContainersToStart = append(changes.InitContainersToStart, i+1)
}
(参考: kuberuntime_container.go#L1205-L1230)
初回の初期化では、逆順に走査して「次に起動する1個」を決めるため、spec の順に進みます。すでに起動したサイドカーの再起動対象も同じ走査で収集するので、常に起動対象が1個だけとは限りません。実際の起動は SyncPod の中で InitContainersToStart に積まれた index を順番に処理する箇所が担います。
// pkg/kubelet/kuberuntime/kuberuntime_manager.go
// Step 8: start init containers.
for _, idx := range podContainerChanges.InitContainersToStart {
start := lazyStart()
if start == nil {
return
}
container := &pod.Spec.InitContainers[idx]
if err := start(ctx, "init container", metrics.InitContainer, containerStartSpec(container)); err != nil {
if podutil.IsRestartableInitContainer(container) {
// logging omitted
continue
}
// logging omitted
return
}
// logging omitted
}
(参考: kuberuntime_manager.go#L1917-L1937、SyncPod 本体は kuberuntime_manager.go#L1583)
2.2 失敗時は RestartPolicy に従って再試行、または Pod ごと終了
init container が失敗した(exit code が非ゼロ、OOMKilled、または状態不明)場合の判定は isInitContainerFailed が行います。
// pkg/kubelet/kuberuntime/helpers.go
func isInitContainerFailed(status *kubecontainer.Status) bool {
if status.Reason == "OOMKilled" {
return true
}
if status.State == kubecontainer.ContainerStateExited && status.ExitCode != 0 {
return true
}
if status.State == kubecontainer.ContainerStateUnknown {
return true
}
return false
}
(参考: helpers.go#L169-L188)
失敗と判定された場合、Pod の RestartPolicy が Never でなければ同じ init container を再度起動対象に積んで再試行し、Never であれば Pod 全体を終了させます。
// pkg/kubelet/kuberuntime/kuberuntime_container.go
if isInitContainerFailed(status) {
restartOnFailure := restartOnFailure
if utilfeature.DefaultFeatureGate.Enabled(features.ContainerRestartRules) {
restartOnFailure = kubecontainer.ShouldContainerBeRestarted(logger, container, pod, podStatus)
}
if !restartOnFailure {
changes.KillPod = true
changes.InitContainersToStart = nil
return false
}
changes.InitContainersToStart = append(changes.InitContainersToStart, i)
break
}
(参考: kuberuntime_container.go#L1206-L1222、restartOnFailure のデフォルトの計算は pod.Spec.RestartPolicy != Never (kuberuntime_manager.go#L678-L680))
ContainerRestartRules feature gate (v1.37.1 時点で beta、デフォルト有効) を使うと、コンテナ単位で Pod 全体の RestartPolicy とは異なる再起動ルールを指定できますが、本記事ではコンテナ単位のルールを指定せず、Pod 単位の restartPolicy に従う場合を扱います。
2.3 サイドカー形式の init container は「Completed」でなく「Started」で次に進む
restartPolicy: Always を指定した init container は、Pod の生存期間中ずっと動き続ける「サイドカー」として扱われます。この機能のベースになった SidecarContainers feature gate は v1.29 で beta(デフォルト有効)、v1.33 で stable となり、v1.37 では gate 自体が削除されて常時有効な機能になっています。
SidecarContainers は v1.33 から常時有効で、v1.37 では feature gate 自体が削除されています。
(参考: CHANGELOG-1.37.md#L623)
通常の init container は「exit code 0 で終了」した時点で次に進みますが、サイドカー形式の init container はプロセスが動き続けるため「終了」を待つことができません。代わりに、kubelet が started ステータスを true にした時点(startupProbe があればその成功、無ければコンテナが Running になった時点)で次の init container・本体コンテナの起動に進みます。
サイドカー形式は、startupProbe があれば成功時、なければプロセスが Running として認識された時点で started=true となり、次の Init Container に進みます。
(参考: Sidecar Containers)
この挙動はソースコード上でも「完了判定の条件」が通常の init container と明確に分かれています。
// pkg/kubelet/kuberuntime/kuberuntime_container.go
case kubecontainer.ContainerStateRunning:
if !podutil.IsRestartableInitContainer(container) {
...
break
} else { // If container is restartable
if container.StartupProbe != nil {
startup, found := m.startupManager.Get(status.ID)
if !found {
// If the startup probe has not been run, wait for it.
break
}
if startup != proberesults.Success {
...
break
}
}
logger.V(4).Info("Init container has been initialized", "pod", klog.KObj(pod), "container", container.Name)
if i == (len(pod.Spec.InitContainers) - 1) {
podHasInitialized = true
} else if !isPreviouslyInitialized {
changes.InitContainersToStart = append(changes.InitContainersToStart, i+1)
}
...
}
(参考: kuberuntime_container.go#L1130-L1161)
通常の init container が ContainerStateExited(終了済み)を見るのに対して、サイドカーは ContainerStateRunning(実行中)のケースで次へ進む判定をしている点が実装上の違いです。サイドカーは通常の init container と混在させることができ、両者を組み合わせた複雑な初期化フローを組むこともできます。
サイドカー形式も .spec.initContainers の順序に従うため、通常の Init Container と組み合わせて初期化を構成できます。
(参考: Sidecar Containers)
2.4 Initialized condition は「Ready」でなく「Started」を見ることがある
Initialized condition を計算する GeneratePodInitializedCondition は、各 init container が「初期化済み」かどうかを isInitContainerInitialized で判定します。このとき、サイドカー形式の init container だけは Ready ではなく Started フィールドを見ます。
// pkg/kubelet/status/generate.go
func isInitContainerInitialized(initContainer *v1.Container, containerStatus *v1.ContainerStatus) bool {
if podutil.IsRestartableInitContainer(initContainer) {
if containerStatus.Started == nil || !*containerStatus.Started {
return false
}
} else { // regular init container
if !containerStatus.Ready {
return false
}
}
return true
}
(参考: generate.go#L171-L182)
初回起動では、全ての init container がこの条件を満たせば Initialized=True になります。init container が1つも無い Pod では、このループ自体が空のまま終わるため、Initialized は(他の条件を待たずに)早い段階で True になります。なお、本体コンテナがすでに起動していれば、サイドカーの再起動中も Initialized=True を維持する分岐があります。通常の Init Container の Ready は、Probe ではなく正常終了を表す内部状態です。
(参考: generate.go#L219-L229、prober_manager.go#L398-L403)
第3章: postStart Lifecycle Hook が後続コンテナの起動をブロックする
この章では、Hook を設定できるサイドカー形式の Init Container と本体コンテナについて、postStart の待ち時間が起動処理に与える影響を確認します。
サイドカー形式の Init Container と本体コンテナには lifecycle.postStart を設定できます。通常の Init Container には設定できません。この Hook がどう実行されるかは、startupProbe/readinessProbe/livenessProbe とは別の仕組みで、かつ同じ Pod 内の後続コンテナの起動タイミングに直接影響します。
3.1 postStart は ENTRYPOINT の初期化完了を待たない
公式ドキュメントは、postStart と ENTRYPOINT が並行して動き得ることを説明しています。厳密に同時に開始するという意味ではありません。
postStart と ENTRYPOINT の間に、アプリケーションの初期化完了を保証する順序関係はありません。Hook が長時間終わらなければ、起動処理や Running の状態反映が遅れることがあります。
(参考: Container Lifecycle Hooks)
一方、kubelet は postStart の呼び出しが返るまで待つため、Hook の実行時間は同じ Pod の後続コンテナの起動にも影響します。
3.2 postStart が終わるまで、次のコンテナは起動すら始まらない
コンテナを起動する startContainer は、CRI の StartContainer を呼んだ(Step 3)直後に、Step 4 として postStart Hook を同期的に実行してから関数を返します。
// pkg/kubelet/kuberuntime/kuberuntime_container.go
// Step 3: start the container.
err = m.runtimeService.StartContainer(ctx, containerID)
...
m.recordContainerEvent(ctx, pod, container, containerID, v1.EventTypeNormal, events.StartedContainer, "Container started")
...
// Step 4: execute the post start hook.
if container.Lifecycle != nil && container.Lifecycle.PostStart != nil {
...
msg, handlerErr := m.runner.Run(ctx, kubeContainerID, pod, container, container.Lifecycle.PostStart)
if handlerErr != nil {
...
if err := m.killContainer(ctx, pod, kubeContainerID, container.Name, "FailedPostStartHook", reasonFailedPostStartHook, nil, nil); err != nil {
...
}
return msg, ErrPostStartHook
}
}
return "", nil
(参考: kuberuntime_container.go#L291-L339)
そして、この startContainer を呼び出す側である SyncPod の Step 8 (init container) / Step 9 (本体コンテナ) のループは、go start(...) ではなく start(...) を直接呼んでいます。
// pkg/kubelet/kuberuntime/kuberuntime_manager.go
// Step 9: start containers in podContainerChanges.ContainersToStart.
for _, idx := range podContainerChanges.ContainersToStart {
start := lazyStart()
if start == nil {
return
}
start(ctx, "container", metrics.Container, containerStartSpec(&pod.Spec.Containers[idx]))
}
(参考: kuberuntime_manager.go#L1954-L1961、init container 側の同じ構造のループは kuberuntime_manager.go#L1917-L1937)
ループが同期的である以上、ある index のコンテナの start() が返ってくる(= postStart も含めて完了する)までは、次の index のコンテナの CreateContainer/StartContainer すら呼ばれません。つまり、spec 内の前のコンテナに postStart が設定されていれば、その実行が終わるまで次のコンテナの起動は一切始まりません。これは Hook を指定できるサイドカー形式の Init Container と、本体コンテナでの動作です。通常の Init Container には postStart を設定できません。
一方で、失敗時の扱いには違いがあります。通常の Init Container の起動エラーでは return して処理を中断します。サイドカー形式の起動エラーは continue で同じ回の残りの起動対象へ進み、本体コンテナのループも start の戻り値では中断しません。postStart の失敗でコンテナが kill されても、本体コンテナのループ自体は次のコンテナの起動に進みます。サイドカーのエラー時に continue することは、後続の初期化条件を成功扱いに変えるという意味ではありません。
postStart の失敗時はコンテナが停止され、再起動の扱いは再起動ポリシーに従います。
(参考: Container Lifecycle Hooks)
3.3 postStart の実行中は API 上の状態反映も遅れる
これだけだと「次のコンテナの起動開始が遅れるだけ」のようにも見えますが、kubelet の SyncPod の構造上、初回の起動処理中は、コンテナの Running が API 上に反映されるまで遅れが生じます。pkg/kubelet/kubelet.go の SyncPod は、まず直前の状態から apiPodStatus を組み立てて書き込み、そのあとで(別関数である)コンテナ起動処理を呼び出しています。
// pkg/kubelet/kubelet.go
// Generate final API pod status with pod and status manager status
apiPodStatus := kl.generateAPIPodStatus(ctx, pod, podStatus, false)
...
kl.statusManager.SetPodStatus(logger, pod, apiPodStatus)
...
// Call the container runtime's SyncPod callback
result := kl.containerRuntime.SyncPod(sctx, pod, podStatus, pullSecrets, kl.crashLoopBackOff, restartingAllContainers)
(参考: kubelet.go#L2108-L2135、kubelet.go#L2267)
kl.containerRuntime.SyncPod が、3.2節で見た「コンテナを1つずつ同期的に起動する」処理そのものです。この呼び出しは postStart の完了を待って初めて返ってくるため、この回の SyncPod 呼び出しの中では、新しく起動したコンテナの Running 状態を反映した Pod Status は書き込まれません。SyncPod はこの後そのまま関数を抜けます。
// pkg/kubelet/kubelet.go
if len(result.SyncResults) > 0 && err == nil {
postSync = func() {
kl.RequestPodRelist(klog.FromContext(ctx), pod.UID)
}
}
return false, postSync, err
(参考: kubelet.go#L2321-L2328)
その後の relist や状態更新を受けた sync で、新しいランタイム状態から API の Pod Status が組み立てられます。これはコンテナプロセスが停止していることを意味しません。また、Pod Status の全フィールドが更新されないわけではなく、sandbox 準備完了の非同期更新などは別に行われます。
3.4 kind で実際に確認する
以下は既存の検証記録です。1コンテナだけの Pod に15秒待つ postStart を設定し、起動中のステータスをポーリングしています。再現する場合は、専用クラスタを作成し、次の YAML を poststart-demo.yaml として保存します。
kind create cluster --name pod-poststart-review --image kindest/node:v1.37.0
apiVersion: v1
kind: Pod
metadata:
name: poststart-demo
spec:
containers:
- name: main
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
lifecycle:
postStart:
exec:
command: ["sh", "-c", "sleep 15"]
kubectl apply -f poststart-demo.yaml
for i in $(seq 1 8); do
date -u +%T
kubectl get pod poststart-demo -o json | jq -r '.status.containerStatuses[0] | {started, ready, state}'
sleep 2
done
02:14:25 {"started": false, "ready": false, "state": {"waiting": {"reason": "ContainerCreating"}}}
02:14:27 {"started": false, "ready": false, "state": {"waiting": {"reason": "ContainerCreating"}}}
...
02:14:33 {"started": false, "ready": false, "state": {"waiting": {"reason": "ContainerCreating"}}}
02:14:35 {"started": true, "ready": true, "state": {"running": {"startedAt": "2026-10-18T02:14:20Z"}}}
postStart の sleep 15 が終わる(コンテナの開始時刻は 02:14:20)まで、API 上はずっと ContainerCreating のままで、02:14:35 になって突然 running に切り替わりました。しかも startedAt は 02:14:20 と、ランタイムが報告した開始時刻を記録しています。これは3.3節で見た「postStart を含むコンテナ起動処理が完了するまで、更新後の Pod Status が書き込まれない」という実装と整合する観測です。
kubectl の ContainerCreating 表示と、ランタイム上でプロセスが動き始めた時刻は一致するとは限りません。起動遅延を調べる際は、Hook の設定と Event も確認すると良いと思います。
kind delete cluster --name pod-poststart-review
第4章: startupProbe が readiness/liveness をゲートする仕組み
この章では、1つのコンテナ内で3種類の Probe がどう連携するかを確認します。初回起動を対象とし、kubelet 再起動後の結果引き継ぎは扱いません。
3つの probe のうち、startupProbe は「他の2つの probe をいつから動かすか」を決めるゲートとしての役割を持っています。
startupProbe を指定した場合、成功するまで同じコンテナの readinessProbe / livenessProbe は実行されません。
(参考: Pod Lifecycle)
4.1 doProbe: Started になるまで readiness/liveness は実行自体をスキップする
各 probe はコンテナ・probe 種別ごとに専用の worker goroutine を持ち、doProbe が定期的に呼ばれます。doProbe の中には、コンテナの Started フィールドを見て readiness/liveness を早期リターンさせる分岐があります。
// pkg/kubelet/prober/worker.go
if c.Started != nil && *c.Started {
// Stop probing for startup once container has started.
// we keep it running to make sure it will work for restarted container.
if w.probeType == startup {
return true
}
} else {
// Disable other probes until container has started.
if w.probeType != startup {
return true
}
}
// Note, exec probe does NOT have access to pod environment variables or downward API
result, err := w.probeManager.prober.probe(ctx, w.probeType, w.pod, status, w.container, w.containerID)
(参考: worker.go#L334-L350)
Started が false の間、readiness/liveness の worker は実際の probe (w.probeManager.prober.probe) に到達せず true を返してその回の処理を終えます。つまり HTTP/TCP/exec/gRPC の実行自体が行われません。
readiness/liveness の結果を保持する resultsManager には、worker 作成時に初期値がセットされています。
// pkg/kubelet/prober/worker.go
case readiness:
w.spec = container.ReadinessProbe
w.resultsManager = m.readinessManager
w.initialValue = results.Failure
case liveness:
w.spec = container.LivenessProbe
w.resultsManager = m.livenessManager
w.initialValue = results.Success
case startup:
w.spec = container.StartupProbe
w.resultsManager = m.startupManager
w.initialValue = results.Unknown
(参考: worker.go#L112-L125)
readiness の初期値は Failure、liveness の初期値は Success です。Started が false の間は実際に probe を実行して上書きすることがないため、この初期値がそのまま維持されます。readiness は「まだ Ready でない」、liveness は「まだ Kill しない」という安全側の値で止まっていることになります。
4.2 Started はいつ true になるか
Started フィールドを計算しているのは prober_manager.go の isContainerStarted です。
// pkg/kubelet/prober/prober_manager.go
func (m *manager) isContainerStarted(logger klog.Logger, pod *v1.Pod, containerStatus *v1.ContainerStatus) bool {
if containerStatus.State.Running == nil {
return false
}
if result, ok := m.startupManager.Get(kubecontainer.ParseContainerID(logger, containerStatus.ContainerID)); ok {
return result == results.Success
}
...
// if there is a startup probe which hasn't run yet, the container is not started.
if _, exists := m.getWorker(pod.UID, containerStatus.Name, startup); exists {
return false
}
// there is no startup probe, so the container is started.
return true
}
(参考: prober_manager.go#L287-L308、呼び出し元の UpdatePodStatus は prober_manager.go#L344-L348)
- startupProbe が設定されているコンテナ:
startupManagerにその成否が記録されるまでStarted=false。成功すればtrue - startupProbe が設定されていないコンテナ: startup 用の worker がそもそも作られないため、Running と認識された状態更新で
Started=true
4.3 livenessProbe: startup 完了前は失敗判定を行わない
liveness によるコンテナの Kill 判定は kuberuntime_manager.go の computePodActions が行いますが、ここで参照する livenessManager の結果は、4.1節で見た通り startup 完了前は初期値の Success のまま変化しません。つまり startup 完了前に livenessProbe の失敗を理由として停止されることはありません。OOM やプロセス終了、Pod の削除など、別の要因による停止は起こり得ます。
// pkg/kubelet/kuberuntime/kuberuntime_manager.go
} else if liveness, found := m.livenessManager.Get(containerStatus.ID); found && liveness == proberesults.Failure {
message = fmt.Sprintf("Container %s failed liveness probe", container.Name)
reason = reasonLivenessProbe
} else if startup, found := m.startupManager.Get(containerStatus.ID); found && startup == proberesults.Failure {
message = fmt.Sprintf("Container %s failed startup probe", container.Name)
reason = reasonStartupProbe
}
(参考: kuberuntime_manager.go#L1447-L1454)
なお、startupProbe の連続失敗回数が failureThreshold に達した場合は、この2つ目の分岐によって liveness とは別の理由 (reasonStartupProbe) で Kill されます。公式ドキュメントの記述とも一致します。
startupProbe の失敗が閾値に達するとコンテナは停止され、再起動ポリシーに従って扱われます。livenessProbe は readinessProbe の成功を待つ仕組みではありません。
(参考: Pod Lifecycle)
4.4 readinessProbe: startup 完了前は Ready の計算自体をスキップする
readiness についても、startup 完了前は probe が実行されないだけでなく、UpdatePodStatus 側でも Ready フィールドの計算そのものをスキップしています。
// pkg/kubelet/prober/prober_manager.go
for i, c := range podStatus.ContainerStatuses {
started := m.isContainerStarted(logger, pod, &podStatus.ContainerStatuses[i])
podStatus.ContainerStatuses[i].Started = &started
if !started {
continue
}
var ready bool
...
(参考: prober_manager.go#L344-L352)
started が false の間はこのループの残りに進まないため、コンテナの Ready はゼロ値の false のままです。startup が完了して初めて readinessManager の結果を見て Ready が決まります。
第5章: 5つの Pod Condition はどの順序で True になるか
この章では、起動に関わる主な5種類の Condition を、成立条件と API 上の観測に分けて整理します。
この図は Init Container を持つ Pod の初回起動における論理的な前後関係です。API 上で別々の更新や別々の時刻として観測できる保証はありません。Init Container がない場合は Initialized が先に成立します。また、sandbox の喪失や Probe の失敗で、一度 True になった Condition が False に戻る場合もあります。
ここまでで、init container の完了と startupProbe の成功が、それぞれ何に影響するかが分かりました。この章では、それらが PodScheduled / PodReadyToStartContainers / Initialized / ContainersReady / Ready という5つの condition にどうつながるかを見ます。
公式ドキュメントでの定義は次の通りです。
本記事で扱う5種類の Condition は、配置、コンテナ起動の前提条件、初期化、コンテナの Ready、Pod 全体の Ready を表します。Pod にはこのほかの組み込み Condition や、readinessGates 用のカスタム Condition も存在します。
(参考: Pod Lifecycle)
5.1 通常の Bind 処理では kube-apiserver が PodScheduled=True を設定する
標準の Bind API を使った配置では、Binding サブリソースを処理する kube-apiserver が PodScheduled=True を設定します。kube-scheduler は配置先を選んで Bind API を呼び出します。一方、スケジュールできない場合の PodScheduled=False などは scheduler も更新するため、この Condition 全体を apiserver だけが管理しているわけではありません。
(参考: schedule_one.go#L1252-L1263)
// pkg/registry/core/pod/storage/storage.go
err = r.store.Storage.GuaranteedUpdate(ctx, podKey, &api.Pod{}, false, preconditions, storage.SimpleUpdate(func(obj runtime.Object) (runtime.Object, error) {
pod, ok := obj.(*api.Pod)
...
pod.Spec.NodeName = machine
...
podutil.UpdatePodCondition(&pod.Status, &api.PodCondition{
Type: api.PodScheduled,
Status: api.ConditionTrue,
})
finalPod = pod
return pod, nil
}))
(参考: storage.go#L228-L267)
spec.nodeName の設定と PodScheduled=True の設定が、同じ GuaranteedUpdate (CAS ベースの楽観的排他制御) の中で行われている点がポイントです。この Bind 処理では両方が同じ更新に含まれます。作成時から spec.nodeName を指定する Pod や Static Pod など、Bind API を使わない場合にまで、この説明を一般化することはできません。
5.2 PodReadyToStartContainers は sandbox 作成直後に非同期で確定する
PodReadyToStartContainers は、Pod sandbox・ネットワーク・ボリュームと、必要な動的リソースの準備を反映します。この condition は2箇所から更新されます。
1つ目は、新しい sandbox を作った直後に呼ばれるコールバックです。
// pkg/kubelet/kuberuntime/kuberuntime_manager.go
// Step 5: invoke the sandbox ready callback before image pulling.
// At this point, dynamic resources are prepared (at PrepareDynamicResources() call above)
// and volumes are already mounted (at the kubelet SyncPod() level), so,
// all requirements (sandbox, networking, volumes) are met to set `PodReadyToStartContainers=True` condition.
if err := m.runtimeHelper.OnPodSandboxReady(ctx, pod); err != nil {
...
}
(参考: kuberuntime_manager.go#L1781-L1789、OnPodSandboxReady の実装は kubelet.go#L3626-L3659)
2つ目は、毎回の sync で再計算されるフォールバック用のロジックです。
// pkg/kubelet/status/generate.go
func GeneratePodReadyToStartContainersCondition(pod *v1.Pod, oldPodStatus *v1.PodStatus, podStatus *kubecontainer.PodStatus) v1.PodCondition {
newSandboxNeeded, _, _, reason := runtimeutil.PodSandboxChanged(pod, podStatus)
if !newSandboxNeeded {
return v1.PodCondition{Type: v1.PodReadyToStartContainers, Status: v1.ConditionTrue, ...}
}
return v1.PodCondition{Type: v1.PodReadyToStartContainers, Status: v1.ConditionFalse, Reason: kubetypes.PodSandboxNotReadyReason, ...}
}
(参考: generate.go#L271-L291、判定ロジック本体の PodSandboxChanged は util.go#L31-L70)
公式ドキュメントも、sandbox 作成からイメージ pull・コンテナ作成までの前後関係を明記しています。
kubelet は sandbox・ネットワーク・ボリュームと、必要な動的リソースの準備後、イメージの取得やコンテナ作成に先立って PodReadyToStartContainers=True を設定します。API への反映は非同期です。
(参考: Pod Lifecycle)
PodReadyToStartContainersCondition feature gate は v1.28 で alpha、v1.29 で beta(デフォルト有効)、v1.37 で stable(ロック済みデフォルト有効)になっています。v1.37.1 では常時有効です。
5.3 Condition の計算順と成立順は異なる
kubelet は Pod の状態が変わるたびに generateAPIPodStatus を呼び、その中の次の箇所で5つの condition を配列に積みます。
// pkg/kubelet/kubelet_pods.go
if utilfeature.DefaultFeatureGate.Enabled(features.PodReadyToStartContainersCondition) {
s.Conditions = append(s.Conditions, status.GeneratePodReadyToStartContainersCondition(pod, &oldPodStatus, podStatus))
}
allContainerStatuses := append(s.InitContainerStatuses, s.ContainerStatuses...)
s.Conditions = append(s.Conditions, status.GeneratePodInitializedCondition(pod, &oldPodStatus, allContainerStatuses, s.Phase))
s.Conditions = append(s.Conditions, status.GeneratePodReadyCondition(pod, &oldPodStatus, s.Conditions, allContainerStatuses, s.Phase))
s.Conditions = append(s.Conditions, status.GenerateContainersReadyCondition(pod, &oldPodStatus, allContainerStatuses, s.Phase))
s.Conditions = append(s.Conditions, v1.PodCondition{
Type: v1.PodScheduled,
ObservedGeneration: podutil.CalculatePodConditionObservedGeneration(&oldPodStatus, pod.Generation, v1.PodScheduled),
Status: v1.ConditionTrue,
})
(参考: kubelet_pods.go#L2007-L2018)
この箇所の配列への追加順は PodReadyToStartContainers → Initialized → Ready → ContainersReady → PodScheduled です。ただし、GeneratePodReadyCondition は内部で GenerateContainersReadyCondition を呼び、さらに readinessGates の Condition を参照します。互いに独立した5つの計算という意味ではありません。
(参考: generate.go#L122-L168)
通常の初回起動では、次のように読み分けます。
-
PodScheduled: 配置先への Bind を反映する。kubelet も自ノードで扱う Pod の状態に True を設定する。 -
PodReadyToStartContainers: sandbox などの準備を反映する。Bind 直後に必ず成立するわけではなく、ネットワーク・ボリュームなどの準備で遅れる場合がある。 -
Initialized: 通常の Init Container が正常終了し、サイドカー形式が Started になったことを反映する。 -
ContainersReady: 本体コンテナとサイドカー形式の Init Container がすべて Ready になったことを反映する。readinessProbe を指定しない場合は Started 後に Ready となる。 -
Ready: ContainersReady に加えて、指定された readinessGates がすべて True であることを確認する。gate に対応する Condition が未設定なら False となる。
(参考: generate.go#L48-L168、prober_manager.go#L344-L435)
init container が無い Pod では Initialized が sandbox 作成より先に True になることもあります。公式ドキュメントにもこの非対称性が明記されています。
Init Container がある Pod はその初期化後に Initialized=True となります。一方、Init Container がない Pod では、sandbox 作成より先に Initialized=True になります。
(参考: Pod Lifecycle)
5.4 kind で実際の遷移を確認する
以下は kind で確認した既存の記録です。再現する場合は、次の YAML を lifecycle-demo.yaml として保存します。通常の init container (init-regular) の後に、startupProbe 付きのサイドカー init container (init-sidecar) を置き、本体コンテナ (nginx) にも startupProbe/readinessProbe/livenessProbe を設定しています。
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
spec:
initContainers:
- name: init-regular
image: busybox:1.36
command: ["sh", "-c", "sleep 5"]
- name: init-sidecar
image: busybox:1.36
restartPolicy: Always
command: ["sh", "-c", "touch /tmp/ready; while true; do sleep 1; done"]
startupProbe:
exec:
command: ["sh", "-c", "test -f /tmp/ready"]
initialDelaySeconds: 5
periodSeconds: 1
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
startupProbe:
httpGet: { path: /, port: 80 }
initialDelaySeconds: 5
periodSeconds: 1
failureThreshold: 10
readinessProbe:
httpGet: { path: /, port: 80 }
periodSeconds: 1
livenessProbe:
httpGet: { path: /, port: 80 }
periodSeconds: 5
kind create cluster --name pod-lifecycle-demo --image kindest/node:v1.37.0
kubectl apply -f lifecycle-demo.yaml
kubectl wait --for=condition=Ready pod/lifecycle-demo --timeout=120s
Pod が Ready になった後、各 Condition の lastTransitionTime を並べた記録です。これは直近の状態遷移の時刻で、全履歴ではありません。同じ秒の値から、内部処理の同時性や前後関係を確定することもできません。
kubectl get pod lifecycle-demo -o json | jq -r '.status.conditions | sort_by(.lastTransitionTime)[] | "\(.lastTransitionTime) \(.type)=\(.status)"'
2026-10-18T01:51:18Z PodReadyToStartContainers=True
2026-10-18T01:51:18Z PodScheduled=True
2026-10-18T01:51:33Z Initialized=True
2026-10-18T01:51:45Z Ready=True
2026-10-18T01:51:45Z ContainersReady=True
各コンテナの開始・終了時刻も合わせた既存の記録を整理すると、次のようになります。Probe の成功時刻は Condition と開始時刻からの解釈を含みます。再現時は次のコマンドで各フィールドを確認できます。
kubectl get pod lifecycle-demo -o json | jq '{init: .status.initContainerStatuses, main: .status.containerStatuses}'
| 時刻 | 出来事 |
|---|---|
| 01:51:18 |
PodScheduled/PodReadyToStartContainers の遷移時刻が同じ秒で記録されている |
| 01:51:23 |
init-regular が起動 (このコンテナ自体が5秒 sleep するだけの内容) |
| 01:51:28 |
init-regular が exit code 0 で終了。直後に init-sidecar が起動 (state.running.startedAt) |
| 01:51:33 |
Initialized=True の遷移時刻。サイドカー開始の5秒後で、startupProbe 成功後に成立する条件と整合する |
| 01:51:40 |
nginx が起動 (state.running.startedAt) |
| 01:51:45 |
Ready/ContainersReady の遷移時刻。nginx 開始の5秒後で、startupProbe と readinessProbe 成功後の状態を反映している |
通常の Init Container の終了後にサイドカーが起動し、その startupProbe の成功を経て Initialized が成立する流れと整合する記録です。ただし、表示された時刻だけでは Probe の個々の実行時刻まで直接確認できません。initialDelaySeconds は最短の待ち時間であり、API の反映遅延や Probe の実行間隔も含め、指定秒数ちょうどで Condition が変わる保証はありません。
kind delete cluster --name pod-lifecycle-demo
おわりに
Init Container の正常終了、コンテナの Started、Pod の Ready を分けて見ると、起動中のどこで待っているかを整理しやすくなりました。特に postStart の待ち時間と API 上の状態反映の違いは、ContainerCreating が長引く場合に意識したい点です。
参考
参考リンク
本文で登場する順に並べています。