Kubernetes Kubernetes

K8s 트러블슈팅을 관통하는 3가지 멘탈 모델

Declarative Control, Pod Lifecycle, Networking — 대부분의 K8s 문제가 3가지 멘탈 모델로 수렴하는 이유.

도입

트러블슈팅 가이드는 “CrashLoopBackOff이면 이렇게 하세요”라고 말합니다. 그런데 처음 보는 에러가 뜨면? 또 다른 가이드를 찾아야 합니다.

원리를 알면 다릅니다. K8s에서 발생하는 대부분의 문제는 3가지 멘탈 모델 중 하나(또는 교차)로 수렴합니다.

Declarative Control — 누가, 무엇으로 관리하는가
Pod Lifecycle — Pod가 왜 이 상태인가
Networking — 트래픽이 어디서 끊기는가

이 3가지를 이해하면 처음 보는 문제 앞에서도 “첫 번째 질문”을 던질 수 있습니다.


Declarative Control — “누가, 무엇으로 관리하는가”

개념

K8s는 level-triggered 시스템입니다.

  • edge-triggered: 이벤트(변화)에 반응 → 이벤트를 놓치면 상태 불일치
  • level-triggered: 현재 상태를 주기적으로 확인 → 놓쳐도 다음 루프에서 보정

모든 컨트롤러는 같은 루프를 무한 반복합니다:

flowchart LR
A[Desired State] -->|비교| B[Current State]
B -->|차이 발견| C[조정 Action]
C -->|반영| B

이 루프가 K8s의 자가치유(self-healing)를 만듭니다. 컨트롤러가 살아있는 한, 상태는 결국 수렴합니다.

리소스는 ownerReferences로 부모-자식 계층을 형성합니다.

Deployment → ReplicaSet → Pod
StatefulSet → Pod + PVC
CronJob → Job → Pod

컨트롤러는 자신이 소유한 자식 리소스를 관리합니다. 자식을 수정하면 부모가 되돌리고, 자식을 삭제하면 부모가 다시 만듭니다.

반대로 부모를 삭제하면? Garbage Collection이 자식을 정리합니다:

삭제 모드동작
Background (기본)부모 즉시 삭제, 자식은 GC가 비동기 정리
Foreground자식 먼저 삭제, 완료 후 부모 삭제
Orphan부모만 삭제, 자식은 남김

단, StatefulSet의 PVC는 의도적으로 보존됩니다 — 데이터 유실 방지 설계입니다.

삭제가 즉시 완료되지 않는 경우도 있습니다. 리소스에 Finalizer가 등록되어 있으면, 해당 컨트롤러가 정리 작업을 완료할 때까지 리소스는 Terminating 상태에 머뭅니다.

레버리지

K8s에서 가장 흔한 세 가지 혼란이 있습니다:

  • “설정을 바꿨는데 되돌아간다”
  • “리소스를 삭제했는데 다시 뜬다”
  • “삭제했는데 Terminating에서 안 끝난다”

세 증상 모두 같은 모델로 설명됩니다:

  • 되돌아간다 → 수정한 곳이 Source of Truth가 아닙니다. 컨트롤러는 자기가 아는 Desired State로 되돌립니다.
  • 재생성된다 → 소유자(부모 컨트롤러)가 desired state를 맞추고 있습니다. ownerReferences를 확인하면 소유자를 알 수 있습니다.
  • 안 지워진다 → Finalizer가 등록된 컨트롤러가 정리 작업을 완료하지 못한 것입니다.

소유자 확인: kubectl get <resource> -o yaml | grep -A5 ownerReferences

Source of Truth 체인

“되돌아간다”와 “재생성된다” 모두, 수정해야 할 곳은 체인의 최상위입니다.

단순한 경우:

Deployment → ReplicaSet → Pod

실전 복잡도:

Git Repo → ArgoCD → Helm Release → Deployment → RS → Pod
EnvoyProxy CRD → GatewayClass → Controller → Service → CCM → Cloud LB

체인의 어디를 수정하든, 그 위에 컨트롤러가 있으면 되돌아갑니다. 가장 위(upstream)를 찾아서 수정하는 것이 핵심입니다.

적용 범위

증상원인 / 확인
Cloud 콘솔에서 LB 설정 변경 → 되돌아감K8s Service 어노테이션 (또는 그 위의 CRD)이 Source of Truth
HPA가 늘린 replica를 ArgoCD가 원복Git manifest에서 replicas 필드 제거 → HPA에 소유권 이전
ConfigMap을 kubectl edit → 원복됨GitOps 컨트롤러가 Git 상태로 되돌림
cert-manager 꺼진 후 인증서 만료컨트롤러 미실행 = 조정 중단 → drift 누적
Pod 삭제해도 즉시 재생성ownerReferences 확인 — RS/STS/DaemonSet이 소유 중
Deployment 삭제했는데 Pod가 남음GC 지연 또는 orphan 모드로 삭제됨
StatefulSet 삭제 후 PVC 남음정상 (by design) — 데이터 보존
Namespace가 Terminating에 멈춤하위 리소스의 Finalizer 처리 미완료
CRD 삭제 후 CR이 TerminatingCRD 먼저 삭제 → 컨트롤러가 Finalizer 처리 불가
PVC가 Terminating에 멈춤pvc-protection Finalizer — Pod가 아직 사용 중

진단 공식

"되돌아간다" or "재생성된다" or "안 지워진다"
→ 되돌아감/재생성:
이 리소스를 관리하는 컨트롤러는? (ownerReferences 확인)
→ 그 컨트롤러의 Source of Truth는?
→ 체인의 최상위를 수정
→ 삭제 안 됨:
어떤 Finalizer가 남아있는가? (kubectl get -o yaml)
→ 그 Finalizer를 처리하는 컨트롤러가 실행 중인가?
→ 컨트롤러 복구 또는 (이해한 뒤) Finalizer 수동 제거

Pod Lifecycle — “Pod가 왜 이 상태인가”

개념

Pod는 명확한 상태 머신을 따릅니다:

flowchart LR
A[Pending] -->|스케줄링| B[PVC Binding /<br>Volume Attach]
B -->|마운트| C[ContainerCreating]
C --> D[Running]
D --> E[Succeeded / Failed]
A --> F[Unschedulable]
B --> G[Multi-Attach /<br>Mount Error]
C --> H[ImagePullBackOff]
D --> I[CrashLoopBackOff]

각 상태가 실패할 수 있는 지점이 다르고, 진단 방법도 다릅니다.

레버리지

Pod의 상태를 읽으면 문제가 어느 단계에서 발생했는지 즉시 알 수 있습니다. “안 된다”라는 막연한 상황이 “Pending이다” → “스케줄링 실패다”로 구체화됩니다.

단계별 진단

Pending — 스케줄러가 노드를 찾지 못함

스케줄러 파이프라인: Filter(배제) → Score(순위) → Bind(확정)

Filter에서 모든 노드가 탈락하면 Pending이 됩니다. 주요 카테고리:

카테고리Events 메시지원인
ResourcesInsufficient cpu/memory요청량 > 노드 여유 (requests 기준, 실사용량 아님)
Affinitydidn't match pod anti-affinity rules모든 노드에 충돌 Pod 존재
Taintshad taint that pod didn't tolerate유지보수 후 taint 미제거 등
Volumevolume node affinity conflictPV가 특정 zone에 묶임

실전 함정:

  • Request vs 실사용량: 스케줄러는 requests만 봅니다. 클러스터가 한가해 보여도 requests 합계가 allocatable을 넘으면 Pending이 됩니다
  • Resource fragmentation: 5노드 x 2CPU, 각 1.5CPU 사용 → 총 2.5CPU 여유지만 1CPU Pod 스케줄 불가 (단일 노드에 1CPU 없음)
  • DaemonSet의 숨은 소비: 모니터링 DaemonSet 노드당 500m → 4CPU 노드의 12.5% 선점
  • StorageClass Immediate: PVC 먼저 생성 → zone-A에 볼륨 → Pod가 zone-B 필요 → 영원히 Pending. WaitForFirstConsumer로 해결할 수 있습니다

ContainerCreating에서 멈춤 — 볼륨 단계

스케줄링 성공 후, 컨테이너가 시작되기 전에 볼륨 단계를 거칩니다:

PVC Binding (PVC ↔ PV 매칭)
→ Volume Attach (PV를 노드에 연결)
→ Volume Mount (컨테이너 경로에 마운트)

각 단계에서 실패할 수 있습니다:

Events 메시지원인단계
persistentvolumeclaim not foundPVC가 존재하지 않음Binding
unbound immediate PersistentVolumeClaimsPVC가 Pending — 매칭 PV 없거나 프로비저너 실패Binding
Multi-Attach error for volumeRWO 볼륨이 이전 노드에서 detach 안 됨Attach
FailedAttachVolume노드당 볼륨 수 제한 초과 (클라우드별 상이)Attach
FailedMount / MountVolume.SetUp failed마운트 타임아웃, 파일시스템 손상Mount

흔한 시나리오:

  • 노드 이동 후 Multi-Attach: Pod가 새 노드에 뜨는데, RWO 볼륨이 이전 노드에서 detach되지 않는 경우입니다. 이전 노드가 NotReady면 6분(기본 attachDetachReconcilerSyncDuration) 동안 대기합니다. 급하면 이전 노드의 VolumeAttachment를 수동 삭제할 수 있습니다
  • PVC Pending: kubectl describe pvc로 Events를 확인합니다. 프로비저너 Pod가 살아있는지, StorageClass가 존재하는지 점검합니다
  • 볼륨 Terminating: kubernetes.io/pvc-protection 파이널라이저가 아직 사용 중인 PVC 삭제를 차단합니다. Pod가 PVC를 사용 중이면 삭제할 수 없습니다

ImagePullBackOff — 이미지를 못 가져옴

원인확인
이미지명/태그 오타spec.containers[].image 확인
Private registry 인증 실패imagePullSecrets 설정 확인
네트워크 차단노드에서 registry 접근 가능한지

CrashLoopBackOff — 컨테이너가 시작 후 즉시 종료를 반복

kubelet이 재시작 시도 → 실패 → 대기(10s, 20s, 40s… 최대 5분) → 반복합니다.

원인확인
앱 에러kubectl logs <pod> (이전 로그: --previous)
OOMKilledkubectl describe podLast State: OOMKilled → memory limit 증가
잘못된 command/argsDockerfile ENTRYPOINT와 Pod spec 충돌
의존성 미충족DB 연결 실패 등 — 환경변수, Service 이름 확인

Running이지만 트래픽이 안 옴 — Readiness Probe 실패

Pod는 Running이지만 Ready 상태가 아니면 Service Endpoint에서 제외됩니다. 공식 문서에서도 이 패턴을 다루고 있습니다.

kubectl get pod <name> -o wide
# READY 열이 0/1이면 readiness probe 실패
  • Probe path/port 오타
  • 앱 startup 시간 > initialDelaySeconds
  • 리소스 부족으로 응답 지연 > timeoutSeconds

진단 공식

Pod 문제
→ kubectl get pod <name> → STATUS 컬럼 확인
→ Pending → describe pod → Events (스케줄링)
→ ContainerCreating → describe pod → Events (볼륨 attach/mount)
→ kubectl get pvc (PVC Pending 여부)
→ ImagePullBack → describe pod → Events (이미지)
→ CrashLoopBack → logs --previous (앱 로그)
→ Running/NotReady → describe pod → Conditions (probe)

Networking — “어디서 끊기는가”

개념

K8s 네트워킹은 4개 계층으로 나뉘고, 각 계층마다 실패 모드가 다릅니다:

┌──────────────────────────────────────────────┐
│ Layer 4: External → Cluster │
│ Gateway API / LoadBalancer / NodePort │
├──────────────────────────────────────────────┤
│ Layer 3: Pod → Service │
│ kube-proxy (iptables/IPVS) + CoreDNS │
├──────────────────────────────────────────────┤
│ Layer 2: Pod → Pod (across nodes) │
│ CNI plugin (Calico, Cilium, Flannel) │
├──────────────────────────────────────────────┤
│ Layer 1: Container → Container (same Pod) │
│ Shared localhost │
└──────────────────────────────────────────────┘

레버리지

“서비스에 접근이 안 된다”는 4개 계층 중 어디든 원인일 수 있습니다. 계층을 모르면 DNS 문제인데 CNI를 의심하고, Gateway 문제인데 Service를 뒤지게 됩니다.

진단 방향은 아래에서 위로 (bottom-up)입니다. Pod가 살아있는지 먼저 확인하고, 그다음 Service, 그다음 Gateway/LB 순으로 봅니다.

계층별 진단

Layer 1: Container → Container

같은 Pod 내 컨테이너는 localhost를 공유합니다. 거의 깨지지 않습니다. 깨진다면 Pod spec 자체가 잘못된 것입니다.

Layer 2: Pod → Pod

CNI 플러그인이 Pod 간 통신을 담당합니다.

증상원인
Pod 간 ping 불가CNI 미설치/장애, 노드 라우팅 문제
특정 Pod만 통신 불가NetworkPolicy가 차단
# Pod에서 다른 Pod IP로 직접 통신 테스트
kubectl exec <pod> -- curl <other-pod-ip>:<port>

Layer 3: Pod → Service

Service는 가상 IP(ClusterIP)입니다. 실체가 없고, iptables/IPVS 규칙으로만 존재합니다. kube-proxy가 이 규칙을 관리하고, CoreDNS가 이름 → ClusterIP를 해석합니다.

증상원인확인
Service에 접근 불가Endpoint가 없음kubectl get endpoints <svc>
Endpoint가 비어있음selector ↔ Pod label 불일치selector와 Pod labels 대조
DNS 해석 실패CoreDNS Pod 장애kubectl get pods -n kube-system -l k8s-app=kube-dns
간헐적 연결 끊김conntrack 테이블 포화노드의 conntrack_max 확인

가장 흔한 실수는 Service selector와 Pod label 불일치, 그리고 port와 targetPort 혼동입니다. Debug Services 가이드에서 이 진단 과정을 상세히 다루고 있습니다.

Service:
selector: app=web ←─┐
port: 80 │ 이 3개가 모두 맞아야 트래픽이 흐릅니다
targetPort: 8080 ←─┐│
││
Pod: ││
labels: app=web ────┘│
containerPort: 8080 ─────┘

Layer 4: External → Cluster

외부 트래픽이 클러스터에 진입하는 경로: Gateway API / LoadBalancer / NodePort.

Gateway API는 Ingress의 후속으로, GatewayClass → Gateway → HTTPRoute 3계층 구조입니다. 각 계층마다 독립적으로 실패할 수 있습니다:

증상원인
Gateway Programmed: FalseGatewayClass 미존재 또는 컨트롤러(Envoy, Istio 등) 미실행
HTTPRoute Accepted: FalseparentRefs의 Gateway 이름/namespace 불일치, listener 미매칭
Route 연결됐는데 트래픽 안 옴backendRefs의 Service 이름/포트 오류
LB가 PendingCCM 미실행 또는 cloud 인증 만료
502/504 에러Backend Pod not ready, health check 실패
TLS 에러인증서 만료, Secret 미연결
특정 노드에서만 접근 불가externalTrafficPolicy: Local + 해당 노드에 Pod 없음

진단 공식

"접근이 안 된다"
→ Pod는 살아있는가? (Layer 1-2)
→ Service에 Endpoint가 있는가? (Layer 3)
→ 외부에서 Gateway/LB까지 도달하는가? (Layer 4)
→ 아래에서 위로, 각 계층의 연결을 확인

모델 교차: 복합 문제

까다로운 문제는 단일 모델이 아니라 교차점에서 발생합니다. 각 모델 하나씩만 보면 정상인데, 두 조건이 동시에 걸리면서 장애가 됩니다. 한 모델로 설명이 안 되면, 두 번째 모델을 겹쳐보는 것이 포인트입니다.

Rolling Update 데드락 (Declarative Control + Pod Lifecycle)

  • Declarative Control 관점: 새 RS가 Pod를 만들고, 이전 RS가 줄이면 끝납니다. 정상.
  • Pod Lifecycle 관점: 노드에 자리가 있으면 스케줄링 성공입니다. 정상.
  • 교차하면: 이전 Pod가 노드를 점유 중이라 새 Pod를 띄울 곳이 없고, maxUnavailable: 0이라 이전 Pod를 내릴 수도 없습니다.
Deployment
├── old RS (replica: 2) — 기존 Pod가 노드를 점유
└── new RS (replica: 2) — 새 Pod 생성 시도

데드락 조건: required podAntiAffinity(노드당 1개) + maxUnavailable: 0 + 노드 수 = replica 수

새 Pod를 띄울 노드가 없고, 이전 Pod를 내릴 수도 없습니다. kubernetes/kubernetes#116891 — K8s에서 공식 미해결 이슈입니다.

해소법:

  • maxUnavailable: 1 (순간 용량 감소 허용)
  • preferred anti-affinity (임시 같은 노드 허용)
  • 이전 RS를 수동 scale 0 (긴급 대응)

LB Health Check 실패 (Pod Lifecycle + Networking)

  • Pod Lifecycle 관점: Pod가 node-1에 배치되어 정상 Running입니다. 문제 없습니다.
  • Networking 관점: LB가 각 노드에 health check를 보냅니다. 정상 동작입니다.
  • 교차하면: externalTrafficPolicy: Local은 Pod가 있는 노드에서만 응답합니다. node-2에 Pod가 없으면 health check 실패 → 503이 발생합니다.

노드 이동 후 Multi-Attach (Pod Lifecycle + Declarative Control)

  • Pod Lifecycle 관점: 새 노드에 Pod가 스케줄링됩니다. 정상.
  • Declarative Control 관점: AD Controller가 볼륨 attach/detach를 조정합니다. 정상.
  • 교차하면: RWO 볼륨은 동시에 하나의 노드에만 attach 가능합니다. 이전 노드가 NotReady면 detach 완료까지 6분(기본값) 대기하게 되고, 그 사이 새 Pod는 ContainerCreating에 멈춥니다.

설정 변경이 5단계 뒤에 반영 (Declarative Control 다단계)

단일 모델이지만, 체인이 길어서 복합적으로 느껴지는 경우입니다:

EnvoyProxy CRD → GatewayClass → Controller → Service → CCM → Cloud LB

중간 어디를 수정하든 그 위의 컨트롤러가 되돌립니다. 최상위(upstream)를 찾아야 합니다.


진단 알고리즘

모든 K8s 문제에 적용할 수 있는 4단계입니다:

Step 1: 어떤 모델인가?
"되돌아간다/재생성/안 지워진다" → Declarative Control
→ "되돌아감/재생성" → Source of Truth 체인 역추적
→ "삭제 안 됨" → Finalizer 확인
"Pending/Crash/NotReady" → Pod Lifecycle
"접근이 안 된다" → Networking
Step 2: Desired vs Current 갭은?
spec(원하는 상태)과 status(현재 상태)를 비교합니다
Step 3: 무엇이 수렴을 막는가?
컨트롤러가 안 돌고 있는가? 잘못된 레벨을 수정하고 있는가? (Declarative Control)
어떤 Filter에 걸리는가? (Pod Lifecycle)
어떤 계층이 끊겼는가? (Networking)
Step 4: Events를 확인합니다
kubectl describe <resource> → Events
kubectl get events --field-selector type!=Normal

핵심 정리

증상멘탈 모델첫 질문
되돌아간다 / 재생성 / 안 지워진다Declarative ControlSource of Truth는? 소유자는? Finalizer는?
Pod가 뜨지 않거나 죽는다Pod LifecycleSTATUS가 뭐라고? Events에 뭐라고?
트래픽이 도달하지 않는다Networking어떤 계층에서 끊기는가?
복합 문제모델 교차어떤 모델 2개가 충돌하는가?

증상별 해결법을 외우는 것보다, 모델을 익히는 것이 효과적입니다. 3가지면 대부분의 K8s 트러블슈팅이 수렴합니다.

관련 콘텐츠

댓글