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 podscheduler・kubelet がこの Pod に何をしたのか (Events)
k logsアプリが死ぬ直前にどんなメッセージを残したか
k logs --previous再起動の直前、つまり 死んだそのコンテナ が何を言い残したか
k get eventsクラスター次元で最近何が起きたか

試験ポイント: describe の Events を最初に #

最もよくある失敗が k logs から見ることです。Pending 状態ならコンテナがそもそも上がっていないのでログがありません。ImagePull 失敗もコンテナ開始前なのでログがありません。こういう場合の答えはすべて describeEvents セクション に書かれています。だから順序は常に 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 hungry

Events セクションで、次のような行が核心です。

Events:
  Warning  FailedScheduling  ... 0/3 nodes are available:
  3 Insufficient cpu. preemption: 0/3 nodes are available ...

FailedScheduling メッセージが 理由をそのまま言ってくれます。Pending の原因はほぼこの一行で分かれます。

Events に見える文言原因解決
Insufficient cpu / Insufficient memoryrequests がノードの空きより大きいrequests を下げるか、ノード増設/空き確保
node(s) didn't match node selectornodeSelector ラベルがどのノードにもないラベルをノードに追加するか selector を修正
node(s) had untolerated taintノードの taint に toleration がないPod に toleration を追加
pod has unbound immediate PersistentVolumeClaimsPVC が PV にバインドされていないPV/StorageClass を確認、PVC バインドを解決

解決 #

リソース不足なら requests を現実的な値に下げます。

k delete pod hungry
k run hungry --image=nginx

nodeSelector 不一致なら、ノードラベルを確認して合わせます。

# どのラベルを要求しているか
k get pod <name> -o jsonpath='{.spec.nodeSelector}'

# ノードにそのラベルがあるか
k get nodes --show-labels

# ノードにラベルを付けて解決する場合
k label node node01 disktype=ssd

taint が原因なら toleration を追加するか (下記)、意図した taint でなければノードから taint を削除します。

tolerations:
- key: "key1"
  operator: "Exists"
  effect: "NoSchedule"

PVC 未バインドは k get pvck 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/argsexec: "..." : not found、exit 127command/args をイメージに合わせて修正
必須設定の欠落logs に missing env、接続失敗ConfigMap/Secret のマウント・キーを確認
liveness probe 失敗describe Events に Liveness probe failedprobe のパス・ポート・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.yaml

3) 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 badimg

Events で次の行を見ます。

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.27

private レジストリの認証失敗なら、imagePullSecret を作って ServiceAccount か Pod スペックに接続します。

k create secret docker-registry regcred \
  --docker-server=<registry> \
  --docker-username=<user> \
  --docker-password=<pass>
spec:
  imagePullSecrets:
  - name: regcred

4) OOMKilled: メモリ limit を超えた #

OOMKilled は コンテナが自身のメモリ limit を超過し、カーネルの OOM killer によって強制終了された 状態です。特徴的なシグナルは 終了コード 137 (128 + SIGKILL 9) です。コンテナは上がって死にましたが、殺した主体がアプリではなくカーネルなので、ログには痕跡がないことがあります。手がかりは describeLast 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 oom

describe で次の部分が決定的です。

    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137

Reason: OOMKilledExit Code: 137 が一緒に見えたら、メモリ不足が確定です。RESTARTS が一緒に増えると STATUS は CrashLoopBackOff に見えることがあるので、終了コード 137 を手がかりにメモリ問題を切り分けます。

解決 #

原因は 2 つのうちのどちらかです。limit がアプリの実際の使用量より非現実的に低いか (設定問題)、アプリが実際にメモリを使いすぎているか (アプリ問題) です。

# 普段の使用量を見る (metrics-server が必要)
k top pod oom

limit が低すぎるのが原因なら、現実的な値に上げます。

resources:
  requests:
    memory: "128Mi"
  limits:
    memory: "256Mi"

requests と limits の関係、QoS クラス (BestEffort/Burstable/Guaranteed) が OOM 時にどの Pod から終了するかに影響を与える点は、リソース管理編 (#15) で扱った内容をそのまま適用します。運用環境でメモリ・CPU をどう観測してアラートをかけるかは、オブザーバビリティ編 でメトリクス軸に整理しました。

一枚で見る診断フロー #

試験会場で Pod 障害に出会ったら、次の順序で下りていきます。

  1. k get pod -o wide で STATUS と RESTARTS を見る
  2. 無条件で k describe pod の Events を最初に読む
  3. STATUS が Pending なら → Events の FailedScheduling 文言でリソース/selector/taint/PVC に分岐
  4. ImagePull 系なら → Events の Failed to pull image 文言でタグ/認証/ネットワークに分岐
  5. コンテナが上がって死んだなら → k logs --previous でアプリメッセージを確認
  6. describe の Last State に OOMKilled / Exit Code: 137 なら → メモリ limit 問題として処理

このフローの出発点は常に同じです。推測せず describe の Events から読む。 この一つの習慣がトラブルシューティング 30% の半分を持っていきます。

まとめ #

この記事で押さえたこと:

  • Troubleshooting は CKA の最大ドメイン (30%)。壊れたものを素早く直す診断速度が点数を分ける
  • 診断ツールは k describe (Events)、k logs --previousk get eventsk get pod -o widedescribe の Events を常に最初に 読む
  • Pending。scheduler がノードを見つけられない状態。FailedScheduling 文言でリソース不足・nodeSelector・taint・PVC 未バインドに分岐
  • CrashLoopBackOff。上がって死んだ。logs --previous でアプリエラー・誤った command・probe 失敗を確認
  • ImagePullBackOff / ErrImagePull。イメージを取得できない状態。Events でタグの打ち間違い・レジストリ認証・ネットワークに分岐
  • OOMKilled。メモリ limit 超過。describe の Last State に Reason: OOMKilledexit code 137

次へ: トラブルシューティング 2 #

Pod レベルは押さえました。ところが、まともなマニフェストの Pod なのに上がらず、さらにはノード全体が NotReady に落ちるケースがあります。そうなると一段下、ノードと kubelet へ下りていく必要があります。

#23 トラブルシューティング 2: ノードと kubelet では、ノードが NotReady になる原因を追跡します。kubelet サービスが死んだ、証明書・kubeconfig がずれた、ノードに disk pressure・memory pressure がかかった、といったケースを systemctl status kubeletjournalctl -u kubelet で下りながら診断して復旧します。

X