22·23강 · 트러블슈팅¶
영상: 22강 앱이 안 뜰 때 (1~8) · 23강 클러스터가 아플 때 (9~12) · 이어서 풀 채점 세트:
22-troubleshooting
실습 목표¶
- 증상(STATUS)을 보고 어느 층(스케줄링 · 이미지 · 컨테이너 · 프로브 · 서비스 · DNS · 노드) 을 볼지 고르는 순서를 몸에 익힌다.
- Pending · ImagePullBackOff · CrashLoopBackOff · OOMKilled ·
0/1 Running을 직접 만들고, 각각 어느 명령의 어느 줄에 원인이 찍히는지 확인한다. - Service 의 Endpoints 가 비는 두 원인(셀렉터 불일치 · 준비 안 된 파드)과, 차 있는데도 안 되는 원인(targetPort)을 가른다.
- 워커의 kubelet 을 멈춰 NotReady 노드를 만들고
systemctl·journalctl로 복구한다. - kubectl 로 볼 수 없을 때를 대비해 컨트롤플레인 로그를
crictl·/var/log/pods에서 읽는다. - API 서버가 내려간 클러스터를
crictl→ 매니페스트 플래그 순서로 되살리고, kubelet 로그의6443 refused를 증상으로 읽는다. - 스태틱 파드의 CPU 요청을 노드 CPU 의 몇 % 로 맞추는 계산(코어 → 밀리코어)과, kubelet 의
OutOfcpu거부를 확인한다.
0) 준비¶
이 페이지는 일부러 고장을 냅니다. 9) 은 워커 노드의 kubelet 을, 10) 은 컨트롤플레인의 스케줄러를, 11) 은 API 서버를 잠시 멈추고, 12) 는 컨트롤플레인 노드에 실습용 스태틱 파드를 둡니다. 11) 을 하기 전에는 컨트롤플레인 VM 스냅샷을 찍어 두세요. 두 절에는 되돌리는 명령이 바로 붙어 있고, 정리 절에서 한 번 더 확인합니다.
노드 이름을 확인하세요
이 페이지는 워커를 worker-1 · worker-2, 컨트롤플레인을 control-plane 으로 적습니다. kubectl get nodes 로 내 이름을 확인해 바꿔 입력하세요.
1) 진단 순서 — 네 개의 명령과 판단 흐름¶
왜¶
트러블슈팅은 명령을 많이 아는 것보다 순서를 건너뛰지 않는 것이 중요합니다. 거의 모든 문제는 아래 네 명령 안에서 원인이 드러납니다.
| 순서 | 명령 | 보는 것 |
|---|---|---|
| ① | kubectl get pod -o wide |
STATUS · READY · RESTARTS · 어느 노드 |
| ② | kubectl describe pod |
맨 아래 Events, 컨테이너의 State / Last State |
| ③ | kubectl logs [--previous] |
앱이 남긴 말. 죽은 컨테이너는 --previous |
| ④ | kubectl events / get events |
네임스페이스 전체에서 시간순으로 무슨 일이 있었나 |
판단 흐름¶
flowchart TD
S["kubectl get pod -o wide"] --> P{"STATUS"}
P -- Pending --> P1["describe → Events: FailedScheduling<br/>자원 부족 · taint · nodeSelector/affinity · PVC 미바인딩"]
P -- ContainerCreating --> C1["describe → Events<br/>볼륨 마운트 실패 · Secret/ConfigMap 없음 · CNI 오류"]
P -- "ErrImagePull / ImagePullBackOff" --> I1["describe → Events: Failed to pull<br/>태그 오타 · 없는 저장소 · 인증(imagePullSecrets)"]
P -- "CrashLoopBackOff / Error" --> L1["logs --previous<br/>describe → Last State: Exit Code"]
L1 --> L2{"Exit Code"}
L2 -- "137 + OOMKilled" --> L3["메모리 limit 초과"]
L2 -- "1 등" --> L4["앱 에러 · 설정 누락 · 잘못된 command"]
L2 -- "128 StartError" --> L5["실행 파일 없음 · 잘못된 command"]
P -- "Running 인데 READY 0/1" --> R1["describe → Unhealthy<br/>readinessProbe 실패"]
P -- "Running 1/1 인데 접속 안 됨" --> V1{"get endpoints"}
V1 -- "비어 있음" --> V2["Service selector ↔ 파드 레이블<br/>또는 파드가 Ready 아님"]
V1 -- "IP 는 있음" --> V3["targetPort ↔ containerPort<br/>→ 그다음 DNS → NetworkPolicy"]
S -.-> N{"노드가 NotReady?"}
N -- 예 --> N1["ssh 노드 → systemctl status kubelet<br/>journalctl -u kubelet"]
S -.-> K{"kubectl 자체가 안 됨?"}
K -- 예 --> K1["컨트롤플레인 ssh → /etc/kubernetes/manifests<br/>crictl ps -a · crictl logs"]
이벤트 보는 두 가지 방법¶
kubectl get events -n guide-trouble --sort-by=.lastTimestamp # 오래된 것 → 최근 것
kubectl get events -n guide-trouble --field-selector type=Warning # 경고만
kubectl events -n guide-trouble --for pod/<이름> # 특정 오브젝트만 (시간순 정렬이 기본)
kubectl events -n guide-trouble --types=Warning --watch # 경고를 실시간으로
시험 팁 — 이벤트는 1시간이면 사라집니다
이벤트는 기본 1시간만 보관됩니다. 오래전에 난 문제는 이벤트가 없을 수 있으니, 그때는 describe 의 Last State 와 logs --previous 가 증거입니다.
자원 사용량 — kubectl top¶
kubectl top 은 metrics-server 가 있어야 동작합니다. kubeadm 클러스터에는 기본으로 없습니다.
metrics-server 설치 (선택)
kubeadm 의 kubelet 서빙 인증서는 클러스터 CA 가 아니라 kubelet 이 스스로 서명한 것이라, metrics-server 가 검증하지 못합니다. 실습용으로 --kubelet-insecure-tls 를 추가합니다.
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl patch deployment metrics-server -n kube-system --type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
kubectl rollout status deployment/metrics-server -n kube-system
2) Pending — 스케줄러가 놓을 곳을 못 찾는다¶
왜¶
Pending 은 아직 노드가 정해지지 않은 상태입니다. 컨테이너는 시작도 안 했으니 logs 는 비어 있고, 원인은 전부 스케줄러의 이벤트에 있습니다.
① 자원 부족¶
too-big.yaml:
apiVersion: v1
kind: Pod
metadata:
name: too-big
namespace: guide-trouble
spec:
containers:
- name: app
image: nginx:1.27
resources:
requests:
cpu: "64" # ← 64 코어. 어느 노드에도 없다
memory: 64Mi
kubectl apply -f too-big.yaml
kubectl get pod too-big -n guide-trouble -o wide
kubectl events -n guide-trouble --for pod/too-big
NAME READY STATUS RESTARTS AGE IP NODE
too-big 0/1 Pending 0 8s <none> <none> ← NODE 가 비어 있다
LAST SEEN TYPE REASON OBJECT MESSAGE
8s Warning FailedScheduling Pod/too-big 0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 Insufficient cpu. preemption: 0/3 nodes are available: 1 Preemption is not helpful for scheduling, 2 No preemption victims found for incoming pod.
관찰 포인트: 메시지가 노드 3대를 어떻게 탈락시켰는지 나눠서 말해 줍니다. 컨트롤플레인 1대는 taint 로, 워커 2대는 CPU 부족으로 탈락했습니다.
노드에 실제로 얼마가 남았는지는 describe node 의 Allocated resources 로 봅니다.
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 350m (17%) 0 (0%)
memory 140Mi (7%) 340Mi (18%)
스케줄러는 실제 사용량이 아니라 requests 의 합으로 판단합니다. kubectl top 이 한가해 보여도 requests 가 차 있으면 Pending 입니다.
② nodeSelector 가 맞는 노드가 없다¶
kubectl run picky -n guide-trouble --image=nginx:1.27 \
--overrides='{"spec":{"nodeSelector":{"disktype":"nvme"}}}'
kubectl events -n guide-trouble --for pod/picky | tail -1
... FailedScheduling Pod/picky 0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 node(s) didn't match Pod's node affinity/selector. preemption: ...
kubectl get pod picky -n guide-trouble -o jsonpath='{.spec.nodeSelector}{"\n"}'
kubectl get nodes -L disktype
kubectl label node worker-1 disktype=nvme
kubectl get pod picky -n guide-trouble -o wide
{"disktype":"nvme"}
NAME STATUS ROLES AGE VERSION DISKTYPE
control-plane Ready control-plane 5d v1.32.x
worker-1 Ready <none> 5d v1.32.x
worker-2 Ready <none> 5d v1.32.x
node/worker-1 labeled
NAME READY STATUS NODE
picky 1/1 Running worker-1
Pending 파드는 조건이 맞는 순간 스케줄러가 알아서 다시 시도합니다. 파드를 지우고 다시 만들 필요가 없습니다.
| FailedScheduling 메시지 조각 | 원인 | 대응 |
|---|---|---|
Insufficient cpu / memory |
requests 가 남은 자원보다 크다 | requests 줄이기 · 다른 파드 정리 |
had untolerated taint {…} |
노드의 taint | toleration 추가 또는 taint 제거 |
didn't match Pod's node affinity/selector |
nodeSelector · affinity | 노드 레이블 추가 또는 조건 수정 |
pod has unbound immediate PersistentVolumeClaims |
PVC 가 Pending | 13강 스토리지 — PV · StorageClass 확인 |
node(s) were unschedulable |
kubectl cordon 된 노드 |
kubectl uncordon |
| 이벤트가 아예 없음 | 스케줄러가 죽었다 | 10) 컨트롤플레인 확인 |
3) ImagePullBackOff — 이미지를 못 받는다¶
왜¶
노드는 정해졌는데 kubelet 이 컨테이너 런타임(containerd)에게 이미지를 받아 오라고 했다가 실패한 상태입니다.
ErrImagePull(방금 실패) → ImagePullBackOff(잠시 기다렸다 재시도) 를 번갈아 보입니다.
kubectl create deployment cart -n guide-trouble --image=redis:7.9.99
kubectl get pods -n guide-trouble -l app=cart -w
NAME READY STATUS RESTARTS AGE
cart-6f8b7d9c5-k2x7p 0/1 ContainerCreating 0 1s
cart-6f8b7d9c5-k2x7p 0/1 ErrImagePull 0 4s
cart-6f8b7d9c5-k2x7p 0/1 ImagePullBackOff 0 17s
Ctrl+C 로 빠져나와 이벤트를 봅니다.
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 30s default-scheduler Successfully assigned guide-trouble/cart-6f8b7d9c5-k2x7p to worker-2
Normal Pulling 29s kubelet Pulling image "redis:7.9.99"
Warning Failed 27s kubelet Failed to pull image "redis:7.9.99": rpc error: code = NotFound desc = failed to pull and unpack image "docker.io/library/redis:7.9.99": failed to resolve reference "docker.io/library/redis:7.9.99": docker.io/library/redis:7.9.99: not found
Warning Failed 27s kubelet Error: ErrImagePull
Normal BackOff 14s kubelet Back-off pulling image "redis:7.9.99"
Warning Failed 14s kubelet Error: ImagePullBackOff
관찰 포인트: Scheduled 는 성공했습니다. 스케줄링 문제가 아니라 노드(kubelet) 단계의 문제라는 뜻입니다.
redis:7.9.99 가 docker.io/library/redis:7.9.99 로 풀려서 해석된 것도 보입니다.
| 메시지 조각 | 원인 |
|---|---|
not found / manifest unknown |
태그 · 이미지 이름 오타 |
pull access denied / unauthorized / 401 |
비공개 저장소 — imagePullSecrets 필요 |
i/o timeout / dial tcp … no such host |
노드에서 저장소로 나가는 네트워크 · DNS |
toomanyrequests |
Docker Hub 요청 한도 |
고치기 — Deployment 템플릿을 바꾼다¶
kubectl get deploy cart -n guide-trouble -o jsonpath='{.spec.template.spec.containers[*].name}{"\n"}'
kubectl set image deployment/cart -n guide-trouble redis=redis:7.4
kubectl rollout status deployment/cart -n guide-trouble
파드의 이미지는 고칠 수 있습니다
파드 필드는 대부분 수정할 수 없지만 image 는 예외입니다. Deployment 없는 단독 파드라면
kubectl set image pod/<이름> <컨테이너>=<이미지> 로 바로 고칠 수 있습니다. 다른 필드를 바꿔야 하면 kubectl replace --force -f 로 지우고 다시 만듭니다.
노드에서 직접 받아 보면 kubelet 을 거치지 않고 확인할 수 있습니다.
... pulling image: failed to pull and unpack image "docker.io/library/redis:7.9.99": failed to resolve reference ... not found
4) CrashLoopBackOff — 떴다가 죽고, 다시 뜨고¶
왜¶
컨테이너가 시작은 됐는데 곧 종료되고, kubelet 이 재시작하는 것을 반복합니다. 재시작 간격은 10초 → 20초 → 40초 … 최대 5분까지 늘어납니다. 원인은 죽은 컨테이너가 남긴 로그와 종료 코드에 있습니다.
설정 파일이 없어 죽는 앱¶
crashy.yaml:
apiVersion: v1
kind: Pod
metadata:
name: crashy
namespace: guide-trouble
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c"]
args:
- |
echo "starting app, reading /etc/app/app.conf"
cat /etc/app/app.conf || { echo "FATAL: config not found"; exit 1; } # ← 파일이 없으면 exit 1
sleep 3600
kubectl logs crashy -n guide-trouble --previous
kubectl describe pod crashy -n guide-trouble | grep -A8 ' State:'
starting app, reading /etc/app/app.conf
cat: can't open '/etc/app/app.conf': No such file or directory
FATAL: config not found
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: Error
Exit Code: 1 ← 앱이 스스로 에러로 끝냈다
Started: Tue, 29 Sep 2026 07:41:03 +0000
Finished: Tue, 29 Sep 2026 07:41:03 +0000
Ready: False
Restart Count: 2
--previous 를 빼면
kubectl logs crashy 는 지금 막 재시작한 컨테이너의 로그를 봅니다. 그 컨테이너가 아직 아무것도 못 찍었거나 Waiting 상태면 비어 있거나 에러가 납니다.
죽기 직전의 말은 --previous(-p) 에 있습니다.
고치기 — 없는 설정을 넣어 준다¶
kubectl create configmap crashy-conf -n guide-trouble --from-literal=app.conf='mode=prod'
kubectl get pod crashy -n guide-trouble -o yaml > crashy-fixed.yaml
crashy-fixed.yaml 에 볼륨을 추가합니다. SA 토큰용 volumes · volumeMounts 목록이 이미 있으니 그 목록에 항목을 하나씩 더합니다.
(파드의 볼륨은 실행 중에 바꿀 수 없으므로 replace --force 로 지우고 다시 만듭니다.)
spec:
containers:
- name: app
...
volumeMounts:
- name: conf
mountPath: /etc/app # ← 앱이 찾는 경로
volumes:
- name: conf
configMap:
name: crashy-conf # ← 키 app.conf 가 파일 /etc/app/app.conf 가 된다
kubectl replace --force -f crashy-fixed.yaml
kubectl get pod crashy -n guide-trouble
kubectl logs crashy -n guide-trouble
pod "crashy" deleted
pod/crashy replaced
NAME READY STATUS RESTARTS AGE
crashy 1/1 Running 0 5s
starting app, reading /etc/app/app.conf
mode=prod
비교 — 실행 파일 자체가 없으면¶
kubectl run no-binary -n guide-trouble --image=busybox:1.36 --command -- start-server --port 80
sleep 20; kubectl get pod no-binary -n guide-trouble
kubectl logs no-binary -n guide-trouble --previous
kubectl describe pod no-binary -n guide-trouble | grep -A6 'Last State'
NAME READY STATUS RESTARTS AGE
no-binary 0/1 CrashLoopBackOff 2 (12s ago) 20s
(비어 있다 — 프로세스가 시작조차 못 했다)
Last State: Terminated
Reason: StartError
Message: failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "start-server": executable file not found in $PATH: unknown
Exit Code: 128
관찰 포인트: 앱 에러는 logs 에, 실행 자체의 실패는 describe 의 Message 에 남습니다. 로그가 비어 있으면 describe 로 갑니다.
| Exit Code | 뜻 | 먼저 볼 곳 |
|---|---|---|
| 0 | 정상 종료 — 그런데 restartPolicy: Always 라 다시 뜬다 (할 일 없이 끝나는 command) |
command/args |
| 1, 2 … | 앱이 에러로 종료 | logs --previous |
| 126 / 127 | 셸에서 실행 권한 없음 / 명령 없음 | logs --previous, command |
128 (StartError) |
컨테이너 프로세스를 시작하지 못함 | describe 의 Message |
| 137 | SIGKILL — OOMKilled 이거나 강제 종료 | describe 의 Reason |
| 143 | SIGTERM — 정상적인 종료 요청 | 보통 문제 아님 (liveness 실패 재시작 등) |
5) OOMKilled — 메모리 limit 을 넘었다¶
왜¶
컨테이너가 resources.limits.memory 를 넘으면 커널이 프로세스를 즉시 죽입니다(SIGKILL, 137). 앱은 로그를 남길 틈도 없습니다.
memhog.yaml:
apiVersion: v1
kind: Pod
metadata:
name: memhog
namespace: guide-trouble
spec:
containers:
- name: stress
image: polinux/stress
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "150M", "--vm-hang", "1"] # ← 150MB 를 잡으려 한다
resources:
requests:
memory: 32Mi
limits:
memory: 64Mi # ← 한도는 64Mi
kubectl apply -f memhog.yaml
sleep 15; kubectl get pod memhog -n guide-trouble
kubectl describe pod memhog -n guide-trouble | grep -A5 'Last State'
NAME READY STATUS RESTARTS AGE
memhog 0/1 OOMKilled 1 (8s ago) 15s
Last State: Terminated
Reason: OOMKilled ← 결정적 증거
Exit Code: 137
Started: ...
Finished: ...
잠시 뒤에는 STATUS 가 CrashLoopBackOff 로 바뀝니다. STATUS 만 보면 CrashLoop 과 구분이 안 되므로 Last State 의 Reason 을 꼭 봅니다.
kubectl get pod memhog -n guide-trouble \
-o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
고치는 방법은 둘입니다 — limit 을 올리거나, 앱이 덜 쓰게 합니다. 여기서는 사용량을 줄여 봅니다.
sed -i 's/"150M"/"40M"/' memhog.yaml
kubectl replace --force -f memhog.yaml
sleep 10; kubectl get pod memhog -n guide-trouble
노드 전체 메모리가 모자라면
컨테이너 limit 이 아니라 노드 메모리가 부족하면 kubelet 이 파드를 축출(Evicted) 합니다. STATUS 가 Evicted 이고
describe node 의 Conditions 에 MemoryPressure True 가 보입니다. OOMKilled 와는 다른 경로입니다.
6) Running 인데 READY 0/1 — readinessProbe 실패¶
왜¶
컨테이너는 살아 있지만 "요청 받을 준비가 됐다" 는 검사를 통과하지 못한 상태입니다. 준비 안 된 파드는 Service 의 Endpoints 에서 빠집니다. 파드는 멀쩡해 보이는데 서비스가 안 되는 흔한 원인입니다.
menu.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: menu
namespace: guide-trouble
spec:
replicas: 2
selector:
matchLabels: { app: menu }
template:
metadata:
labels: { app: menu }
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /healthz # ← nginx 기본 설정에는 이 경로가 없다 → 404
port: 80
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: menu
namespace: guide-trouble
spec:
selector: { app: menu }
ports:
- port: 80
targetPort: 80
kubectl apply -f menu.yaml
sleep 20; kubectl get pods -n guide-trouble -l app=menu
kubectl get endpoints menu -n guide-trouble
NAME READY STATUS RESTARTS AGE
menu-7d5c9b6f8-4hq9z 0/1 Running 0 20s ← Running 이지만 0/1
menu-7d5c9b6f8-m2n8c 0/1 Running 0 20s
NAME ENDPOINTS AGE
menu <none> 20s
Readiness: http-get http://:80/healthz delay=0s timeout=1s period=5s #success=1 #failure=3
Warning Unhealthy 3s (x6 over 28s) kubelet Readiness probe failed: HTTP probe failed with statuscode: 404
EndpointSlice 를 보면 파드 IP 는 알고 있지만 ready 가 false 라서 빠진 것이 보입니다.
kubectl get endpointslice -n guide-trouble -l kubernetes.io/service-name=menu -o yaml | grep -E 'addresses|- 10\.|ready:'
고치기¶
kubectl patch deployment menu -n guide-trouble --type=json \
-p='[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/path","value":"/"}]'
kubectl rollout status deployment/menu -n guide-trouble
kubectl get endpoints menu -n guide-trouble
| readinessProbe 실패 | livenessProbe 실패 | |
|---|---|---|
| 결과 | Endpoints 에서 빠진다 (READY 0/1) | 컨테이너를 재시작한다 (RESTARTS 증가) |
| STATUS | Running | Running ↔ CrashLoopBackOff |
| 이벤트 | Readiness probe failed |
Liveness probe failed · Container … failed liveness probe, will be restarted |
7) Service 가 파드를 못 찾는다 — 셀렉터와 targetPort¶
왜¶
Service 문제는 순서대로 좁힙니다. ① Endpoints 가 비었나 → ② 차 있는데 안 되나(포트) → ③ 이름만 안 되나(DNS, 8절) → ④ 다 맞는데 안 되나(NetworkPolicy).
billing.yaml — 두 군데를 일부러 틀렸습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: billing
namespace: guide-trouble
spec:
replicas: 2
selector:
matchLabels: { app: billing }
template:
metadata:
labels: { app: billing, tier: backend }
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: billing-svc
namespace: guide-trouble
spec:
selector:
app: billing-api # ← 틀림 ①: 파드 레이블은 app=billing
ports:
- port: 80
targetPort: 8080 # ← 틀림 ②: nginx 는 80 에서 듣는다
① Endpoints 가 비었다 → 셀렉터¶
kubectl get svc billing-svc -n guide-trouble -o jsonpath='{.spec.selector}{"\n"}'
kubectl get pods -n guide-trouble -l app=billing --show-labels
kubectl get pods -n guide-trouble -l app=billing-api # 서비스 셀렉터로 직접 조회
{"app":"billing-api"}
NAME READY STATUS LABELS
billing-5b8c7d6f9-7wq4k 1/1 Running app=billing,pod-template-hash=5b8c7d6f9,tier=backend
billing-5b8c7d6f9-x2k8p 1/1 Running app=billing,pod-template-hash=5b8c7d6f9,tier=backend
No resources found in guide-trouble namespace.
관찰 포인트: 서비스 셀렉터를 그대로 -l 에 넣어 조회하면 한 번에 판정됩니다. 결과가 없으면 셀렉터 문제입니다.
6절처럼 파드가 Ready 가 아닐 때도 Endpoints 가 비므로, READY 칸도 함께 봅니다.
kubectl patch svc billing-svc -n guide-trouble -p '{"spec":{"selector":{"app":"billing"}}}'
kubectl get endpoints billing-svc -n guide-trouble
patch 로 셀렉터를 바꿀 때
-p '{"spec":{"selector":{...}}}' 는 기존 키에 병합됩니다. 틀린 키 이름이 따로 있었다면(예: application: billing) 그 키는 남습니다.
kubectl edit svc 로 셀렉터 전체를 눈으로 확인하는 편이 안전합니다.
② Endpoints 는 있는데 접속이 안 된다 → targetPort¶
kubectl run probe -n guide-trouble --rm -i --restart=Never --image=busybox:1.36 -- \
wget -qO- -T 3 http://billing-svc
wget: can't connect to remote host (10.103.72.15): Connection refused
pod "probe" deleted
pod guide-trouble/probe terminated (Error)
kubectl get pods -n guide-trouble -l app=billing -o jsonpath='{.items[0].spec.containers[0].ports[*].containerPort}{"\n"}'
kubectl patch svc billing-svc -n guide-trouble --type=json \
-p='[{"op":"replace","path":"/spec/ports/0/targetPort","value":80}]'
kubectl run probe -n guide-trouble --rm -i --restart=Never --image=busybox:1.36 -- \
wget -qO- -T 3 http://billing-svc | head -4
| 증상 | 원인 | 확인 명령 |
|---|---|---|
ENDPOINTS <none>, 파드 Ready |
selector ↔ 파드 레이블 | get pods -l <서비스 셀렉터> |
ENDPOINTS <none>, 파드 0/1 |
readinessProbe | describe pod → Unhealthy |
ENDPOINTS 에 IP 있음, Connection refused |
targetPort ↔ containerPort | get svc -o yaml, 파드 ports |
| ENDPOINTS 에 IP 있음, 타임아웃 | NetworkPolicy · CNI | get networkpolicy -n <ns> |
| IP 로는 되는데 이름으로 안 됨 | DNS | 8) |
8) DNS — 이름으로만 안 될 때¶
왜¶
파드는 /etc/resolv.conf 에 적힌 CoreDNS(kube-dns Service) 에게 이름을 묻습니다. 이름으로만 안 되면 CoreDNS 와 resolv.conf 를 봅니다.
kubectl run dns -n guide-trouble --rm -i --restart=Never --image=busybox:1.28 -- \
nslookup billing-svc.guide-trouble.svc.cluster.local
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: billing-svc.guide-trouble.svc.cluster.local
Address 1: 10.103.72.15 billing-svc.guide-trouble.svc.cluster.local
busybox 1.28 을 쓰는 이유
최신 busybox 의 nslookup 은 search 도메인 처리가 달라 헷갈리는 결과를 낼 때가 있습니다. DNS 확인용으로는 1.28 이 흔히 쓰입니다.
파드 안의 resolv.conf 는 이렇게 생겼습니다.
kubectl run dns -n guide-trouble --rm -i --restart=Never --image=busybox:1.28 -- cat /etc/resolv.conf
search guide-trouble.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10 ← kube-dns Service 의 ClusterIP
options ndots:5
search 줄 덕분에 같은 네임스페이스에서는 billing-svc 만, 다른 네임스페이스에서는 billing-svc.guide-trouble 로 부를 수 있습니다.
깨 보기 — CoreDNS 를 잠시 0 개로¶
클러스터 전체의 이름 해석이 멈춥니다
몇십 초 동안 모든 파드의 DNS 가 안 됩니다. 실습 클러스터에서만 하고, 바로 되돌립니다.
kubectl get deploy coredns -n kube-system -o jsonpath='{.spec.replicas}{"\n"}' # 원래 개수 기억 (보통 2)
kubectl scale deploy coredns -n kube-system --replicas=0
kubectl run dns -n guide-trouble --rm -i --restart=Never --image=busybox:1.28 -- \
nslookup billing-svc
2
deployment.apps/coredns scaled
Server: 10.96.0.10
Address 1: 10.96.0.10
nslookup: can't resolve 'billing-svc'
pod guide-trouble/dns terminated (Error)
kubectl get endpoints kube-dns -n kube-system
kubectl scale deploy coredns -n kube-system --replicas=2
kubectl rollout status deploy coredns -n kube-system
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=5
NAME ENDPOINTS AGE
kube-dns <none> 5d ← 7절과 같은 모양: 서비스는 있는데 뒤에 파드가 없다
deployment.apps/coredns scaled
deployment "coredns" successfully rolled out
.:53
[INFO] plugin/reload: Running configuration SHA512 = ...
CoreDNS-1.11.3
linux/amd64, go1.21.11, a6338e9
관찰 포인트: DNS 장애도 결국 "kube-dns Service 의 Endpoints 가 비었다" 로 보입니다. 7절의 점검 순서가 그대로 통합니다.
9) 노드 NotReady — kubelet 을 멈춰 보기¶
왜¶
노드의 상태는 그 노드의 kubelet 이 API 서버에 주기적으로 보고해서 정해집니다. 보고가 끊기면 컨트롤플레인은 약 40초 뒤 노드를 NotReady(정확히는 Unknown)로 표시합니다.
kubelet 은 파드가 아니라 systemd 서비스라서, 고치려면 그 노드에 들어가야 합니다.
워커 하나에서만, 끝나면 반드시 되돌립니다
worker-2 의 kubelet 을 멈춥니다. 5분 넘게 두면 그 노드의 파드가 다른 노드로 축출되기 시작합니다. 관찰이 끝나면 곧바로 systemctl start kubelet 합니다.
멈추기¶
● kubelet.service - kubelet: The Kubernetes Node Agent
Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; preset: enabled)
Drop-In: /usr/lib/systemd/system/kubelet.service.d
└─10-kubeadm.conf
Active: inactive (dead) since Tue 2026-09-29 08:02:11 UTC; 5s ago
40초쯤 지나면 터미널 1 에:
관찰¶
Taints: node.kubernetes.io/unreachable:NoExecute
node.kubernetes.io/unreachable:NoSchedule
Unschedulable: false
Conditions:
Type Status LastHeartbeatTime Reason Message
---- ------ ----------------- ------ -------
MemoryPressure Unknown ... NodeStatusUnknown Kubelet stopped posting node status.
DiskPressure Unknown ... NodeStatusUnknown Kubelet stopped posting node status.
PIDPressure Unknown ... NodeStatusUnknown Kubelet stopped posting node status.
Ready Unknown ... NodeStatusUnknown Kubelet stopped posting node status.
Addresses:
관찰 포인트:
- Status 가
False가 아니라Unknown입니다. "나빠졌다" 가 아니라 "소식이 끊겼다" 입니다. - 노드 컨트롤러가
unreachabletaint 를 자동으로 붙였습니다.NoSchedule때문에 새 파드는 오지 않고,NoExecute때문에 기존 파드는 기본 toleration(300초)이 지나면 축출됩니다. - 그 노드의 파드는 API 에 여전히
Running으로 보일 수 있습니다. 보고할 kubelet 이 없어서 갱신이 안 된 것입니다.
원인 찾고 복구하기¶
worker-2 에서:
Sep 29 08:02:11 worker-2 systemd[1]: Stopping kubelet.service - kubelet: The Kubernetes Node Agent...
Sep 29 08:02:11 worker-2 systemd[1]: kubelet.service: Deactivated successfully.
Sep 29 08:02:11 worker-2 systemd[1]: Stopped kubelet.service - kubelet: The Kubernetes Node Agent.
누가 멈췄다는 기록만 있습니다. 이번 원인은 "멈춰 있다" 이므로 시작하고, 부팅 때도 뜨도록 enable 을 확인합니다.
sudo systemctl start kubelet
sudo systemctl enable kubelet
systemctl is-active kubelet; systemctl is-enabled kubelet
exit
taint 도 자동으로 사라졌습니다.
| journalctl 에 보이는 것 | 흔한 원인 | 대응 |
|---|---|---|
Stopped kubelet.service 뿐 |
누가 멈췄다 · disable 됐다 | systemctl enable --now kubelet |
failed to load kubelet config file |
/var/lib/kubelet/config.yaml 경로 · 문법 |
파일 확인 후 restart |
unable to load client CA file / 인증서 에러 |
인증서 경로 · 만료 | /etc/kubernetes/kubelet.conf, /var/lib/kubelet/pki |
container runtime is down / containerd 소켓 에러 |
containerd 가 죽었다 | systemctl status containerd |
running with swap on is not supported |
swap 이 켜졌다 | swapoff -a + fstab |
시험 팁 — 설정을 고쳤으면 daemon-reload
/usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf 같은 systemd 유닛 파일을 고쳤다면 sudo systemctl daemon-reload 후 restart 해야 반영됩니다.
/var/lib/kubelet/config.yaml 만 고쳤다면 restart 로 충분합니다.
10) 컨트롤플레인 로그 — kubectl 이 안 믿길 때¶
왜¶
컨트롤플레인 구성 요소는 스태틱 파드입니다(18강). API 서버가 살아 있으면 kubectl logs 로 보면 되지만,
API 서버가 죽으면 kubectl 이 아예 동작하지 않습니다. 그때는 노드의 컨테이너 런타임에게 직접 묻습니다.
API 서버가 살아 있을 때¶
kubectl get pods -n kube-system -l tier=control-plane
kubectl logs -n kube-system kube-scheduler-control-plane --tail=3
API 서버 없이 보는 법 — crictl 과 /var/log/pods¶
컨트롤플레인 노드에서:
sudo crictl ps --name kube-scheduler
sudo crictl logs --tail 3 $(sudo crictl ps -q --name kube-scheduler)
ls /var/log/pods/ | grep kube-system_kube-scheduler
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD
3f9e2c1d8a7b4 a389e107f4ff1 5 days ago Running kube-scheduler 0 9b2d4c6e1f3a5 kube-scheduler-control-plane
I0929 08:10:02.112345 1 leaderelection.go:...] successfully acquired lease kube-system/kube-scheduler
...
kube-system_kube-scheduler-control-plane_6c1f0d2e8b...
crictl 이 경고를 낸다면
WARN ... runtime connect using default endpoints 경고가 나오면 소켓을 알려 줍니다.
깨 보기 — 스케줄러에 없는 옵션을 넣는다¶
API 서버는 그대로 두고 스케줄러만 고장 냅니다. 먼저 매니페스트를 manifests 디렉터리 밖에 백업합니다.
백업을 /etc/kubernetes/manifests/ 안에 두지 마세요
kubelet 은 그 디렉터리의 모든 파일을 스태틱 파드로 띄우려 합니다. 백업 파일이 거기 있으면 같은 이름의 파드가 충돌합니다.
sudo cp /etc/kubernetes/manifests/kube-scheduler.yaml ~/guide-trouble/kube-scheduler.yaml.bak
sudo sed -i 's/ - --bind-address=127.0.0.1/ - --bind-address=127.0.0.1\n - --no-such-flag=true/' \
/etc/kubernetes/manifests/kube-scheduler.yaml
sudo grep -n 'no-such-flag' /etc/kubernetes/manifests/kube-scheduler.yaml
kubelet 이 파일 변경을 감지해 스케줄러를 다시 띄웁니다. 30초쯤 뒤:
kubectl get pods -n kube-system -l component=kube-scheduler
kubectl run after-break -n guide-trouble --image=nginx:1.27
kubectl get pod after-break -n guide-trouble
NAME READY STATUS RESTARTS AGE
kube-scheduler-control-plane 0/1 CrashLoopBackOff 2 (20s ago) 5d
NAME READY STATUS RESTARTS AGE
after-break 0/1 Pending 0 5s ← 이벤트조차 없다 (2절 표의 마지막 줄)
이제 kubectl 없이 원인을 찾습니다.
sudo crictl ps -a --name kube-scheduler | head -3
sudo crictl logs $(sudo crictl ps -a -q --name kube-scheduler | head -1) 2>&1 | head -3
sudo tail -3 /var/log/pods/kube-system_kube-scheduler-control-plane_*/kube-scheduler/*.log
CONTAINER IMAGE CREATED STATE NAME ATTEMPT ...
8c1e4f2a9d6b3 a389e107f4ff1 12 seconds ago Exited kube-scheduler 3 ...
Error: unknown flag: --no-such-flag
...
2026-09-29T08:15:40.123Z stderr F Error: unknown flag: --no-such-flag
관찰 포인트: crictl ps 에는 -a 를 붙여야 죽은(Exited) 컨테이너까지 보입니다. 로그 파일은 /var/log/pods/<ns>_<파드>_<uid>/<컨테이너>/<재시작번호>.log 입니다.
되돌리기¶
sudo cp ~/guide-trouble/kube-scheduler.yaml.bak /etc/kubernetes/manifests/kube-scheduler.yaml
sleep 30
kubectl get pods -n kube-system -l component=kube-scheduler
kubectl get pod after-break -n guide-trouble
NAME READY STATUS RESTARTS AGE
kube-scheduler-control-plane 1/1 Running 0 20s
NAME READY STATUS RESTARTS AGE
after-break 1/1 Running 0 1m ← 스케줄러가 돌아오자 바로 배치됐다
| 구성 요소가 죽으면 | 눈에 보이는 증상 |
|---|---|
| kube-apiserver | kubectl 전부 The connection to the server … was refused |
| etcd | API 서버도 곧 실패 (etcd 에 못 붙어서) |
| kube-scheduler | 새 파드가 이벤트 없이 Pending |
| kube-controller-manager | Deployment 를 만들어도 ReplicaSet · 파드가 안 생긴다, 스케일이 안 먹는다 |
| kubelet (노드) | 그 노드 NotReady (9절) |
시험 팁 — API 서버가 죽었을 때의 순서
① 컨트롤플레인에 ssh → ② /etc/kubernetes/manifests/kube-apiserver.yaml 을 직접 읽어 오타 확인 →
③ crictl ps -a | grep apiserver · crictl logs <ID> → ④ 컨테이너조차 없으면 journalctl -u kubelet (YAML 문법 오류는 kubelet 로그에 남습니다).
11) 컨트롤플레인 복구 — API 서버가 내려갔을 때¶
왜¶
10) 에서는 API 서버가 살아 있어서 kubectl 로 증상을 볼 수 있었습니다. 시험에서는 "kubectl 이 안 된다" 에서 출발하는 문제가 나옵니다.
원인은 대개 kube-apiserver.yaml 의 플래그 한 줄입니다. kubectl 도 kubectl logs 도 못 쓰니, 컨트롤플레인 노드에서 런타임(crictl)과 kubelet 에게 직접 묻습니다.
| 순서 | 명령 | 알 수 있는 것 |
|---|---|---|
| ① | kubectl get nodes |
refused → API 서버 프로세스 자체가 없다 |
| ② | sudo crictl ps -a --name kube-apiserver |
컨테이너가 Exited 를 반복하나, 아예 없나 |
| ③ | sudo crictl logs <ID> |
왜 죽는지 — 마지막 몇 줄에 파일·주소가 찍힌다 |
| ④ | sudo grep ... kube-apiserver.yaml |
로그가 가리킨 플래그를 매니페스트에서 찾는다 |
| ⑤ (②에서 컨테이너가 없을 때) | sudo journalctl -u kubelet |
YAML 문법 오류처럼 kubelet 이 파드를 못 만드는 이유 |
VM 스냅샷을 먼저 찍으세요
이 절은 API 서버를 멈춥니다. 되돌리는 명령이 바로 붙어 있지만, 매니페스트를 잘못 고치면 클러스터 전체가 멈춘 채로 남습니다. 시작 전에 컨트롤플레인 VM 의 스냅샷을 찍어 두세요. 백업은 10) 과 같이 manifests 디렉터리 밖에 둡니다.
깨 보기 — etcd CA 파일 경로를 틀리게¶
컨트롤플레인 노드에서:
sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml ~/guide-trouble/kube-apiserver.yaml.bak
sudo sed -i 's#--etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt#--etcd-cafile=/etc/kubernetes/pki/etcd/ca-typo.crt#' \
/etc/kubernetes/manifests/kube-apiserver.yaml
sudo grep -n 'etcd-cafile' /etc/kubernetes/manifests/kube-apiserver.yaml
kubelet 이 파일 변경을 알아채고 API 서버를 다시 띄우려 합니다. 30초~1분 뒤:
The connection to the server 192.168.56.10:6443 was refused - did you specify the right host or port?
처음 20초쯤은 옛 컨테이너가 아직 응답할 수 있습니다. refused 가 나올 때까지 기다리세요.
증상 — kubelet 로그의 6443 refused 는 범인이 아니다¶
Sep 29 08:31:02 control-plane kubelet[812]: E0929 08:31:02.118 ... failed to list *v1.Node: Get "https://192.168.56.10:6443/api/v1/nodes?...": dial tcp 192.168.56.10:6443: connect: connection refused
Sep 29 08:31:04 control-plane kubelet[812]: E0929 08:31:04.522 ... "Failed to update lease" err="Put \"https://192.168.56.10:6443/apis/coordination.k8s.io/v1/namespaces/kube-node-lease/leases/control-plane?timeout=10s\": dial tcp 192.168.56.10:6443: connect: connection refused"
관찰 포인트:
- kubelet 도 API 서버의 클라이언트입니다.
6443 ... connection refused는 "API 서버가 없다" 는 사실을 kubelet 이 한 번 더 말해 주는 증상입니다. - 이 줄을 보고 kubelet 설정을 뒤지거나
systemctl restart kubelet을 반복하면 시간만 갑니다. 원인은 API 서버 컨테이너의 로그에 있습니다.
원인 — crictl 로 API 서버 컨테이너를 본다¶
sudo crictl ps -a --name kube-apiserver | head -3
sudo crictl logs $(sudo crictl ps -a -q --name kube-apiserver | head -1) 2>&1 | tail -2
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD
5d1c7e9a2b4f8 6e6d5fe7d4ac9 15 seconds ago Exited kube-apiserver 3 7a3c1e0b9d2f4 kube-apiserver-control-plane
I0929 08:31:19.981204 1 options.go:...] ...
E0929 08:31:20.004512 1 run.go:72] "command failed" err="open /etc/kubernetes/pki/etcd/ca-typo.crt: no such file or directory"
마지막 줄의 형식은 버전마다 조금 다르지만, 어느 파일 · 어느 주소에서 실패했는지가 찍힙니다. 그 값을 매니페스트에서 찾고, 짝이 되는 etcd 쪽 설정과 맞춰 봅니다.
sudo grep -nE 'etcd-(servers|cafile|certfile|keyfile)' /etc/kubernetes/manifests/kube-apiserver.yaml
sudo grep -nE 'listen-client-urls|listen-peer-urls|trusted-ca-file' /etc/kubernetes/manifests/etcd.yaml
ls /etc/kubernetes/pki/etcd/
22: - --etcd-cafile=/etc/kubernetes/pki/etcd/ca-typo.crt ← 이 파일이 없다
23: - --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
24: - --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
25: - --etcd-servers=https://127.0.0.1:2379
20: - --listen-client-urls=https://127.0.0.1:2379,https://192.168.56.10:2379
22: - --listen-peer-urls=https://192.168.56.10:2380
29: - --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
ca.crt ca.key healthcheck-client.crt healthcheck-client.key peer.crt peer.key server.crt server.key
2379 와 2380 — 포트 하나 차이¶
같은 패턴의 고장으로 --etcd-servers 의 포트가 틀리는 경우가 자주 나옵니다. etcd 는 포트를 두 개 엽니다.
| 포트 | etcd 의 플래그 | 누가 붙나 |
|---|---|---|
| 2379 | --listen-client-urls |
클라이언트 — kube-apiserver, etcdctl |
| 2380 | --listen-peer-urls |
etcd 멤버끼리 (복제 · 리더 선출) |
API 서버는 etcd 의 클라이언트이므로 --etcd-servers 는 반드시 2379 입니다. 2380 으로 바뀌어 있으면 API 서버 로그에
127.0.0.1:2380 에 붙지 못한다는 줄(연결 거부나 context deadline exceeded)이 반복되다가 컨테이너가 죽습니다.
위 출력처럼 kubeadm 의 peer 포트는 노드 IP 에만 열려 있어서 127.0.0.1:2380 에는 듣는 쪽조차 없습니다.
| 로그 · 상태에 보이는 것 | 의심할 플래그 |
|---|---|
127.0.0.1:2380 · :2381 등 다른 포트로 etcd 연결 실패 |
--etcd-servers 포트 → etcd.yaml 의 --listen-client-urls 와 맞춘다 |
open ... .crt/.key: no such file or directory |
--etcd-cafile · --etcd-certfile · --tls-*-file · --client-ca-file 경로 → ls /etc/kubernetes/pki |
tls: ... certificate signed by unknown authority |
--etcd-cafile 이 엉뚱한 CA(/etc/kubernetes/pki/ca.crt)를 가리킨다 |
unknown flag |
플래그 이름 오타 (10절) |
crictl ps -a 에 컨테이너가 아예 없다 |
매니페스트 YAML 문법 → journalctl -u kubelet |
etcd 컨테이너가 Exited |
API 서버가 아니라 etcd 가 먼저 → crictl logs 를 etcd 쪽에서 |
고치기¶
sudo cp ~/guide-trouble/kube-apiserver.yaml.bak /etc/kubernetes/manifests/kube-apiserver.yaml
# 시험에서는 백업이 없으니 vi 로 그 줄만 고칩니다 (ca-typo.crt → ca.crt)
watch -n2 'sudo crictl ps --name kube-apiserver' # STATE 가 Running 으로 바뀌면 Ctrl+C
kubectl get nodes
kubectl get pods -n kube-system -l component=kube-apiserver
NAME STATUS ROLES AGE VERSION
control-plane Ready control-plane 5d v1.32.x
worker-1 Ready <none> 5d v1.32.x
worker-2 Ready <none> 5d v1.32.x
NAME READY STATUS RESTARTS AGE
kube-apiserver-control-plane 1/1 Running 0 40s
관찰 포인트: API 서버가 돌아오면 kubelet 로그의 6443 refused 도 저절로 멈춥니다. kubelet 은 처음부터 고장이 아니었습니다.
시험 팁 — 고쳤는데 1~2분이 지나도 그대로라면
kubelet 은 manifests 디렉터리를 주기적으로(약 20초) 다시 읽습니다. 그래도 컨테이너가 안 바뀌면 sudo systemctl restart kubelet 을 한 번 해 봅니다.
그래도 컨테이너가 없으면 sudo journalctl -u kubelet -n 50 --no-pager | grep -i apiserver 로 YAML 오류를 찾습니다. 탭 문자 · 들여쓰기가 흔한 원인입니다.
12) static pod 의 자원 요청 고치기 — 노드 CPU 의 10%¶
왜¶
2) 에서 64 코어를 요청한 일반 파드는 Pending 이었고, 스케줄러가 FailedScheduling 이벤트를 남겼습니다.
스태틱 파드는 스케줄러를 거치지 않고 그 노드의 kubelet 이 바로 띄웁니다. 그래서 자원이 모자라면 Pending 이 아니라
kubelet 이 입장(admission)을 거부하고, 이유는 OutOfcpu 입니다. 컨트롤플레인 구성 요소가 이렇게 되면 그 구성 요소가 통째로 사라집니다.
kubeadm 이 만든 컨트롤플레인 스태틱 파드에도 requests 가 있습니다.
/etc/kubernetes/manifests/etcd.yaml: requests:
/etc/kubernetes/manifests/etcd.yaml- cpu: 100m
--
/etc/kubernetes/manifests/kube-apiserver.yaml: requests:
/etc/kubernetes/manifests/kube-apiserver.yaml- cpu: 250m
--
/etc/kubernetes/manifests/kube-controller-manager.yaml: requests:
/etc/kubernetes/manifests/kube-controller-manager.yaml- cpu: 200m
--
/etc/kubernetes/manifests/kube-scheduler.yaml: requests:
/etc/kubernetes/manifests/kube-scheduler.yaml- cpu: 100m
노드 CPU 읽기 → 밀리코어 → 10%¶
kubectl get node control-plane -o jsonpath='{.status.capacity.cpu}{" / "}{.status.allocatable.cpu}{"\n"}'
nproc
capacity 는 노드가 가진 전체, allocatable 은 시스템 예약을 뺀 나머지입니다. kubeadm 기본은 예약이 없어 둘이 같습니다.
문제가 "노드의 CPU" 라고 하면 capacity 를 씁니다. 1 CPU = 1000m(밀리코어) 입니다.
| capacity.cpu | 밀리코어 | 10% | 매니페스트에 쓸 값 |
|---|---|---|---|
| 2 | 2000m | 200m | cpu: 200m |
| 3 | 3000m | 300m | cpu: 300m |
| 4 | 4000m | 400m | cpu: 400m |
| 8 | 8000m | 800m | cpu: 800m |
CAP=$(kubectl get node control-plane -o jsonpath='{.status.capacity.cpu}')
echo "$(( CAP * 1000 / 10 ))m"
0.2 와 200m 은 같은 값이지만, 시험에서는 단위가 눈에 보이는 m 표기가 실수가 적습니다.
깨 보기 — 요청이 너무 큰 스태틱 파드¶
컨트롤플레인 구성 요소 대신 실습용 스태틱 파드로 같은 현상을 만듭니다. 컨트롤플레인 노드에서:
cat <<'EOF' | sudo tee /etc/kubernetes/manifests/guide-cpu-hog.yaml >/dev/null
apiVersion: v1
kind: Pod
metadata:
name: guide-cpu-hog
namespace: guide-trouble
spec:
containers:
- name: web
image: nginx:1.27
resources:
requests:
cpu: "64" # ← 64 코어
EOF
30초쯤 뒤:
kubectl get pods -n guide-trouble | grep cpu-hog
kubectl get events -n guide-trouble --field-selector reason=OutOfcpu
sudo crictl pods --name guide-cpu-hog
guide-cpu-hog-control-plane 0/1 OutOfcpu 0 25s
LAST SEEN TYPE REASON OBJECT MESSAGE
25s Warning OutOfcpu Pod/guide-cpu-hog-control-plane Node didn't have enough resource: cpu, requested: 64000, used: 950, capacity: 2000
POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME ← 샌드박스조차 만들지 않았다
관찰 포인트:
- Pending 이 아닙니다. 스케줄러가 아니라 kubelet 이 거부했습니다. 메시지의 숫자는 모두 밀리코어입니다(
requested: 64000,capacity: 2000). used는 그 노드에 이미 있는 파드들의 requests 합입니다. 새 요청이allocatable − used안에 들어와야 입장합니다.- 첫 줄(미러 파드)은 타이밍에 따라 보이지 않을 수 있습니다. 그래서 이벤트 ·
crictl pods· kubelet 로그를 같이 봅니다.sudo journalctl -u kubelet --since "5 min ago" --no-pager | grep -iE 'outofcpu|enough resource' - 이 파드가 kube-scheduler 였다면? 스케줄러가 사라지고, 그 뒤 만든 파드는 이벤트 없이 Pending 입니다(10절 표).
고치기 — 노드 CPU 의 10% 로¶
미러 파드는 kubelet 이 만든 읽기 전용 사본이라 kubectl edit 으로는 고칠 수 없습니다. 매니페스트 파일을 고칩니다.
CAP=$(kubectl get node control-plane -o jsonpath='{.status.capacity.cpu}')
sudo vi /etc/kubernetes/manifests/guide-cpu-hog.yaml # cpu: "64" → cpu: 200m
# vi 대신 한 줄로:
# sudo sed -i "s/cpu: \"64\"/cpu: $(( CAP * 100 ))m/" /etc/kubernetes/manifests/guide-cpu-hog.yaml
kubelet 이 바뀐 파일을 읽고 파드를 새로 만듭니다. 30초쯤 뒤:
kubectl get pods -n guide-trouble -o wide | grep cpu-hog
kubectl get pod guide-cpu-hog-control-plane -n guide-trouble \
-o jsonpath='{.spec.containers[0].resources.requests.cpu}{"\n"}'
관찰 포인트: RESTARTS 가 0 이고 AGE 가 새로 시작합니다. 컨테이너를 재시작한 게 아니라 파드를 새로 만든 것입니다(스태틱 파드는 매니페스트 내용이 바뀌면 다른 파드로 취급됩니다).
미러 파드를 kubectl 로 지우거나 고치지 마세요
kubectl delete pod guide-cpu-hog-control-plane 을 해도 kubelet 이 곧 다시 만듭니다(매니페스트가 그대로이므로). 원인은 언제나 파일에 있습니다.
시험 팁 — 컨트롤플레인 구성 요소도 순서는 같습니다
① capacity 확인 → 밀리코어로 바꿔 % 계산 ② 매니페스트를 manifests 밖에 백업 ③ resources.requests.cpu 한 줄만 바꾼다(다른 플래그는 손대지 않는다)
④ sudo crictl ps --name <구성요소> 로 Running 확인 ⑤ 그 구성 요소가 하는 일까지 확인 — 스케줄러라면 kubectl run 으로 새 파드가 노드에 배치되는지까지 봅니다.
실습용 스태틱 파드를 치웁니다. 파일을 지우면 kubelet 이 파드를 거둬 갑니다.
시험에서 빠르게¶
# 네 개의 명령
kubectl get pod <p> -n <ns> -o wide
kubectl describe pod <p> -n <ns> | sed -n '/Events:/,$p'
kubectl logs <p> -n <ns> --previous # -c <컨테이너>
kubectl events -n <ns> --for pod/<p> # 또는 get events --sort-by=.lastTimestamp
# 종료 이유 한 줄로
kubectl get pod <p> -n <ns> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
# Pending
kubectl describe node <n> | grep -A6 'Allocated resources'
kubectl get nodes -L <label> ; kubectl describe node <n> | grep Taints
# 이미지
kubectl set image deployment/<d> <container>=<image> -n <ns>
# Service
kubectl get endpoints <svc> -n <ns>
kubectl get svc <svc> -n <ns> -o jsonpath='{.spec.selector}'
kubectl get pods -n <ns> -l <서비스 셀렉터 그대로> --show-labels
kubectl run t --rm -i --restart=Never --image=busybox:1.28 -n <ns> -- nslookup <svc>
# 노드
ssh <node>; sudo systemctl status kubelet; sudo journalctl -u kubelet -n 50 --no-pager
sudo systemctl enable --now kubelet
# 컨트롤플레인
sudo crictl ps -a --name <component>; sudo crictl logs <ID>
ls /var/log/pods/ ; ls /etc/kubernetes/manifests/
sudo grep -nE 'etcd-servers|listen-client-urls' /etc/kubernetes/manifests/{kube-apiserver,etcd}.yaml # 2379 끼리 맞나
# 스태틱 파드 CPU 를 노드의 N%
kubectl get node <n> -o jsonpath='{.status.capacity.cpu}' # 2 → 2000m → 10% = 200m
kubectl get events -n <ns> --field-selector reason=OutOfcpu
정리¶
먼저 노드와 컨트롤플레인이 정상인지 확인하고, 그다음 실습 리소스를 지웁니다.
# 9) · 10) · 11) · 12) 를 되돌렸는지
kubectl get nodes # 모두 Ready
kubectl get pods -n kube-system -l tier=control-plane # 모두 1/1 Running
sudo grep -c no-such-flag /etc/kubernetes/manifests/kube-scheduler.yaml # 0
sudo grep -c ca-typo /etc/kubernetes/manifests/kube-apiserver.yaml # 0
sudo rm -f /etc/kubernetes/manifests/guide-cpu-hog.yaml # 12) 의 실습용 스태틱 파드
kubectl get deploy coredns -n kube-system # 8) 에서 되돌린 개수 (예: 2/2)
# 2) 에서 붙인 노드 레이블
kubectl label node worker-1 disktype-
# 실습 리소스
kubectl delete namespace guide-trouble
cd ~ && rm -rf ~/guide-trouble
metrics-server 를 설치했다면
남겨 두면 이후 강의에서도 kubectl top 을 쓸 수 있습니다. 지우려면
kubectl delete -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml.
확인 질문¶
1. 파드가 Pending 입니다. kubectl logs 를 보면 무엇이 나오나요? 어디를 봐야 하나요?
Pending 은 컨테이너가 시작도 안 했으므로 로그가 없습니다. describe pod 의 Events 에서 FailedScheduling 메시지를 봅니다.
메시지가 노드마다 탈락 이유(자원 · taint · selector)를 나눠 알려 줍니다. 이벤트조차 없으면 스케줄러를 의심합니다.
2. CrashLoopBackOff 파드에서 kubectl logs 가 비어 있습니다. 다음 명령은?
kubectl logs <파드> --previous 로 죽은 컨테이너의 마지막 로그를 봅니다. 그래도 비어 있으면
describe 의 Last State 에서 Reason(StartError, OOMKilled)과 Message 를 봅니다.
3. STATUS 가 CrashLoopBackOff 인데 원인이 메모리인지 어떻게 확인하나요?
describe pod 의 Last State 에 Reason: OOMKilled, Exit Code: 137 이 있으면 메모리 limit 초과입니다.
jsonpath 로 .status.containerStatuses[0].lastState.terminated.reason 을 봐도 됩니다.
4. 파드는 Running 인데 READY 가 0/1 이고 Service 로 접속이 안 됩니다. 원인은?
readinessProbe 실패입니다. 준비되지 않은 파드는 Endpoints 에서 빠집니다.
describe pod 의 Readiness probe failed 이벤트에서 이유(404, 연결 거부 등)를 확인하고 프로브나 앱을 고칩니다.
5. Service 의 Endpoints 에 IP 는 있는데 Connection refused 가 납니다. 무엇을 비교하나요?
Service 의 targetPort 와 파드의 containerPort(실제로 앱이 듣는 포트)를 비교합니다.
Endpoints 가 <none> 이면 selector 쪽, IP 가 있으면 포트 쪽입니다.
6. kubelet 을 멈춘 노드의 Ready 조건은 False 인가요 Unknown 인가요? 어떤 taint 가 붙나요?
Unknown 입니다(Message: Kubelet stopped posting node status.). 노드 컨트롤러가
node.kubernetes.io/unreachable 을 NoSchedule 과 NoExecute 두 가지로 붙이고, 기존 파드는 기본 300초 뒤 축출됩니다.
7. kubectl 이 connection refused 로 전부 실패합니다. 어디서 무엇을 보나요?
컨트롤플레인 노드에 ssh 해서 /etc/kubernetes/manifests/kube-apiserver.yaml 을 확인하고,
crictl ps -a 로 apiserver 컨테이너 상태를, crictl logs <ID> 나 /var/log/pods/ 로 로그를 봅니다. 컨테이너가 아예 없으면 journalctl -u kubelet 입니다.
8. 스태틱 파드 매니페스트 백업을 /etc/kubernetes/manifests/kube-scheduler.yaml.bak 으로 두면 무엇이 문제인가요?
kubelet 은 그 디렉터리의 파일을 모두 스태틱 파드로 읽으려 하므로 같은 이름의 파드가 중복 정의되어 문제가 생깁니다. 백업은 반드시 manifests 디렉터리 밖에 둡니다.
9. kubectl 이 refused 이고 journalctl -u kubelet 에 dial tcp ...:6443: connect: connection refused 가 가득합니다. kubelet 을 고쳐야 하나요?
아닙니다. kubelet 도 API 서버의 클라이언트라서, API 서버가 없다는 증상을 보여 줄 뿐입니다.
sudo crictl ps -a --name kube-apiserver → crictl logs 로 API 서버가 왜 죽는지 보고, 가리키는 플래그를 매니페스트에서 고칩니다.
--etcd-servers 라면 etcd 의 클라이언트 포트 2379 여야 합니다(2380 은 etcd 멤버끼리 쓰는 peer 포트).
10. CPU 가 4개인 노드에서 스태틱 파드의 CPU 요청을 노드 CPU 의 10% 로 하라고 합니다. 무엇을 어디에 씁니까? 요청이 너무 크면 어떤 상태가 됩니까?
4 CPU = 4000m 이므로 10% 는 400m 입니다. /etc/kubernetes/manifests/<파드>.yaml 의 resources.requests.cpu 를 바꾸면 kubelet 이 파드를 새로 만듭니다.
요청이 노드에 들어가지 않으면 스태틱 파드는 Pending 이 아니라 kubelet 이 OutOfcpu 로 거부합니다(스케줄러를 거치지 않으므로).
다음 단계 — 채점 실습¶
개념이 잡혔으면 시험 형식으로 풀어 봅니다. 이번에는 답이 없습니다. 시작하면 고장 난 리소스가 미리 만들어집니다.
| 문제 | 이 페이지에서 연습한 것 |
|---|---|
| Q1 CrashLoopBackOff 진단 | 4) logs --previous · Last State · Exit Code 표 |
| Q2 ImagePullBackOff 수정 | 3) Events 의 pull 실패 메시지 · 이미지 교체 |
| Q3 Endpoints 가 비어 있는 Service | 7) ① 셀렉터 · 6) Ready 아닌 파드 |
| Q4 노드 상태 점검 | 9) NotReady · Conditions · systemctl · journalctl |
| Q5 API 서버 복구 (kubectl refused) | 11) crictl → 로그 → 매니페스트 플래그 · 2379 vs 2380 · kubelet 의 6443 refused 는 증상 |
| Q6 kube-scheduler CPU 요청 (노드 CPU 의 10%) | 12) capacity → 밀리코어 계산 · OutOfcpu · 매니페스트 수정 · 10) 스케줄러가 없을 때의 Pending |
Q5 · Q6 은 컨트롤플레인을 고장 냅니다
그 문제로 넘어가는 순간 채점 스크립트가 kube-apiserver.yaml(Q5) · kube-scheduler.yaml(Q6)을 바꿉니다. Q4 를 마치면 VM 스냅샷을 먼저 찍으세요.
컨트롤플레인 노드에서, 암호 없는 sudo 로 실행해야 합니다. 원본은 /var/tmp/cka-lab-backup/ 에 백업되고 bash exam.sh clean 이 되돌립니다.