クラスタと namespace を固定し、Pod → EndpointSlice → Service → Ingress の関係から問題を絞る。状態、probe、疎通、業務の正しさは別々の根拠である。
context と namespace を固定
kubectl config get-contexts
星印が現在の選択で、current-context はその名前だけを表示する。以降の your-context と default を確認済みの値に置き換える。各コマンドで明示し、前の変数に依存させない。
kubectl --context your-context -n default get deploy,svc,ingress,pvc
各層の設定が存在するかを確認する。Pod の配置は次で調べる。NotFound は名前と範囲、Forbidden は権限を確認する。
kubectl --context your-context -n default get pods -o wide
-o wide は IP・ノードなどを追加する。your-pod はこの一覧、コンテナ名 app は describe の Containers で確認する。
kubectl --context your-context get nodes -o wide
Ready でもすべてが配置可能とは限らない。requests・taint・affinity も確認する。
kubectl --context your-context -n default auth can-i get pods
yes / no は指定 verb/resource の認可だけを示し、API の稼働や他の操作の許可は保証しない。
例の context・namespace・リソース名を確認済みの対象に置き換える。Secret 一覧を見るために中身を出力する必要はない。--as による代理実行にも権限が必要。
Pod 状態は停止段階を示し原因は別
イベントとコンテナごとの状態を先に読む
kubectl --context your-context -n default describe pod your-pod
State と Last State を区別し、終了コード・理由・時刻・回数を記録する。イベントは期限で消えるため、出力なしでも異常がなかったとは限らない。
kubectl --context your-context -n default logs your-pod -c app --tail=100
-c は Pod 内のコンテナを選ぶ。複数なら明示する。-f で追跡し Ctrl-C で表示を終了する。
kubectl --context your-context -n default logs your-pod -c app --previous --tail=100
再起動ループではこちらを読む。前のインスタンスやログが残っていない場合のエラーは、再起動の指示ではない。
kubectl --context your-context -n default get events --sort-by=.metadata.creationTimestamp
繰り返し発生した全件の時系列ではない。describe の最終時刻と回数を照合し、下表で確認先を選ぶ。
| 現象 | 確認 |
| Pending / FailedScheduling | Events の requests、quota、affinity、taint、PVC。CPU の空きだけではない |
| ImagePullBackOff | ノードからのイメージ名・tag・registry 接続・認証・制限 |
| CrashLoopBackOff | 終了コード、lastState、前回ログ、startup/liveness probe。再試行待機状態を表す |
| Running / 0/1 Ready | readiness probe/gate と各コンテナ。Running は配信可能と同義ではない |
設定項目と通信経路を対応させる
図を準備しています
Service の選択対象と転送先通常 Service は label 一致・Ready backend・正しい targetPort が必要。Running や port-forward 成功だけで ClusterIP 経路は保証されない。
{"id":"ops-k8s-service","title":"Service の選択対象と転送先","summary":"通常 Service は label 一致・Ready backend・正しい targetPort が必要。Running や port-forward 成功だけで ClusterIP 経路は保証されない。","height":600,"html":"<main><p class=\"quiet\" data-i18n=\"scope\"></p><div class=\"checks\"><label><input id=\"match\" type=\"checkbox\" checked><span data-i18n=\"match\"></span></label><label><input id=\"ready\" type=\"checkbox\" checked><span data-i18n=\"ready\"></span></label><label><input id=\"port\" type=\"checkbox\" checked><span data-i18n=\"port\"></span></label></div><div class=\"flow\"><div class=\"row\" data-i18n=\"service\"></div><div class=\"arrow\" id=\"edge1\">↓</div><div class=\"row code\" id=\"endpoint\"></div><div class=\"arrow\" id=\"edge2\">↓</div><div class=\"row\"><span data-i18n=\"pod\"></span> :8080</div></div><div class=\"explain\"><div class=\"stable \"><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"yes\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"labels\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"notready\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"wrongport\"></p><p id=\"message\" aria-live=\"polite\"></p></div></div></main>\n","css":"main {\n display: grid;\n gap: 18px;\n min-width: 0;\n}\np {\n overflow-wrap: anywhere;\n}\n.controls {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n}\n.presets {\n display: flex;\n flex-wrap: wrap;\n}\n.presets button {\n flex: 1 1 85px;\n font-size: 15px;\n}\n.cards {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 10px;\n}\n.card {\n border: 1px solid var(--rule);\n border-radius: 10px;\n padding: 14px;\n min-width: 0;\n background: var(--surface);\n}\n.card h3 {\n font-size: 15px;\n color: var(--muted);\n margin-bottom: 8px;\n}\n.card strong {\n font-size: 23px;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.code {\n font-family: var(--viz-font-mono);\n font-size: 15px;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.stable {\n display: grid;\n min-width: 0;\n}\n.stable > * {\n grid-area: 1/1;\n margin: 0;\n}\n.stable > .reserve {\n visibility: hidden;\n pointer-events: none;\n}\n.explain {\n border-left: 3px solid var(--accent);\n padding: 12px 14px;\n background: var(--accent-soft);\n}\n.arrow {\n text-align: center;\n color: var(--accent);\n font-size: 25px;\n}\n.row {\n padding: 12px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n min-width: 0;\n}\n.row.on {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n.row.off {\n color: var(--muted);\n}\n.bar {\n height: 18px;\n background: var(--surface);\n border: 1px solid var(--rule);\n border-radius: 5px;\n overflow: hidden;\n margin-top: 8px;\n}\n.bar > i {\n display: block;\n height: 100%;\n background: var(--accent);\n width: 0;\n transition: width 0.22s;\n}\n.checks {\n display: grid;\n gap: 8px;\n}\n.checks label {\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 9px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n font-size: 16px;\n}\n.checks input {\n width: 18px;\n height: 18px;\n accent-color: var(--accent);\n flex-shrink: 0;\n}\nlabel.range {\n display: block;\n font-size: 16px;\n}\nlabel.range output {\n float: right;\n font-variant-numeric: tabular-nums;\n}\n.pair {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n}\n.flow {\n display: grid;\n gap: 6px;\n}\n.quiet {\n font-size: 14px;\n color: var(--muted);\n}\n.badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 5px;\n background: var(--accent-soft);\n color: var(--accent);\n}\nbutton {\n overflow-wrap: anywhere;\n}\n.large {\n font-size: 32px;\n color: var(--accent);\n font-weight: 500;\n}\n.layers {\n display: grid;\n gap: 12px;\n}\n.layer {\n border-left: 3px solid var(--rule);\n padding-left: 12px;\n}\n.layer.on {\n border-color: var(--accent);\n}\n@media (max-width: 520px) {\n .cards,\n .pair {\n grid-template-columns: 1fr;\n }\n .card {\n padding: 12px;\n }\n .card strong {\n font-size: 22px;\n }\n}\n","js":"const $ = (s) => document.querySelector(s);\nconst $$ = (s) => [...document.querySelectorAll(s)];\nconst t = (k) => viz.t(k);\nconst put = (id, v) => ($(\"#\" + id).textContent = v);\nconst say = (k) => put(\"message\", t(k));\nconst state = (v) => ($(\"main\").dataset.state = JSON.stringify(v));\nconst press = (key, v) =>\n $$(\"[data-\" + key + \"]\").forEach((b) =>\n b.setAttribute(\"aria-pressed\", String(b.dataset[key] === v)),\n );\nfunction draw() {\n const match = $(\"#match\").checked,\n ready = $(\"#ready\").checked,\n port = $(\"#port\").checked ? 8080 : 80;\n put(\n \"endpoint\",\n match ? \"EndpointSlice\\n10.0.0.8:\" + port + \"\\nready: \" + ready : \"EndpointSlice\\n∅\\n—\",\n );\n put(\"edge1\", match ? \"↓\" : \"×\");\n put(\"edge2\", match && ready && port === 8080 ? \"↓\" : \"×\");\n $(\"#endpoint\").classList.toggle(\"on\", match && ready);\n say(!match ? \"labels\" : !ready ? \"notready\" : port !== 8080 ? \"wrongport\" : \"yes\");\n state({ match, ready, port, reachable: match && ready && port === 8080 });\n}\n$$(\"input\").forEach((e) => (e.onchange = draw));\ndraw();\n","audio":false,"strings":{"labels":"label 一致 backend なし。targetPort を変えても候補は増えない。","match":"label が app=web に一致","notready":"Pod は選択されるが endpoint は非 Ready。通常 traffic の転送対象にならない。","pod":"Pod アプリ待受け","port":"targetPort=8080(他は80)","ready":"Pod Ready","scope":"Running Pod 一つ、8080 待受け。publishNotReadyAddresses=false、他障害を省略。各条件を独立変更する。","service":"client → Service :80","wrongport":"backend は有効だが80へ転送。アプリは8080なので接続失敗。","yes":"選択・Ready・port が成立し、この模型ではアプリに到達する。"}}apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- name: http
port: 80
targetPort: 8080
同じ namespace の app: web の Pod を選び、8080 に接続する。アプリ自身の待ち受けが必要。containerPort はポートを開く設定ではなく、整数 targetPort に必須ではない。名前付き targetPort には対応する名前付きポートが必要。
selector・endpoint・port を順に照合する
上の YAML は Service オブジェクトの例で、Pod は作らない。service.yaml に保存する場合も公開手順で検証・適用する。
kubectl --context your-context -n default get pods -l app=web --show-labels
出力がなければポート変更の前にラベルと namespace を確認する。
kubectl --context your-context -n default get endpointslice -l kubernetes.io/service-name=web -o yaml
複数 slice の endpoints.addresses・conditions.ready・ports.port を確認する。存在しても NetworkPolicy・CNI・待ち受け先の確認は必要。
kubectl --context your-context -n default describe service web
Service への接続は port、アプリの待ち受けは targetPort。名前指定は Pod の同名ポートへ解決できる必要がある。
port-forward が通る経路だけを検証する
kubectl --context your-context -n default port-forward service/web 8080:80
この端末を維持し、別端末で curl http://127.0.0.1:8080/ を実行する。Ctrl-C で停止。選択された Pod が終了した場合は再接続が必要になる。
kubectl --context your-context -n default exec client-pod -c app -- curl -v http://web.default.svc.cluster.local/
curl がある許可対象の client-pod/app、実際の namespace と集群ドメインに置き換える。その主体で DNS と Service 経路を検証し、Ingress は外部から別途調べる。
EndpointSlice に宛先がなければ selector と namespace、未準備なら readiness を確認。port-forward は API 経由で Pod に接続し、ClusterIP・クラスタ DNS・Ingress は検証しない。クラスタ内クライアントから Service を検証し、Ingress の host/path/backend、コントローラー、外部入口を別に調べる。
展開とデバッグの作用範囲を確認
定義確認・API 検証・適用を分ける
kubectl --context your-context -n default diff -f manifest.yaml
manifest.yaml は今回の変更ファイル。内部の namespace と集群単位の対象も確認する。0 は差分なし、1 は差分あり。それ以外は先に解決する。
kubectl --context your-context -n default apply --dry-run=server -f manifest.yaml
API・admission に接続する。失敗を直す。成功してもイメージ取得や動作は保証されない。
kubectl --context your-context -n default apply -f manifest.yaml
created/configured/unchanged の対象が意図通りか確認してから rollout を観察する。
kubectl --context your-context -n default rollout status deployment/web --timeout=120s
タイムアウト時は新 ReplicaSet・イベント・ログを調べる。成功後も実リクエストを確認する。待機タイムアウトは自動 rollback ではない。
revision を確認してから戻す
kubectl --context your-context -n default rollout history deployment/web
rollout history deployment/web --revision=2 で候補を読む。2 は確認した revision に置き換える。
kubectl --context your-context -n default rollout undo deployment/web --to-revision=2
rollout status・Pod・実リクエストを再確認する。DB、外部システム、独立 ConfigMap の内容は戻らない。
diff の終了値1は通常差分あり。server dry-run は API/admission にアクセスする。展開後も実リクエストを確認する。rollout undo はテンプレート履歴を戻すだけで DB は戻さない。restart は新 Pod を作るがイメージタグ自体は変えない。
ツールがあるコンテナでは exec を使う
kubectl --context your-context -n default exec -it your-pod -c app -- sh
-- 以降が内部のコマンド。-i は入力、-t は端末を割り当てる。主プロセスを置き換えず、終了しても Pod は削除されない。
最小イメージでは一時診断コンテナを選ぶ
kubectl --context your-context -n default debug -it your-pod --image=busybox:1.37 --target=app
exec の代替であり権限・ポリシーが必要。承認済みイメージを使う。終了後も記録は Pod に残り、一時コンテナの項目だけを削除できない。
ファイルコピーの方向とコンテナを明示する
kubectl --context your-context -n default cp your-pod:/tmp/report.txt ./report.txt -c app
必要な既存ファイルを上書きせず、内容・サイズを確認する。方向を逆にするとコンテナへ書く。通常、内部の tar が必要なので失敗時はエラーを読む。
kubectl debug は既存 Pod に一時コンテナを追加し、ポリシーとランタイムの制約がある。kubectl run は別 Pod を作りネットワーク条件が異なり得る。cp は通常コンテナ内 tar が必要。管理対象 Pod を削除する前にログとイベントを保存する。
Debug Services · Debug Pods · EndpointSlices