콘텐츠로 이동
Study NoteCKA

트러블슈팅

배점 30% — 앞의 모든 장이 재료다

고치려 하지 말고, 먼저 “어느 층에서 끊겼는지”를 확정하라.

클러스터는 결국 선언된 상태(spec)를 실제 상태(status)로 맞추는 루프들의 모음이다 (시작하기 전에). 고장이란 그 루프가 어딘가에서 멈춘 것이고, 층을 가른다는 건 멈춘 지점이 루프의 어느 구간인지를 좁히는 일이다. 선언이 API 서버에 들어가고 → 컨트롤러·스케줄러가 그것을 읽어 결정하고 → kubelet이 노드에서 실행하고 → 런타임·CNI(Container Network Interface, Pod 네트워크)· CSI(Container Storage Interface, 볼륨 연결)가 실제로 만들어 내는 사슬에서, 층은 그 사슬을 끊어 보는 눈금이다.

층은 넷이다.

1 · 애플리케이션

Pod 자체의 문제. describe → logs → -o yaml

2 · 노드

kubelet, 런타임, 자원. journalctl -u kubelet, df -h

3 · 컨트롤 플레인

API 서버, 스케줄러, 컨트롤러, etcd. crictl ps -a, 매니페스트

4 · 네트워크

Service, DNS, CNI, 정책. get endpoints, nslookup

각 층은 확인 명령이 다르다. 층을 잘못 짚으면 엉뚱한 곳을 파게 된다. 시험에서는 이 판별이 곧 시간이다.

층을 정하는 질문은 셋, 치는 명령은 두 줄이다.

kubectl get nodes 응답 여부 · 노드 Ready 여부 · Pod Running 여부로 갈라져 애플리케이션 · 노드 · 컨트롤 플레인 · 네트워크 네 층 중 어디로 내려갈지 정하는 분기
터미널 창
kubectl get nodes # Q1 · Q2
kubectl get pods -A | grep -vE 'Running|Completed' # Q3
상황도구
왜 안 뜨는가kubectl describe → Events
앱이 무슨 말을 하는가kubectl logs, logs --previous
시간순으로 무슨 일이 있었나kubectl get events --sort-by=.lastTimestamp
정확한 값이 뭔가kubectl get ... -o yaml
자원을 얼마나 쓰나kubectl top, describe node
안에서 확인하고 싶다kubectl exec, kubectl debug, 임시 Pod
kubectl이 안 될 때crictl(컨테이너 런타임 직접 조회 CLI), journalctl -u kubelet(kubelet의 systemd 서비스 로그)
노드 자체systemctl status, df -h, free -m, dmesg

순서가 중요하다.

describe 의 Events 를 먼저 보고 logs --previous, get -o yaml 순으로 가는 순서와, 로그부터 보면 스케줄링 · 볼륨 · 이미지 문제에서 아무것도 안 나오는 이유

describe(Events) → logs → -o yaml. 로그부터 보면 스케줄링·볼륨·이미지 문제는 아무것도 안 나온다. 막혔을 때 시험 중에 열어 볼 곳도 정해져 있다 — 공식 Monitoring, Logging, and Debugging 문서가 애플리케이션/클러스터 갈래로 이 장과 같은 층 구분을 쓴다.

루프가 끝까지 돌긴 했는데 마지막 한 걸음에서 걸린 경우다 — 스케줄러는 노드를 정했고 kubelet도 만들라는 지시를 받았다. Pod의 STATUS는 그 마지막 걸음이 어디서 멈췄는지 알려 주는 눈금이라, 상태 이름만 읽어도 다음에 칠 명령이 정해진다.

STATUS를 보는 순간 다음 명령이 정해져야 한다.

Pod 의 STATUS 값 열 가지별로 스케줄링 · 볼륨 · 이미지 · 앱 · 리소스 · 설정 중 어디를 볼지와 다음 명령이 정해지는 진단 지도
STATUS층첫 명령
Pending스케줄링describe pod → Events
ContainerCreating볼륨/네트워크describe pod → Events
ImagePullBackOff이미지describe pod → Events
CrashLoopBackOff앱logs --previous
Error앱logs, exit code
OOMKilled리소스describe pod → Last State
CreateContainerConfigError설정describe pod → ConfigMap/Secret
Running 인데 0/1readinessProbedescribe pod → Events
Terminating 지속finalizer / 노드get pod -o yaml
Completed 반복restartPolicyJob 여부 확인
터미널 창
kubectl describe pod web | grep -A20 Events

Events 메시지 하나가 원인을 그대로 지목한다.

스케줄링 실패 시 Events 메시지 일곱 가지별 원인과 대응, Events 자체가 없으면 스케줄러를 의심한다는 분기
메시지원인조치
Insufficient cpu/memoryrequest 여유 부족request 낮추거나 노드 추가
had untolerated tainttainttoleration 추가 또는 taint 제거
didn't match node affinity/selector라벨 불일치노드에 라벨 추가 또는 셀렉터 수정
node(s) were unschedulablecordonkubectl uncordon
unbound immediate PersistentVolumeClaimsPVC 미바인딩스토리지 진단
didn't match pod anti-affinity rulesantiAffinity스케줄링
Events 자체가 없다스케줄러가 죽었다 또는 schedulerName 오타컨트롤 플레인 확인
터미널 창
kubectl describe node node01 | grep -A8 'Allocated resources'
kubectl get nodes # SchedulingDisabled 표시
터미널 창
kubectl describe pod web | grep -A5 Events
# Failed to pull image "nginx:1.99": rpc error: ... not found
Events 내용원인
not found / manifest unknown이미지 이름·태그 오타
unauthorized / authentication requiredimagePullSecrets 없음
dial tcp: i/o timeout네트워크·레지스트리 접근 불가
no such host레지스트리 도메인 해석 실패
터미널 창
# 이미지 이름이 맞는지 확인
kubectl get pod web -o jsonpath='{.spec.containers[*].image}'
# 노드에서 직접 받아보기
sudo crictl pull nginx:1.27
# 고치기
kubectl set image deploy/web nginx=nginx:1.27
터미널 창
kubectl logs web --previous # ★ 죽기 직전 로그
kubectl describe pod web | grep -A15 'Last State'
# Last State: Terminated
# Reason: Error
# Exit Code: 1

exit code 하나로 절반이 좁혀진다.

CrashLoopBackOff 에서 logs --previous 로 로그 유무를 가르고, 로그가 있으면 Exit Code 0 · 1 · 2 · 126 · 127 · 137 · 143 별 원인으로, 없으면 liveness 프로브를 의심하는 진단
Exit Code의미다음 행동
0정상 종료했는데 restartPolicy: Always명령이 즉시 끝난다 — sleep이 필요하거나 Job이어야 한다
1, 2앱 에러로그를 읽는다
126실행 권한 없음command 경로·권한
127명령을 못 찾음command 오타 또는 이미지에 그 바이너리가 없다
137SIGKILLOOMKilled 또는 liveness 실패로 강제 종료
143SIGTERM정상 종료 신호

OOM(Out of Memory — 메모리 한도 초과)으로 커널이 컨테이너를 강제 종료한 상태다.

터미널 창
kubectl describe pod web | grep -A8 'Last State'
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
터미널 창
kubectl top pod web --containers # 실제 사용량
kubectl get pod web -o jsonpath='{.spec.containers[0].resources}'

조치 순서

  1. kubectl top으로 실제 사용량을 본다

  2. limits.memory를 실사용의 1.5~2배로 올린다

  3. 그래도 계속 오르면 앱의 메모리 누수다 — 인프라 문제가 아니다

둘은 다른 사건이다.

컨테이너가 limits.memory 를 넘으면 그 컨테이너만 OOMKilled 로 재시작되고, 노드 전체 메모리가 부족하면 kubelet 이 QoS 순서대로 Pod 을 축출하는 두 경로의 구분
터미널 창
kubectl describe pod web | grep -A20 Events
메시지원인
MountVolume.SetUp failed ... not foundConfigMap/Secret이 없다
Unable to attach or mount volumesPV/CSI 문제 (스토리지 진단)
Multi-Attach errorRWO 볼륨을 두 노드에서
failed to create pod sandbox ... cniCNI 문제
network plugin is not readyCNI Pod이 안 떴다

Multi-Attach error는 RWO(ReadWriteOnce — 한 번에 한 노드에서만 마운트할 수 있는 접근 모드) 볼륨을 쓰는 Pod이 다른 노드로 옮겨 갈 때 잘 나온다. 옛 Pod이 완전히 사라져 볼륨이 detach될 때까지 새 Pod은 계속 ContainerCreating에 머문다 (스토리지 진단).

터미널 창
# 볼륨 관련
kubectl get cm,secret -n <ns>
kubectl get pvc,pv
# CNI 관련
kubectl get pods -n kube-system | grep -Ei 'calico|cilium|flannel'
ls /etc/cni/net.d/
sudo journalctl -u kubelet | grep -i cni | tail -20
터미널 창
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# web 0/1 Running 0 3m
kubectl describe pod web | grep -A10 Events
# Warning Unhealthy Readiness probe failed: HTTP probe failed with statuscode: 404

컨테이너는 살아 있는데 Service에서 빠진다. 그래서 “연결이 안 된다”로 보인다.

readinessProbe 실패가 Ready 조건 False → EndpointSlice 제외 → Service 엔드포인트 비어 있음 → 클라이언트 연결 실패로 이어지는 경로
  • readinessProbe가 실패하고 있다 — 컨테이너는 살아 있다
  • 결과: Service 엔드포인트에서 빠진다 → 트래픽이 안 온다
터미널 창
# 프로브 설정 확인
kubectl get pod web -o jsonpath='{.spec.containers[0].readinessProbe}'
# 안에서 직접 호출해본다
kubectl exec -it web -- wget -qO- http://localhost:8080/healthz
터미널 창
kubectl get pod web -o yaml | grep -A5 finalizers
kubectl get ns stuck -o yaml | grep -A5 finalizers

삭제는 즉시 지우는 게 아니라 “지워도 좋다”는 표식을 남기는 일이다. 오브젝트에 finalizer(삭제 전에 끝내야 할 정리 작업을 표시해 둔 문자열 목록 — CRD)가 하나라도 남아 있으면 API 서버는 실제 삭제를 미룬다. 그 표식을 지워 줄 컨트롤러가 죽어 있으면 아무도 지워 주지 않아 영원히 Terminating에 머문다.

원인조치
finalizer가 남아 있고 처리할 컨트롤러가 없다finalizer 제거
노드가 unreachable노드 복구 또는 강제 삭제
preStop 훅이 안 끝난다grace period 확인 (종료 유예 시간, 기본 30초)
터미널 창
# Pod 강제 삭제
kubectl delete pod web --force --grace-period=0
# finalizer 제거
kubectl patch pod web -p '{"metadata":{"finalizers":null}}' --type=merge
# 네임스페이스가 Terminating에서 안 끝날 때
kubectl get ns stuck -o json \
| jq '.spec.finalizers = []' \
| kubectl replace --raw "/api/v1/namespaces/stuck/finalize" -f -

마지막 /finalize 호출은 정리를 건너뛰고 표식만 떼어 내는 최후의 수단이다. 원인(아래 함정의 죽은 APIService 등)을 그대로 두면 다음 네임스페이스에서 또 걸린다. 시험에서 이 한 줄까지 필요한 경우는 흔치 않으니, 외우기보다 원인부터 본다는 순서를 기억해 두는 편이 낫다.

층을 좁혔으면 다음은 증거다. 상태(STATUS·Conditions)는 루프가 어디서 멈췄는지까지만 알려 주고, 왜 멈췄는지는 로그와 사용량 숫자에 있다. 이 절은 그 증거를 어디서 꺼내는지, 그리고 kubectl이 죽었을 때 같은 증거를 노드에서 어떻게 찾는지를 다룬다.

로그 다루기 — 커리큘럼의 “컨테이너 출력 스트림”

섹션 제목: “로그 다루기 — 커리큘럼의 “컨테이너 출력 스트림””
터미널 창
kubectl logs web
kubectl logs web -c sidecar # 멀티 컨테이너
kubectl logs web --previous # 이전 컨테이너
kubectl logs web -f --tail=100
kubectl logs web --since=15m --timestamps
kubectl logs -l app=web --all-containers --prefix --max-log-requests=10
kubectl logs deploy/web # 컨트롤러 지정
kubectl logs job/import

로그가 어디를 거쳐 오는지 알면 kubectl이 죽었을 때 어디를 봐야 하는지도 안다.

컨테이너 stdout 이 노드의 로그 파일을 거쳐 kubelet · API 서버 · kubectl logs 까지 오는 경로와, 앱이 파일에 쓰면 kubectl 로 안 보이는 이유
  • 컨테이너는 stdout / stderr로 로그를 낸다. 파일에 쓰면 kubectl로 안 보인다
  • 실체: 노드의 /var/log/pods/<ns>_<pod>_<uid>/<container>/0.log
  • /var/log/containers/ 는 거기로 향하는 심볼릭 링크다
터미널 창
# kubectl이 안 될 때 노드에서 직접
sudo ls /var/log/containers/
sudo tail -f /var/log/containers/kube-apiserver-*.log
sudo crictl logs <container-id>
터미널 창
kubectl top nodes
kubectl top pods -A --sort-by=memory
kubectl top pod web --containers
kubectl describe node node01 | grep -A10 'Allocated resources'
kubectl get pods -A -o wide --field-selector spec.nodeName=node01
터미널 창
# 노드에서 직접
df -h # 디스크
free -m # 메모리
top # CPU
sudo journalctl -u kubelet | grep -i evict

kubelet은 루프의 실행 담당이자 보고 담당이다. 노드가 NotReady라는 건 실행이 멈췄거나 보고가 끊겼다는 뜻이고, 보고가 끊기면 API 서버가 아는 상태 자체를 믿을 수 없게 된다. 그래서 이 층부터는 kubectl이 아니라 노드에 들어가 systemd 로그를 읽는 쪽이 정답에 가깝다.

터미널 창
kubectl get nodes
kubectl describe node node01 | grep -A15 Conditions
노드 NotReady 일 때 describe node 의 Conditions 다섯 가지별 원인과, kubelet 이 멈춘 경우 journalctl 로그 메시지별 조치
Condition원인
Ready: False, KubeletNotReady, cni plugin not initializedCNI 미설치/고장
Ready: Unknown, NodeStatusUnknownkubelet이 보고를 멈췄다
MemoryPressure: True메모리 부족 → 축출 시작
DiskPressure: True디스크 부족 → 이미지 GC, 축출
PIDPressure: True프로세스 수 초과
터미널 창
# 노드에 들어가서
ssh node01
sudo systemctl status kubelet
sudo journalctl -u kubelet -n 100 --no-pager # ★ 여기에 이유가 있다
sudo systemctl status containerd
df -h /var/lib/kubelet /var/lib/containerd
free -m
sudo swapon --show # 스왑이 켜졌나
터미널 창
sudo systemctl status kubelet
sudo journalctl -u kubelet -n 50 --no-pager
로그 메시지원인조치
failed to run Kubelet: running with swap on스왑swapoff -a
misconfiguration: kubelet cgroup driver ... != ...cgroup 드라이버 불일치containerd SystemdCgroup = true
x509: certificate has expired인증서 만료kubeadm certs renew
Unable to register node ... connection refusedAPI 서버가 죽었다컨트롤 플레인을 본다
open /var/lib/kubelet/config.yaml: no such file설정 파일 없음kubeadm 재초기화 필요
failed to load kubelet config fileYAML 문법 오류파일을 되돌린다
터미널 창
sudo systemctl restart kubelet
sudo systemctl enable kubelet # 부팅 시 자동 시작
sudo systemctl daemon-reload # 설정 파일을 바꿨다면

루프를 도는 주체들이 죽은 경우다. 선언은 받아도(혹은 그마저 못 받고) 아무도 그것을 읽고 행동하지 않으니, 증상은 “명령은 되는데 아무 일도 안 일어난다” 또는 “명령 자체가 안 된다”로 나온다. 여기서는 kubectl이 도구가 아니라 진단 대상이므로 crictl과 journalctl로 내려간다 (확장).

터미널 창
kubectl get nodes
# The connection to the server 192.168.1.10:6443 was refused

순서대로 확인한다.

  1. kubeconfig가 맞는가

    터미널 창
    kubectl config current-context
    ls -l ~/.kube/config
    kubectl cluster-info
  2. API 서버 컨테이너가 도는가 — ★ 컨트롤 플레인 노드에서

    터미널 창
    sudo crictl ps -a | grep kube-apiserver
  3. 죽었다면 왜 죽었는가

    터미널 창
    sudo crictl logs $(sudo crictl ps -a --name kube-apiserver -q | head -1)
  4. kubelet이 매니페스트를 읽고 있는가

    터미널 창
    sudo journalctl -u kubelet -n 50 --no-pager | grep -i apiserver
  5. 매니페스트 자체 확인

    터미널 창
    sudo cat /etc/kubernetes/manifests/kube-apiserver.yaml

아래가 죽으면 위가 전부 죽는다. 그래서 고치는 순서가 정해진다.

노드 디스크와 kubelet 위에 etcd · kube-apiserver 가 서고 그 위에 scheduler · controller-manager · kubectl 이 얹히는 컨트롤 플레인 의존 순서

etcd가 죽으면 API 서버도 못 뜬다. API 서버 로그에 etcd 관련 에러가 있으면 etcd부터 고쳐야 한다. 위에서부터 고치려 들면 시간만 버린다.

원인증상조치
매니페스트 YAML 문법 오류컨테이너가 아예 안 생긴다journalctl -u kubelet에 파싱 에러
잘못된 플래그컨테이너가 즉시 죽는다crictl logs에 unknown flag
etcd에 못 붙는다connection refused 반복etcd 컨테이너 확인
인증서 만료/경로 오류x509 에러kubeadm certs check-expiration
포트 충돌bind: address already in use6443을 쓰는 프로세스 확인
터미널 창
sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/backup.yaml # 먼저 백업
sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml
watch sudo crictl ps # 다시 뜨는지 지켜본다

스케줄러 · 컨트롤러 매니저가 죽었을 때

섹션 제목: “스케줄러 · 컨트롤러 매니저가 죽었을 때”

증상이 다르다. 이 비대칭이 판별의 실마리다.

Deployment 를 하나 만들어 ReplicaSet · Pod 생성 여부와 스케줄 여부로 kube-controller-manager 와 kube-scheduler 중 무엇이 죽었는지 가려내는 순서
죽은 것증상
kube-scheduler새 Pod이 영원히 Pending. Events가 비어 있다
kube-controller-managerDeployment를 만들어도 ReplicaSet/Pod이 안 생긴다. kubelet이 죽은 노드가 NotReady/Unknown으로 전환되지 않는다 (노드 status 자체는 kubelet이 갱신한다)
터미널 창
kubectl get pods -n kube-system | grep -E 'scheduler|controller'
kubectl logs -n kube-system kube-scheduler-controlplane
sudo crictl ps -a | grep -E 'scheduler|controller'
sudo cat /etc/kubernetes/manifests/kube-scheduler.yaml
터미널 창
# 스케줄러가 죽은 상태에서 Pod을 띄워야 한다면
kubectl run web --image=nginx --dry-run=client -o yaml > pod.yaml
# spec.nodeName: node01 을 추가한 뒤
kubectl apply -f pod.yaml

컨트롤 플레인 컴포넌트의 로그는 kubectl logs -n kube-system으로 볼 수 있다 — API 서버가 살아 있을 때만. 아니면 crictl logs.

터미널 창
sudo crictl ps -a | grep etcd
# etcd-controlplane은 Pod 이름이다. logs에는 -q가 낸 컨테이너 ID를 넘긴다.
sudo crictl logs $(sudo crictl ps -a --name etcd -q | head -1) 2>&1 | tail -30
sudo ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint health --cluster

x509: certificate signed by unknown authority는 로그를 낸 주체가 상대 인증서를 자신의 CA로 검증하지 못했다는 뜻이다. 메시지 한 줄만으로는 인증서를 제시한 쪽과 CA를 설정한 쪽 중 어느 파일이 바뀌었는지 확정할 수 없다. 로그 주체 → 접속 대상 → 인증서·CA 한 쌍 순으로 좁힌다.

로그 주체·문맥실패한 검증대조할 한 쌍
kube-apiserver 로그의 127.0.0.1:2379 연결API 서버가 etcd 서버 인증서를 검증etcd --cert-file ↔ API 서버 --etcd-cafile
etcd 로그의 client certificate 거부etcd가 API 서버의 클라이언트 인증서를 검증API 서버 --etcd-certfile ↔ etcd --trusted-ca-file
kubectl의 API 서버 인증서 거부kubectl이 API 서버 인증서를 검증API 서버 --tls-cert-file ↔ kubeconfig의 cluster CA

API 서버의 --client-ca-file은 kubectl·kubelet 등 API로 들어오는 클라이언트 인증서를 검증한다. API 서버가 etcd에 나가는 연결에서 쓰는 --etcd-cafile과 혼동하지 않는다.

증상원인
database space exceeded쿼터 초과 → compact + defrag 필요
no leader / context deadline exceeded과반이 안 산다 — 멤버를 확인
API 서버 로그의 etcdserver: request timed outetcd가 느리다 (디스크 I/O)
데이터 디렉터리 권한 오류복구 후 chown 누락 (etcd 백업과 복구)

database space exceeded는 etcd가 오브젝트의 옛 리비전을 계속 쌓다 쿼터에 닿은 상태다 — 오래된 리비전을 잘라내고(compact) 빈 공간을 되돌려받아야(defrag) 쓰기가 다시 열린다. 이쪽 운영 작업까지 깊게 팔 필요는 없다. etcd에서 시험의 본론은 etcd 백업과 복구의 백업·복구이고, 여기서는 “API 서버가 이상하면 etcd 로그부터 본다”는 습관이면 된다.

여기는 루프가 이미 다 돌아간 뒤의 문제다 — 선언대로 Pod이 떠 있고 상태도 정상인데 패킷만 안 간다. kubectl get으로는 다 정상으로 보이기 때문에, 상태를 읽는 대신 실제로 요청을 쏘아 보며 한 겹씩 벗기는 방식으로 바뀐다.

네트워킹 진단 — 안에서 밖으로

섹션 제목: “네트워킹 진단 — 안에서 밖으로”

한 겹씩 벗긴다. 처음 실패하는 겹이 곧 원인이다.

앱 포트 · Pod IP · Service ClusterIP · DNS 이름 · 외부 노출 순으로 안에서 밖으로 한 겹씩 확인하며 각 단계 실패 시 의심할 곳을 짚는 네트워크 진단 사다리
터미널 창
# ① 앱이 포트를 열었는가
kubectl exec -it web -- wget -qO- http://localhost:8080
# ② Pod IP로 직접
kubectl get pod web -o wide
kubectl run tmp --image=busybox:1.36 --rm -it --restart=Never -- wget -qO- http://10.244.1.5:8080
# ③ Service(ClusterIP)로 — 실소스는 EndpointSlice, get endpoints 는 요약 조회다 (v1.33+ 에선 deprecated 경고)
kubectl get endpoints web-svc # ★ 비어 있으면 여기가 문제
kubectl run tmp --image=busybox:1.36 --rm -it --restart=Never -- wget -qO- http://web-svc:80
# ④ DNS로
kubectl run tmp --image=busybox:1.36 --rm -it --restart=Never -- nslookup web-svc
# ⑤ 외부에서
curl http://<노드IP>:30080
kubectl describe ingress web
증상원인 후보확인
엔드포인트가 비어 있다라벨 불일치 / Pod 미Readydescribe svc, get pods --show-labels
ClusterIP만 안 된다kube-proxy 문제kubectl get ds kube-proxy -n kube-system
이름만 안 된다CoreDNSnslookup kubernetes.default
특정 Pod끼리만 안 된다NetworkPolicykubectl get netpol -A
노드 간 Pod 통신이 안 된다CNICNI Pod 상태, /etc/cni/net.d/
외부에서만 안 된다NodePort 범위 / 방화벽 / LBdescribe svc
Ingress에서 404 / 503규칙·백엔드 Servicedescribe ingress, 컨트롤러 로그
egress가 전부 안 된다NetworkPolicy의 DNS 미허용NetworkPolicy

kube-proxy가 죽으면 Pod-to-Pod 직접 통신은 정상이고 ClusterIP만 죽는다. 이 비대칭이 판별의 실마리다.

층 판별이 잘 안 먹는 두 부류다. 인증서와 노드 압박은 한 층에서 시작해 다른 층의 증상으로 나타난다 — 만료된 인증서 하나가 kubelet(노드 층)과 kubectl(컨트롤 플레인 층)을 동시에 무너뜨리고, 디스크가 차면 앱이 축출된다(애플리케이션 층). 층별 확인이 서로 어긋난다 싶으면 이 둘을 의심한다.

터미널 창
sudo kubeadm certs check-expiration
# 개별 인증서 확인
sudo openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -text | grep -A2 Validity
sudo openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -text | grep -A3 'Subject Alternative Name'
에러원인
x509: certificate has expired만료 → kubeadm certs renew all
x509: certificate signed by unknown authorityCA 불일치 — 로그 주체와 접속 대상을 먼저 확인한 뒤 인증서·CA 쌍을 대조
x509: cannot validate certificate for <IP>SAN에 그 IP가 없다
Unauthorized (401)토큰/인증서가 유효하지 않다
터미널 창
sudo kubeadm certs renew all
# 컨트롤 플레인 Pod 재시작 (매니페스트를 잠시 옮겼다 되돌린다)
sudo mv /etc/kubernetes/manifests /tmp/m && sleep 20 && sudo mv /tmp/m /etc/kubernetes/manifests
# admin.conf도 갱신되었으므로 다시 복사
sudo cp /etc/kubernetes/admin.conf ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
터미널 창
kubectl get pods -A --field-selector status.phase=Failed
kubectl get events -A --field-selector reason=Evicted
kubectl describe node node01 | grep -A5 Conditions
  • DiskPressure → kubelet이 이미지 GC를 하고, 그래도 부족하면 Pod을 축출한다
  • MemoryPressure → QoS 순서로 축출 (BestEffort → Burstable → Guaranteed)
  • 축출된 Pod은 Failed 상태로 남는다. 상위 컨트롤러가 새로 만든다
MemoryPressure 상황에서 BestEffort · Burstable · Guaranteed 순으로 축출되는 QoS 우선순위

왼쪽부터 축출된다. Guaranteed가 가장 오래 버틴다.

터미널 창
# 노드에서 공간 확보
sudo crictl rmi --prune # 안 쓰는 이미지 삭제
sudo journalctl --vacuum-size=200M # 저널 정리
df -h /var/lib/containerd /var/log
# 축출된 Pod 정리
kubectl delete pods --field-selector status.phase=Failed -A

종합 시나리오 — 자주 나오는 유형

섹션 제목: “종합 시나리오 — 자주 나오는 유형”
문제진단 경로조치
노드 하나가 NotReadyjournalctl -u kubelet스왑/cgroup/서비스 재시작
앱이 계속 재시작logs --previous + exit codelimit 상향 또는 command 수정
Service 연결 실패get endpoints라벨 수정 / probe 수정
이름 해석 실패nslookup kubernetes.defaultCoreDNS 확인
Pod이 계속 Pendingdescribe pod Eventstaint/affinity/자원/PVC
kubectl이 죽었다crictl ps -a매니페스트 복구
클러스터를 되돌려야 한다etcd 스냅샷restore + etcd.yaml 수정
특정 사용자만 403에러 메시지RoleBinding 추가
터미널 창
# 층별 검증
kubectl get nodes # 전부 Ready
kubectl get pods -A | grep -vE 'Running|Completed' # 비정상 Pod 없음
kubectl get events -A --sort-by=.lastTimestamp | tail -20
# 대상 리소스 직접 확인
kubectl rollout status deploy/web
kubectl get endpoints web-svc
kubectl wait --for=condition=ready pod -l app=web --timeout=60s
# 실제로 동작하는지
kubectl run tmp --image=busybox:1.36 --rm -it --restart=Never -- wget -qO- http://web-svc
  • 고치기 전에 층을 확정한다 — 질문 셋(get nodes 응답 · 전부 Ready · get pods -A 정상), 명령은 두 줄
  • 층은 곧 멈춘 루프의 위치다 — 애플리케이션(마지막 한 걸음) · 노드(실행·보고) · 컨트롤 플레인(루프의 주체) · 네트워크(루프는 끝났는데 패킷이 안 간다)
  • 순서는 describe(Events) → logs → -o yaml. 로그부터 보면 안 되는 문제가 많다
  • logs --previous 와 get events --sort-by 가 두 기둥
  • exit code로 좁힌다: 0=명령이 끝남, 127=명령 없음, 137=OOM/강제종료
  • Running 0/1 = readinessProbe 실패 → 엔드포인트에서 빠진다
  • kubectl이 죽으면 crictl + journalctl -u kubelet
  • 컨트롤 플레인 고장은 거의 항상 /etc/kubernetes/manifests/의 값 하나
  • kube-proxy가 죽으면 ClusterIP만 죽는다 (Pod 직접 통신은 정상)
  • etcd가 죽으면 API 서버도 못 뜬다 — 순서대로 고친다
  • 고친 뒤에는 반드시 최종 상태를 확인한다