はじめに
Kubernetesのトラブルシュートでは「Eventログを確認しましょう」「Event ログは保管しておいた方がいいですよ」という話をよく耳にします。実際、kubectl describe pod を叩けば画面の下の方に Events: という表が出てきますし、kubectl get events でも一覧を取れます。
ただ、「Event を見ましょう」と言われても、そこにどんな種類の情報が載っていて、何が分かって何が分からないのかのイメージが湧かない、という相談を受けたことがあります。実際にEventのReasonを何十種類も見比べたことがある人は少なく、初めて kubectl describe pod の出力を見たときに、FailedScheduling や BackOff といった文字列だけを頼りに手探りで調べることになりがちです。
そこでこの記事では、Kubernetesが標準で発行するEventを「どのコンポーネントが」「どんな条件で」「何のために」発火させているのかを、kubernetes/kubernetes のソースコードを直接読んでカテゴリ別に整理しました。実際にどんな Event ログがあるのか、一緒に見ていきましょう。
対象コンポーネントとバージョン
| コンポーネント | バージョン | リポジトリ | 本記事での役割 |
|---|---|---|---|
| kube-scheduler / kubelet / kube-controller-manager 等 | v1.37.1(タグ) | kubernetes/kubernetes | Event Reasonの発火箇所・発火条件をソースコードで確認 |
TL;DR
-
EventはAPIオブジェクトとしては地味ですが、
reason/type/message/count/firstTimestamp・lastTimestamp/reportingComponentを組み合わせることで「何が・いつから・何回・どのコンポーネントの判断で」起きたかを表現しています(参考: Event API)。 -
Eventは既定で1時間(
--event-ttl)しか保持されません。トラブルシュートの前に消えていることが多く、「保管しておいた方がいい」という話はここに由来します。 - EventのReasonは、スケジューリング→イメージ取得→コンテナ起動→ヘルスチェック→リソース逼迫時のEvictionという、Podのライフサイクルの各フェーズにほぼ1対1で対応しています。 どのフェーズで詰まっているかは、出ているReasonの種類から逆算できます。
-
NodeNotReadyやEvictedのように見た目は深刻でもEventTypeがNormalのReasonがあります。 Warning/Normalの区分だけで深刻度を判断すると見誤ります。 -
StatefulSetのPod操作Eventは固定のReason文字列ではなく
Successful<動詞>/Failed<動詞>という形で動的に組み立てられています。 他のワークロードコントローラーのSuccessfulCreate/FailedCreateと紛らわしいので注意が必要です。 -
kubectl events --types=WarningでWarningだけに絞り込める、kubectl events --for pod/xxx --watchでリアルタイムに追える、といったkubectl eventsサブコマンド独自のオプションも押さえておくと調査が速くなります。
第1章: Event とは何か
1.1 Event オブジェクトの構造
Kubernetesの Event は、他のリソースと同様にAPIオブジェクトとして表現されます。次のフィールドが特に重要です。
| フィールド | 意味 |
|---|---|
reason |
状態遷移の理由を表す短い機械可読な文字列(例: FailedScheduling) |
type |
Normal か Warning(将来増える可能性がある、とドキュメントに明記あり) |
message |
人間可読な説明文 |
involvedObject |
どのリソースに関するEventか |
count / firstTimestamp / lastTimestamp
|
同じEventが何回・いつからいつまで発生したか |
reportingComponent |
どのコンポーネントが発行したか(例: kubernetes.io/kubelet) |
series |
短時間に連続発生した場合の集約情報 |
(参考: Event API リファレンス)
reason/type の組み合わせ方は各コンポーネントの実装に委ねられていますが、この記事で見ていく通り、多くのコンポーネントは「成功したら Normal + 名詞や過去分詞」「失敗したら Warning + Failed始まりの文字列」という緩やかな命名規則に従っています。
1.2 Eventは「ベストエフォートで短命」なデータである
Event用語集ページには次のように明記されています。
Events have a limited retention time and triggers and messages may evolve with time. Event consumers should not rely on the timing of an event with a given reason reflecting a consistent underlying trigger, or the continued existence of events with that reason.
Events should be treated as informative, best-effort, supplemental data.
(参考: Event | Kubernetesドキュメント用語集)
実際、kube-apiserverには保持期間を制御する --event-ttl フラグがあり、既定値は1時間です。
Amount of time to retain events.(Default: 1h0m0s)
(参考: kube-apiserver)
この「1時間で消える」という制約が、「Event ログを保管しておいた方がいい」と言われる理由です。1時間以上前に起きた問題を後から調査したい場合、クラスタ標準のEvent APIだけでは間に合わず、Event Exporterのような仕組みで外部にエクスポートしておく必要があります。
また、kube-apiserverには --etcd-servers-overrides というフラグもあり、リソースごとに別のetcdクラスタへ書き込み先を切り替えられます。ドキュメントのフラグ説明の例には /events#http://etcd6:2379 が挙げられており、Eventは発生頻度が高く揮発性も高いデータであるため、Pod等の本体データとは別のetcdクラスタへ分離する構成例として言及されています。
(参考: kube-apiserver)
1.3 kubectl events というサブコマンド
kubectl describe の末尾に表示される以外に、kubectl events という専用サブコマンドもあります。--types=Normal,Warning でタイプを絞り込んだり、--for TYPE/NAME で特定リソースに絞って --watch でリアルタイムに眺めたりできます。
# Warning だけに絞って一覧表示
kubectl events --types=Warning
# 特定のPodのEventだけをwatchし続ける
kubectl events --for pod/web-pod-13je7 --watch
(参考: kubectl events)
ここから先は、実際にどのコンポーネントがどんなReasonを発行しているのかを、カテゴリ別に見ていきます。
第2章: スケジューリング系Event
Podがどのノードに割り当てられるか、という段階で発生するEventです。kube-schedulerが involvedObject をPodにして発行します。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| FailedScheduling | Warning | スケジューリングに失敗した(適合するノードが無い等) | schedule_one.go#L1253 |
| Scheduled | Normal | Podのノードへのバインドに成功した | schedule_one.go#L1138 |
| Preempted | Normal | 優先度の高いPodのために本Podがプリエンプション(強制削除)された | executor.go#L159 |
| BindingConditionsPending | Normal | Dynamic Resource Allocation(DRA)でデバイスのバインド条件待ち | dynamicresources.go#L1720 |
Preempted は、削除される対象のPod(ソースコード上の変数名は victim)の視点で見るとWarningであってもよさそうな名前ですが、実装上は EventTypeNormal です。プリエンプションはスケジューラの正常な意思決定の結果であり、異常事態ではない、という設計判断が読み取れます。
Pending のまま進まないPodを見たら、まず FailedScheduling の message を読むのが基本です。ここにはCPU/メモリ不足、Taint/Toleration不一致、Affinity不一致などスケジューリングに失敗した具体的な理由が入ります。
第3章: コンテナ起動・イメージ取得系Event
kubeletの pkg/kubelet/kuberuntime/ と pkg/kubelet/images/ が発行するEventです。Reason定数は pkg/kubelet/events/event.go に集約されています。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| Pulling | Normal | イメージのpullを開始した | image_manager.go#L362 |
| Pulled | Normal | イメージのpullに成功した(サイズ・所要時間をmessageに含む) | image_manager.go#L381 |
| Failed(FailedToPullImage) | Warning | イメージのpullに失敗した | image_manager.go#L369 |
| BackOff(BackOffPullImage) | Warning | 直前のpull失敗から間もないため再pullをバックオフした | image_manager.go#L346 |
| InspectFailed | Warning | pull済みイメージのメタデータ取得に失敗した | image_manager.go#L124 |
| ErrImageNeverPull | Warning |
imagePullPolicy: Never でローカルにイメージが無い |
image_manager.go#L164 |
| Created(CreatedContainer) | Normal | コンテナの作成に成功した | kuberuntime_container.go#L289 |
| Started(StartedContainer) | Normal | コンテナの起動に成功した | kuberuntime_container.go#L298 |
| Failed(FailedToCreateContainer / FailedToStartContainer) | Warning | コンテナの作成・起動、あるいはPreCreate/PreStartフックの実行に失敗した | kuberuntime_container.go#L280 |
| BackOff(BackOffStartContainer) | Warning | 直前の終了から一定時間内で、コンテナの再起動をバックオフした | kuberuntime_manager.go#L2098 |
| Killing(KillingContainer) | Normal | コンテナを終了させようとしている | kuberuntime_container.go#L887 |
| FailedPostStartHook / FailedPreStopHook | Warning | lifecycle hook(postStart/preStop)の実行に失敗した | kuberuntime_container.go#L330 |
| SandboxChanged | Normal | 既存のPod sandboxを作り直す必要が生じた | kuberuntime_manager.go#L1591 |
| FailedCreatePodSandBox | Warning | Pod sandboxの作成に失敗した | kuberuntime_manager.go#L1749 |
| Starting(StartingKubelet) | Normal | kubelet自体が起動した(ノード単位で1回) | kubelet.go#L3378 |
Pulling → Pulled → Created → Started という並びが、正常系のコンテナ起動シーケンスをそのまま反映しています。Waiting のまま進まないPodのEventにこの並びの途中で Failed や BackOff が挟まっていれば、そこで止まっている工程が特定できます。
FailedToCreateContainer と FailedToStartContainer はどちらもReason文字列が "Failed" である点に注意が必要です。Reasonだけでは作成の失敗か起動の失敗かを区別できず、message を読む必要があります。
第4章: プローブ(ヘルスチェック)系Event
liveness/readiness/startup probeの結果はkubeletの pkg/kubelet/prober/ から発行されます。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| Unhealthy(ContainerUnhealthy) | Warning | probeが失敗した、またはprobe自体がエラーになった | prober.go#L124 |
| ProbeWarning(ContainerProbeWarning) | Warning | probeは成功したが、レスポンス内容に警告が含まれていた | prober.go#L118 |
Unhealthy はliveness probeの失敗によるコンテナ再起動の直前に出ることが多く、BackOff と組み合わせて「再起動を繰り返している(CrashLoopBackOff)」状態を切り分ける手がかりになります。
第5章: リソース逼迫・Eviction系Event
ノードのリソースが逼迫したときに、kubeletのEviction Managerが発行するEventです。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| EvictionThresholdMet | Warning | Eviction閾値を超過し、リソース回収を試みようとしている(ノードに対して発行) | eviction_manager.go#L392 |
| Evicted | Warning | 閾値超過が続き、実際にPodを強制終了(Evict)した | eviction_manager.go#L643 |
Evicted は pod.status.phase を Failed、pod.status.reason を Evicted にすると同時に発行されます(参考: helpers.go#L44)。message には The node was low on resource: <資源名>. のような形式で、どのリソース(メモリ・ディスク・PID等)が逼迫要因だったかが入ります。
Eviction一歩手前のノード状態は、pkg/kubelet/nodestatus/setters.go がNode Conditionの更新と合わせてEventを発行します。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| NodeHasSufficientMemory / NodeHasInsufficientMemory | Normal | メモリ逼迫のCondition(MemoryPressure)が変化した | setters.go#L604 |
| NodeHasDiskPressure / NodeHasNoDiskPressure | Normal | ディスク逼迫のCondition(DiskPressure)が変化した | setters.go#L728 |
| NodeHasSufficientPID / NodeHasInsufficientPID | Normal | PID逼迫のCondition(PIDPressure)が変化した | setters.go#L666 |
これらはすべて EventTypeNormal です。「メモリが足りない」という状態遷移自体はkubeletにとって正常に検知できている状態であり、実際にPodを追い出すところまで行って初めて Evicted(Warning)になる、という段階分けです。
kubeletが受け付け時点でPodを拒否する(スケジュールはされたがそのノードでは動かせない)場合は、pkg/kubelet/lifecycle/predicate.go で定義された OutOfcpu / OutOfmemory / OutOfephemeral-storage / OutOfpods といったReasonが使われます(参考: predicate.go#L85)。スケジューラが把握しているリソース量とkubeletが実際に確保できるリソース量がずれた場合に発生します。
第6章: ボリューム・ストレージ系Event
kubeletとcontroller-manager(AttachDetachController)は同じ pkg/volume/util/operationexecutor/operation_generator.go を共有しており、Attach/Mount/Resizeの成否をそこから発行します。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| SuccessfulAttachVolume | Normal | ボリュームのAttachに成功した | operation_generator.go#L307 |
| FailedAttachVolume | Warning | ボリュームのAttachに失敗した | operation_generator.go#L317 |
| SuccessfulMountVolume | Normal | ボリュームのMountに成功した | operation_generator.go#L1118 |
| FailedMountVolume(FailedMount) | Warning | Pod起動時にAttach/Mountに失敗した(kubelet側からも発行) | kubelet.go#L2238 |
| VolumeResizeSuccessful / VolumeResizeFailed | Normal / Warning | ボリューム本体のリサイズの成否 | operation_generator.go#L1584 |
| FileSystemResizeSuccessful / FileSystemResizeFailed | Normal / Warning | ボリューム上のファイルシステム拡張の成否 | operation_generator.go#L2144 |
PersistentVolume/PersistentVolumeClaimのバインディングやプロビジョニングは pkg/controller/volume/persistentvolume/pv_controller.go が発行し、定数は pkg/controller/volume/events/event.go にあります。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| FailedBinding | Warning | PVCにマッチするPVが見つからない等でバインドできない | pv_controller.go#L468 |
| ProvisioningFailed | Warning | 動的プロビジョニングに失敗した | pv_controller.go#L1537 |
| ProvisioningSucceeded | Normal | 動的プロビジョニングに成功した | pv_controller.go#L1766 |
| VolumeFailedDelete | Warning | PV削除に失敗した | pv_controller.go#L1310 |
FailedMount はPodが ContainerCreating のまま止まる典型的な原因の1つです。ノード側(kubelet)とcontroller-manager側の両方が似た名前のEventを出すため、involvedObject を見て、どちらのコンポーネントが検知した失敗かを区別すると原因の切り分けが早くなります。
第7章: ノード系Event
ノードの起動・状態変化・強制排除に関するEventです。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| NodeReady | Normal | Node ConditionのReadyがTrueになった | setters.go#L548 |
| NodeNotReady | Normal | Node ConditionのReadyがFalse(またはUnknown)になった | setters.go#L550 |
| Rebooted(NodeRebooted) | Warning | kubeletがノードの再起動を検知した | setters.go#L251 |
| Shutdown(NodeShutdown) | Normal | Graceful Node Shutdownが実行された | nodeshutdown_manager_linux.go#L304 |
| RegisteredNode | Normal | node lifecycle controllerがノードの新規登録を検知した | node_lifecycle_controller.go#L698 |
| RemovingNode | Normal | node lifecycle controllerがノードの削除を検知した | node_lifecycle_controller.go#L706 |
| TaintManagerEviction | Normal |
NoExecute Taintに基づき、taint eviction controllerがPodを削除対象にした(またはキャンセルした) |
taint_eviction.go#L600 |
前章の Evicted(kubeletによるリソース逼迫時の個別Pod排除)と、この章の TaintManagerEviction(Taintに基づくcontroller-manager側の排除)は、どちらも「Podが追い出される」という結果は同じでも、発生源とトリガーの条件がまったく異なります。「Node Not Ready になってからしばらくしてPodが消えた」という現象を見た場合、TaintManagerEviction の方を疑うのが筋です。
NodeNotReady がEventTypeとしては Normal である点は、TL;DRでも触れた通り注意が必要です。Warningで絞り込んでEventを眺めていると、このEvent自体は見逃してしまいます。
第8章: ネットワーク・Service系Event
EndpointSliceの更新やkube-proxyのヘルスチェックサーバーに関するEventです。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| FailedToUpdateEndpoint | Warning | (レガシーの)Endpointsオブジェクトの更新に失敗した | endpoints_controller.go#L549 |
| FailedToUpdateEndpointSlices | Warning | EndpointSliceの更新に失敗した | endpointslice_controller.go#L455 |
| FailedToStartServiceHealthcheck | Warning | kube-proxyのService用ヘルスチェックサーバーの起動に失敗した | service_health.go#L161 |
Service経由の通信が疎通しない場合、Pod自体のEventだけでなく、これらServiceやEndpointSlice宛てのEventも合わせて確認する必要があります。kubectl events --for service/xxx のように involvedObject を指定して絞り込むと見つけやすくなります。
第9章: ワークロードコントローラー系Event
Deployment/ReplicaSet/DaemonSet/Job/StatefulSetなど、Podを間接的に作成するコントローラーが発行するEventです。
ReplicaSet・DaemonSet・Jobに共通するPodの作成・削除は、pkg/controller/controller_utils.go の共通ヘルパー経由で発行されており、Reason文字列も共通です(定数定義: controller_utils.go#L414-L425)。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| SuccessfulCreate | Normal | 管理下のPodの作成に成功した | controller_utils.go#L614 |
| FailedCreate | Warning | 管理下のPodの作成に失敗した | controller_utils.go#L600 |
| SuccessfulDelete | Normal | 管理下のPodの削除に成功した | controller_utils.go#L634 |
| FailedDelete | Warning | 管理下のPodの削除に失敗した | controller_utils.go#L631 |
Deployment固有のEventはこれに加えて次のものがあります。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| ScalingReplicaSet | Normal | 配下のReplicaSetのレプリカ数を増減させた | sync.go#L432 |
| DeploymentRollback(RollbackDone) | Normal | ロールバックが完了した | rollback.go#L108-L109 |
| DeploymentRollbackRevisionNotFound | Warning | ロールバック先のリビジョンが見つからない | rollback.go#L45 |
なお ProgressDeadlineExceeded や NewReplicaSetAvailable は名前だけ見るとEventにありそうですが、pkg/controller/deployment/*.go の範囲では .status.conditions[].reason としてのみ使われており、EventRecorder経由のEventとしては発行されていません(参考: progress.go#L60)。kubectl describe deployment の出力では Conditions: 欄と Events: 欄は別物なので、混同しないよう注意が必要です。
DaemonSet・Jobにも個別のReasonがあります。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| FailedDaemonPod | Warning | DaemonSetのPod作成に失敗した | daemon_controller.go#L894 |
| Suspended | Normal | Jobが一時停止(spec.suspend: true)された |
job_controller.go#L1178 |
| Resumed | Normal | Jobの一時停止が解除された | job_controller.go#L1189 |
| Completed | Normal | Jobが完了した | job_controller.go#L1647 |
StatefulSetは少し毛色が違い、recordPodEvent/recordClaimEvent という共通関数の中で、verb(create/update/deleteなど)から Successful<Verb> / Failed<Verb> というReason文字列をその場で組み立てています。
func (spc *StatefulPodControl) recordPodEvent(verb string, set *apps.StatefulSet, pod *v1.Pod, err error) {
if err == nil {
reason := fmt.Sprintf("Successful%s", cases.Title(language.English).String(verb))
...
spc.recorder.Event(set, v1.EventTypeNormal, reason, message)
} else {
reason := fmt.Sprintf("Failed%s", cases.Title(language.English).String(verb))
...
spc.recorder.Event(set, v1.EventTypeWarning, reason, message)
}
}
(参考: stateful_pod_control.go#L333-L343)
verb に "create" が渡された場合は結果として SuccessfulCreate / FailedCreate になり、他のワークロードコントローラーと同じ文字列になりますが、これは共通のReason定数を参照しているわけではなく、たまたま同じ文字列が生成されているだけです。verb に別の値(update 等)が渡されれば SuccessfulUpdate のようにReasonも変わります。
第10章: In-place Pod Resize系Event
CPU/メモリのrequests・limitsをPodを再作成せずに変更する In-place Pod Resize 機能に関連するEventです。pkg/kubelet/allocation/allocation_manager.go と pkg/kubelet/kubelet.go から発行されます。
| Reason | Type | 発生条件 | ソース |
|---|---|---|---|
| ResizeStarted | Normal | リソース変更の適用を開始した | allocation_manager.go#L626 |
| ResizeDeferred | Normal | ノードの空きリソース不足等で変更を一時保留した | allocation_manager.go#L632 |
| ResizeInfeasible | Warning | そのノードでは変更を実行できないと判断した | allocation_manager.go#L634 |
| ResizeCompleted | Normal | リソース変更が完了した | kubelet.go#L2100 |
| ResizeError | Warning | リソース変更の適用中にエラーが発生した | kubelet.go#L2300 |
ResizeDeferred は他のノードへの再スケジュールを伴わず「今は無理なので待つ」という状態を表しており、FailedScheduling のような即時の失敗とは性質が異なります。この機能を使っている場合、kubectl describe pod のEvents欄にこれらのReasonが出ていないか確認すると、リソース変更がなぜ反映されないのかを追いやすくなります。
おわりに
自分は割とEventログを見ている方だと思っていましたが、実際に調べてみると知らないEventも多く、面白かったです。
普段よく見ているのはこの辺りで、ここまで分かれば基本的なトラブルシュートは一通りできるようになると思います。
- スケジューリング系
- コンテナ起動・イメージ取得系
- プローブ(ヘルスチェック)系
- リソース逼迫・Eviction系
In-place Pod ResizeのEventログは今回初めて把握しましたが、かなり興味を持ちました。Resize系は利用される環境が今後増えていきそうで、目にする機会も増えてくるのではないかと思っています。
参考
参考リンク
本文で登場する順に並べています。