Certified Kubernetes Administrator (CKA) #22 トラブルシューティング 1: Pod とアプリ (Pending、CrashLoop、ImagePull、OOM)
#21 Helm と Kustomize までで、マニフェストを作ってデプロイするドメインをすべて終えました。ここから 4 編は すでに壊れたものを直す トラブルシューティングです。CKA 試験で Troubleshooting は 30% と最も比重が大きいドメイン です。5 つのドメインのうち、何かを新しく作る作業よりも、誰かが壊しておいたクラスターを追跡して直す作業の点数が最も大きいのです。合格ラインが 66% なので、この 30% を落とすとほぼ落ちます。
トラブルシューティングの核心は 推測しないこと です。症状を見て原因を頭の中で当てる代わりに、クラスターがすでに記録しておいた事実 (describe の Events、コンテナログ、終了コード) を順番に読み下していけば、原因はほぼ自ら姿を現します。今回は Pod レベルの障害 4 つを、その順序で診断します。
なぜ Troubleshooting が 30% なのか #
CKA の 5 つのドメインのうち Troubleshooting が 30% と単一最大です。2 番目の Cluster Architecture (25%) と合わせると半分を超えます。この比重は偶然ではありません。クラスター管理者の実際の業務が、新しいリソースを作る仕事よりも 動いていたものが止まったときに原因を見つけて復旧する仕事 に近いからです。
そのため試験でもトラブルシューティングの問題は「この Pod がなぜ上がらないのか直せ」というふうに、すでに壊れた状態 を投げてきます。マニフェストを最初から書く問題と違い、壊れた箇所を素早く突き止める診断速度が点数を分けます。今回はその中で最もよく出る Pod レベルの障害だけを扱い、ノード (#23)・control plane (#24)・ネットワーキング (#25) は次の編に続きます。
診断ツール: 読む順序がすべてです #
トラブルシューティングでコマンド自体はいくつもありません。重要なのは どの順序で読むか です。次の順序を体に染み込ませておけば、ほとんどの Pod 障害は 1〜2 分以内に原因が姿を現します。
# 1) 全体の状態と STATUS、RESTARTS、AGE をまず見る
k get pod -o wide
# 2) describe の Events を最初に読む (診断の 90% がここ)
k describe pod <name>
# 3) コンテナが上がってから死んだ場合、前のコンテナのログを見る
k logs <name>
k logs <name> --previous
# 4) コンテナが複数なら -c で指定
k logs <name> -c <container>
# 5) クラスター全体のイベントを時系列で
k get events --sort-by=.metadata.creationTimestamp各ツールが答える質問は異なります。
| ツール | 答えてくれる質問 |
|---|---|
k get pod -o wide | いま STATUS は何で、どのノードに上がり、RESTARTS は何回か |
k describe pod | scheduler・kubelet がこの Pod に何をしたのか (Events) |
k logs | アプリが死ぬ直前にどんなメッセージを残したか |
k logs --previous | 再起動の直前、つまり 死んだそのコンテナ が何を言い残したか |
k get events | クラスター次元で最近何が起きたか |
試験ポイント: describe の Events を最初に #
最もよくある失敗が k logs から見ることです。Pending 状態ならコンテナがそもそも上がっていないのでログがありません。ImagePull 失敗もコンテナ開始前なのでログがありません。こういう場合の答えはすべて describe の Events セクション に書かれています。だから順序は常に describe (events) が先、logs はその次 です。CrashLoop のようにコンテナが上がってから死んだ場合にだけ、logs --previous が決定的な手がかりになります。
症状別の原因と一次診断 #
4 つの症状を一つの表でまず押さえ、それぞれを再現・解決へと下りていきます。
| 症状 (STATUS) | コンテナが上がったか | 一次に見る場所 | 代表的な原因 |
|---|---|---|---|
| Pending | 上がらない | describe Events | リソース不足、nodeSelector 不一致、taint、PVC 未バインド |
| CrashLoopBackOff | 上がって死んだ | logs --previous | アプリエラー、誤った command、probe 失敗 |
| ImagePullBackOff / ErrImagePull | 上がらない | describe Events | イメージタグの打ち間違い、レジストリ認証失敗 |
| OOMKilled | 上がって死んだ | describe の Last State | メモリ limit 超過 (exit code 137) |
この表の 2 列目が診断の分岐点です。コンテナがまだ上がっていないなら describe Events (scheduler/kubelet の話)、上がって死んだなら logs –previous (アプリの話) を見ます。
1) Pending: スケジュールされない #
Pending は kube-scheduler がこの Pod を載せるノードを見つけられなかった 状態です。コンテナはまだ開始すらしていないのでログがありません。答えは describe の Events にあります。
再現 #
リソースを過度に要求して、どのノードにも合わないようにします。
k run hungry --image=nginx \
--overrides='{"spec":{"containers":[{"name":"hungry","image":"nginx","resources":{"requests":{"cpu":"100"}}}]}}'診断 #
k get pod hungry
# NAME READY STATUS RESTARTS AGE
# hungry 0/1 Pending 0 20s
k describe pod hungryEvents セクションで、次のような行が核心です。
Events:
Warning FailedScheduling ... 0/3 nodes are available:
3 Insufficient cpu. preemption: 0/3 nodes are available ...FailedScheduling メッセージが 理由をそのまま言ってくれます。Pending の原因はほぼこの一行で分かれます。
| Events に見える文言 | 原因 | 解決 |
|---|---|---|
Insufficient cpu / Insufficient memory | requests がノードの空きより大きい | requests を下げるか、ノード増設/空き確保 |
node(s) didn't match node selector | nodeSelector ラベルがどのノードにもない | ラベルをノードに追加するか selector を修正 |
node(s) had untolerated taint | ノードの taint に toleration がない | Pod に toleration を追加 |
pod has unbound immediate PersistentVolumeClaims | PVC が PV にバインドされていない | PV/StorageClass を確認、PVC バインドを解決 |
解決 #
リソース不足なら requests を現実的な値に下げます。
k delete pod hungry
k run hungry --image=nginxnodeSelector 不一致なら、ノードラベルを確認して合わせます。
# どのラベルを要求しているか
k get pod <name> -o jsonpath='{.spec.nodeSelector}'
# ノードにそのラベルがあるか
k get nodes --show-labels
# ノードにラベルを付けて解決する場合
k label node node01 disktype=ssdtaint が原因なら toleration を追加するか (下記)、意図した taint でなければノードから taint を削除します。
tolerations:
- key: "key1"
operator: "Exists"
effect: "NoSchedule"PVC 未バインドは k get pvc と k get pv で状態を確認します。StorageClass の動的プロビジョニングがあれば自動でバインドされるはずで、静的なら合致する PV が存在しなければなりません。この部分の深い診断は、ストレージ編 (#16・#17) の内容をそのまま使います。
2) CrashLoopBackOff: 上がってから死に続ける #
CrashLoopBackOff は コンテナは開始したが、すぐ終了し、kubelet が再起動を繰り返しながら次第に backoff (待ち時間) を延ばしていく 状態です。RESTARTS の数字が上がり続けます。ここでは 死んだそのコンテナのログ、つまり logs --previous が決定的です。
再現 #
存在しないコマンドを実行して即座に終了させます。
k run crasher --image=busybox --restart=Always -- /bin/sh -c "exit 1"診断 #
k get pod crasher
# NAME READY STATUS RESTARTS AGE
# crasher 0/1 CrashLoopBackOff 3 (20s ago) 60s
# いまのコンテナは backoff 中で空かもしれない
k logs crasher
# 死んだそのコンテナの最後のログ
k logs crasher --previous--previous がないと、backoff で待機中の空のコンテナを見ることになり手がかりを逃します。CrashLoop の診断はほぼ常に --previous で見ます。
原因と解決 #
| 原因 | describe / logs での手がかり | 解決 |
|---|---|---|
| アプリ自体のエラーで終了 | logs にスタックトレース・エラーメッセージ | アプリ設定 (環境変数・ConfigMap) を修正 |
| 誤った command/args | exec: "..." : not found、exit 127 | command/args をイメージに合わせて修正 |
| 必須設定の欠落 | logs に missing env、接続失敗 | ConfigMap/Secret のマウント・キーを確認 |
| liveness probe 失敗 | describe Events に Liveness probe failed | probe のパス・ポート・initialDelaySeconds を調整 |
probe 失敗が原因のケースは特に紛らわしいです。アプリは正常なのに liveness probe が早すぎる、あるいは誤ったパスで 検査して、kubelet が正常なコンテナを殺し続けるケースです。describe Events に Liveness probe failed: ... が見えたら probe 設定を疑います。
# probe 設定の確認
k get pod crasher -o jsonpath='{.spec.containers[0].livenessProbe}'command の打ち間違いによる終了なら、マニフェストの command/args を直します。試験では Deployment を直接 edit するか、マニフェストを修正して再適用します。
k edit deploy <name>
# またはマニフェスト修正後
k apply -f deploy.yaml3) ImagePullBackOff / ErrImagePull: イメージを取得できない #
この 2 つは kubelet がコンテナイメージを引っ張ってこられなかった 状態です。ErrImagePull が先に出て、再試行が backoff に入ると ImagePullBackOff になります。コンテナは開始すらできていないのでログはなく、原因は describe の Events にあります。
再現 #
存在しないタグを指定します。
k run badimg --image=nginx:doesnotexist診断 #
k get pod badimg
# NAME READY STATUS RESTARTS AGE
# badimg 0/1 ImagePullBackOff 0 30s
k describe pod badimgEvents で次の行を見ます。
Events:
Warning Failed ... Failed to pull image "nginx:doesnotexist":
... manifest for nginx:doesnotexist not found原因と解決 #
| Events の手がかり | 原因 | 解決 |
|---|---|---|
manifest for ... not found | イメージ名・タグの打ち間違い | イメージ/タグを正しい値に修正 |
repository does not exist | レジストリパスの打ち間違い、private 保存先 | 全体パス (registry/repo:tag) を確認 |
pull access denied / unauthorized | レジストリ認証失敗 | imagePullSecrets を設定・接続 |
no such host / タイムアウト | ノードからレジストリに到達不可 | ノードのネットワーク・DNS を確認 |
タグの打ち間違いが最もよくあります。正しいタグに直します。
k set image pod/badimg badimg=nginx:1.27
# Deployment なら
k set image deploy/<name> <container>=nginx:1.27private レジストリの認証失敗なら、imagePullSecret を作って ServiceAccount か Pod スペックに接続します。
k create secret docker-registry regcred \
--docker-server=<registry> \
--docker-username=<user> \
--docker-password=<pass>spec:
imagePullSecrets:
- name: regcred4) OOMKilled: メモリ limit を超えた #
OOMKilled は コンテナが自身のメモリ limit を超過し、カーネルの OOM killer によって強制終了された 状態です。特徴的なシグナルは 終了コード 137 (128 + SIGKILL 9) です。コンテナは上がって死にましたが、殺した主体がアプリではなくカーネルなので、ログには痕跡がないことがあります。手がかりは describe の Last State にあります。
再現 #
小さな limit をかけて、それより多くのメモリを使わせます。
k run oom --image=polinux/stress \
--overrides='{"spec":{"containers":[{"name":"oom","image":"polinux/stress","resources":{"limits":{"memory":"20Mi"}},"command":["stress"],"args":["--vm","1","--vm-bytes","250M"]}]}}'診断 #
k get pod oom
# NAME READY STATUS RESTARTS AGE
# oom 0/1 OOMKilled 2 (10s ago) 40s # または CrashLoopBackOff として繰り返す
k describe pod oomdescribe で次の部分が決定的です。
Last State: Terminated
Reason: OOMKilled
Exit Code: 137Reason: OOMKilled と Exit Code: 137 が一緒に見えたら、メモリ不足が確定です。RESTARTS が一緒に増えると STATUS は CrashLoopBackOff に見えることがあるので、終了コード 137 を手がかりにメモリ問題を切り分けます。
解決 #
原因は 2 つのうちのどちらかです。limit がアプリの実際の使用量より非現実的に低いか (設定問題)、アプリが実際にメモリを使いすぎているか (アプリ問題) です。
# 普段の使用量を見る (metrics-server が必要)
k top pod oomlimit が低すぎるのが原因なら、現実的な値に上げます。
resources:
requests:
memory: "128Mi"
limits:
memory: "256Mi"requests と limits の関係、QoS クラス (BestEffort/Burstable/Guaranteed) が OOM 時にどの Pod から終了するかに影響を与える点は、リソース管理編 (#15) で扱った内容をそのまま適用します。運用環境でメモリ・CPU をどう観測してアラートをかけるかは、オブザーバビリティ編 でメトリクス軸に整理しました。
一枚で見る診断フロー #
試験会場で Pod 障害に出会ったら、次の順序で下りていきます。
k get pod -o wideで STATUS と RESTARTS を見る- 無条件で
k describe podの Events を最初に読む - STATUS が Pending なら → Events の
FailedScheduling文言でリソース/selector/taint/PVC に分岐 - ImagePull 系なら → Events の
Failed to pull image文言でタグ/認証/ネットワークに分岐 - コンテナが上がって死んだなら →
k logs --previousでアプリメッセージを確認 - describe の Last State に
OOMKilled/Exit Code: 137なら → メモリ limit 問題として処理
このフローの出発点は常に同じです。推測せず describe の Events から読む。 この一つの習慣がトラブルシューティング 30% の半分を持っていきます。
まとめ #
この記事で押さえたこと:
- Troubleshooting は CKA の最大ドメイン (30%)。壊れたものを素早く直す診断速度が点数を分ける
- 診断ツールは
k describe(Events)、k logs --previous、k get events、k get pod -o wide。describe の Events を常に最初に 読む - Pending。scheduler がノードを見つけられない状態。
FailedScheduling文言でリソース不足・nodeSelector・taint・PVC 未バインドに分岐 - CrashLoopBackOff。上がって死んだ。
logs --previousでアプリエラー・誤った command・probe 失敗を確認 - ImagePullBackOff / ErrImagePull。イメージを取得できない状態。Events でタグの打ち間違い・レジストリ認証・ネットワークに分岐
- OOMKilled。メモリ limit 超過。describe の Last State に
Reason: OOMKilledと exit code 137
次へ: トラブルシューティング 2 #
Pod レベルは押さえました。ところが、まともなマニフェストの Pod なのに上がらず、さらにはノード全体が NotReady に落ちるケースがあります。そうなると一段下、ノードと kubelet へ下りていく必要があります。
#23 トラブルシューティング 2: ノードと kubelet では、ノードが NotReady になる原因を追跡します。kubelet サービスが死んだ、証明書・kubeconfig がずれた、ノードに disk pressure・memory pressure がかかった、といったケースを systemctl status kubelet と journalctl -u kubelet で下りながら診断して復旧します。