Certified Kubernetes Application Developer (CKAD) #2 Pod와 컨테이너 라이프사이클: restart policy와 컨테이너 상태

#1에서 2시간 실기를 운영하기 위한 kubectl 환경(alias, dry-run, generators, vim 들여쓰기, context 전환)을 손에 익혔습니다. 셋업이 끝났으니 이제 CKAD의 가장 작은 배포 단위인 Pod 로 들어갑니다. Pod는 모든 워크로드(Deployment,Job,DaemonSet)가 결국 만들어 내는 실체이고, 시험에서 “이 Pod가 왜 안 뜨는가"를 진단하는 능력은 도메인을 가리지 않고 점수에 직결됩니다.

이번 글은 두 축으로 정리하겠습니다. 하나는 Pod가 어떤 상태를 거치는가(생애 주기와 restartPolicy)이고, 다른 하나는 그 안의 컨테이너가 왜 그 상태에 멈췄는가(컨테이너 상태와 종료 코드)입니다. 두 축을 명령과 매니페스트로 직접 확인하며 익히겠습니다.

Pod가 거치는 생애 주기 #

Pod는 만들어진 뒤 사라질 때까지 정해진 phase 를 거칩니다. k get podSTATUS 열에 보이는 값 가운데 상당수가 이 phase이고, 나머지는 컨테이너 상태의 reason 입니다. 둘을 섞어 두면 진단이 어긋나므로 먼저 phase 부터 구분합니다. K8s 실무 트랙 #3에서 Pod의 기본 개념을 다뤘다면, 여기서는 그 상태 전이를 시험 관점으로 좁혀 보겠습니다.

phase의미
PendingAPI 서버에는 등록됐지만 아직 컨테이너가 실행 전. 스케줄 대기, 이미지 pull, volume 마운트 중인 단계
RunningPod가 노드에 바인딩되고 모든 컨테이너가 생성됨. 최소 한 컨테이너가 실행 중이거나 시작,재시작 중
Succeeded모든 컨테이너가 정상(종료 코드 0) 종료되고 다시 시작되지 않음. Job의 정상 완료 형태
Failed모든 컨테이너가 종료됐고 그중 하나 이상이 비정상(0이 아닌 코드) 종료
Unknown노드와 통신이 끊겨 Pod 상태를 알 수 없음. 보통 노드 장애

phase는 kubectl get pod -o jsonpath로 직접 꺼낼 수 있습니다.

k get pod nginx -o jsonpath='{.status.phase}'

여기서 핵심은 Pending이 오래 지속되면 스케줄,이미지,volume 문제이고, Failed 나 반복 재시작은 컨테이너 내부 문제라는 1차 분기입니다. 어느 쪽이냐에 따라 들여다볼 곳이 달라집니다.

restartPolicy: 누가 무엇을 쓰는가 #

restartPolicy는 컨테이너가 종료됐을 때 kubelet이 같은 노드에서 다시 시작할지를 결정합니다. Pod 단위 설정이며 값은 세 가지입니다.

동작기본으로 쓰는 워크로드
Always종료 코드와 무관하게 항상 재시작. 서비스가 계속 떠 있어야 하는 경우Deployment, ReplicaSet, DaemonSet, StatefulSet
OnFailure비정상(0이 아닌 코드) 종료일 때만 재시작. 정상 완료(0)면 그대로 둠Job, CronJob
Never종료 코드와 무관하게 절대 재시작하지 않음한 번만 실행하는 일회성 작업

여기서 시험에 자주 나오는 함정이 있습니다. Deployment가 만드는 Pod는 restartPolicy가 항상 Always로 고정됩니다. Deployment 매니페스트에 restartPolicy: OnFailure를 넣으면 검증 오류가 납니다. 반대로 Job은 restartPolicy를 명시해야 하며 Always는 허용되지 않습니다. 즉, 워크로드 종류가 restartPolicy의 가능한 값을 제약합니다.

restartPolicy를 직접 지정한 Pod 매니페스트는 다음과 같습니다.

apiVersion: v1
kind: Pod
metadata:
  name: oneshot
spec:
  restartPolicy: OnFailure   # 정상 완료면 멈추고, 실패하면 재시작
  containers:
    - name: worker
      image: busybox:1.36
      command: ["sh", "-c", "echo done; exit 0"]

위 Pod는 exit 0으로 정상 종료하므로 restartPolicy: OnFailure에서는 재시작하지 않고 phase가 Succeeded로 갑니다. commandexit 1로 바꾸면 비정상 종료라 kubelet이 계속 재시작하면서 다음 절의 CrashLoopBackOff가 재현됩니다.

restartPolicy의 재시작은 항상 같은 노드에서 컨테이너를 재시작하는 것이지, Pod를 다른 노드로 옮기는 것이 아닙니다. 노드 자체가 죽었을 때 다른 노드에 새 Pod를 띄우는 일은 Deployment,ReplicaSet 같은 상위 컨트롤러의 몫입니다.

컨테이너 상태와 reason 읽기 #

phase가 Pod 전체의 거시 상태라면, 컨테이너 상태는 컨테이너 하나하나의 미시 상태입니다. k describe podState 항목에 나타나며 세 가지입니다.

상태의미
Waiting아직 실행 전. 이미지 pull, 의존성 대기 등. reason에 왜 기다리는지가 적힘
Running정상 실행 중. startedAt 시각 동반
Terminated실행이 끝남. exitCode, reason, startedAt, finishedAt 동반

진단의 8할은 Waiting과 Terminated에 붙는 reason 을 읽는 일입니다. 시험에서 반복해 마주치는 reason을 정리하면 다음과 같습니다.

reason상태의미와 1차 원인
ContainerCreatingWaiting컨테이너 생성 중. volume 마운트,이미지 준비 단계. 오래 멈추면 volume,secret 누락 의심
ImagePullBackOffWaiting이미지 pull 실패가 반복되어 백오프 중. 이미지 이름 오타, 태그 없음, private 레지스트리 인증 누락
ErrImagePullWaiting이미지 pull 즉시 실패. ImagePullBackOff의 직전 단계
CrashLoopBackOffWaiting컨테이너가 시작 직후 죽기를 반복해 재시작 간격을 늘려 가며 대기 중. 컨테이너 내부 프로세스 문제
OOMKilledTerminated메모리 한도(limits.memory) 초과로 커널이 강제 종료. 종료 코드 137
CompletedTerminated정상(종료 코드 0) 종료. Job의 성공 형태
ErrorTerminated0이 아닌 코드로 종료. 애플리케이션 오류

여기서 가장 오해가 많은 것이 CrashLoopBackOff는 에러가 아니라 상태라는 점입니다. 컨테이너가 자꾸 죽으니 kubelet이 재시작 간격을 10초, 20초, 40초처럼 지수로 늘려(최대 5분) 대기하는 중이라는 뜻입니다. 원인은 BackOff 자체가 아니라 컨테이너가 왜 죽는가에 있으므로, 로그와 종료 코드로 내려가야 합니다.

종료 코드 읽는 법 #

Terminated 상태의 exitCode는 원인을 빠르게 좁혀 줍니다. 시험에서 외워 두면 좋은 값입니다.

종료 코드의미
0정상 종료. Completed
1일반적인 애플리케이션 오류. 로그를 봐야 함
137128 + 9(SIGKILL). 강제 종료. OOMKilled 거나 grace period 초과 후 강제 kill
143128 + 15(SIGTERM). 정상 종료 신호를 받고 끝남. 보통 정상적인 graceful shutdown

종료 코드는 다음 한 줄로 바로 확인합니다.

k get pod oneshot -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'

137을 봤다면 OOMKilled 인지부터 확인하고(describe의 reason), 맞다면 limits.memory를 늘리거나 애플리케이션 메모리 사용을 줄이는 방향입니다. 리소스 limits는 #16에서 자세히 다루겠습니다.

진단 명령 세트 #

상태 분류가 끝나면 실제로 들여다보는 명령은 정해져 있습니다. 이 순서를 손에 익히는 것이 시험 트러블슈팅 속도를 결정합니다.

# 1) 넓게 본다: 어느 Pod가 어떤 상태이고 재시작 횟수는 몇 번인가
k get pod -o wide

# 2) 한 Pod를 깊게 본다: 컨테이너 State,reason,exitCode,이벤트가 한 화면에
k describe pod oneshot

# 3) 현재 컨테이너 로그
k logs oneshot

# 4) 직전(크래시 전) 컨테이너 로그: CrashLoopBackOff 진단의 핵심
k logs oneshot --previous

# 5) 클러스터 이벤트를 시간순으로 (스케줄 실패,pull 실패가 여기 찍힘)
k get events --sort-by=.metadata.creationTimestamp

k get podRESTARTS 열은 1차 신호입니다. 숫자가 빠르게 늘면 CrashLoop이고, 0 인 채 Pending이면 스케줄,이미지,volume 문제입니다. describe 하단의 Events 섹션에는 Failed to pull image, Back-off restarting failed container, OOMKilled 같은 메시지가 직접 찍히므로 reason의 근거를 여기서 확인합니다.

CrashLoopBackOff의 결정적 단서는 k logs --previous 입니다. 컨테이너가 이미 죽어 현재 로그가 비어 있어도, 직전 인스턴스의 로그에는 죽은 이유(설정 파일 없음, 포트 충돌, 잘못된 인자 등)가 남아 있기 때문입니다.

시험 단골: “이 Pod가 왜 재시작을 반복하는가” #

CKAD에서 자주 나오는 유형이 동작이 깨진 Pod를 주고 원인을 찾아 고치게 하는 것입니다. 다음 순서로 내려가면 거의 모든 케이스가 풀립니다.

  1. k get pod -o wide로 STATUS와 RESTARTS를 확인합니다. CrashLoopBackOff이고 RESTARTS가 늘고 있는지 봅니다.
  2. k describe pod <이름>으로 컨테이너 State, lastState의 reason과 exitCode, 하단 Events를 읽습니다.
  3. reason이 ImagePullBackOff 면 이미지 이름,태그,레지스트리 인증을 봅니다(컨테이너 안에 들어갈 필요 없음).
  4. reason이 CrashLoopBackOff 면 k logs <이름> --previous로 직전 로그에서 죽은 원인을 찾습니다.
  5. 종료 코드가 137이면 OOMKilled 인지 확인하고 limits.memory를 점검합니다.
  6. 원인을 매니페스트에서 고친 뒤 k apply 또는 Pod를 지우고 다시 만들어 STATUS가 Running으로 가는지 확인합니다.

핵심은 컨테이너 안으로 exec 하기 전에 describe와 logs로 충분히 좁히는 것입니다. 시험 시간은 한정돼 있고, 대부분의 원인은 describe 한 화면과 직전 로그에서 드러납니다.

일부러 실패하는 컨테이너로 CrashLoop 재현하기 #

직접 한 번 만들어 보면 진단 화면이 손에 익습니다. 다음 Pod는 시작 직후 1초 뒤 비정상 종료해 CrashLoopBackOff를 재현합니다.

apiVersion: v1
kind: Pod
metadata:
  name: crasher
spec:
  restartPolicy: Always   # 비정상 종료마다 계속 재시작 → CrashLoopBackOff
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "echo starting; sleep 1; echo boom >&2; exit 1"]
k apply -f crasher.yaml
k get pod crasher -w          # STATUS가 Running → CrashLoopBackOff로 가는 것을 관찰
k describe pod crasher        # lastState.terminated: reason=Error, exitCode=1
k logs crasher --previous     # "starting" 과 "boom" 이 직전 로그에 남아 있음

-w(watch)로 보면 재시작 간격이 점점 벌어지는 백오프를 직접 확인할 수 있습니다. 원인이 exit 1 임을 로그로 확인했다면, 매니페스트의 command를 exit 0으로 고쳐 다시 적용하면 phase가 Succeeded로 정리됩니다.

멀티 컨테이너 Pod의 개별 컨테이너 다루기 #

Pod 안에 컨테이너가 여럿이면 로그와 exec 대상에 -c로 컨테이너를 지정해야 합니다. 멀티 컨테이너 패턴 자체는 #3에서 다루지만, 진단 명령의 형태만 미리 익혀 둡니다.

# 멀티 컨테이너 Pod에서 특정 컨테이너 로그,셸
k logs mypod -c sidecar
k exec -it mypod -c app -- sh

-c를 빼면 첫 번째 컨테이너를 기본으로 잡으며, 컨테이너가 여럿일 때는 어느 컨테이너인지 경고가 나옵니다. 시험에서 “sidecar 컨테이너의 로그를 확인하라” 같은 지시가 나오면 -c 지정이 정답의 일부입니다.

시험 포인트 #

  • phase 5종. Pending(실행 전), Running(실행 중), Succeeded(정상 완료), Failed(비정상 종료), Unknown(노드 통신 두절)
  • restartPolicy 3종과 제약. Always(Deployment 등, 고정), OnFailure,Never(Job은 둘 중 하나, Always 불가)
  • 컨테이너 상태 3종. Waiting,Running,Terminated. 진단은 Waiting,Terminated의 reason 읽기
  • reason 단골. ImagePullBackOff,ErrImagePull(이미지), CrashLoopBackOff(내부 프로세스), OOMKilled(메모리), Completed(정상)
  • 종료 코드. 0(정상), 1(앱 오류), 137(SIGKILL,OOM), 143(SIGTERM,graceful)
  • 진단 순서. get pod -o widedescribe podlogs --previousget events. exec는 마지막
  • CrashLoopBackOff는 에러가 아니라 재시작 백오프 상태. 원인은 logs --previous에 있음
  • 멀티 컨테이너는 -c로 컨테이너 지정해 logs,exec

정리 #

이번 글에서 잡은 것:

  • Pod의 phase(거시 상태)와 컨테이너 상태(미시 상태)를 분리해 진단의 1차 분기를 세웠습니다.
  • restartPolicy 가 워크로드 종류에 따라 가능한 값이 제약된다는 점(Deployment=Always 고정, Job=OnFailure/Never)을 확인했습니다.
  • CrashLoopBackOff,ImagePullBackOff,OOMKilled 같은 reason 과 0,1,137,143 종료 코드로 원인을 빠르게 좁히는 법을 익혔습니다.
  • describelogs --previousevents로 이어지는 진단 명령 순서를 직접 재현한 Pod로 확인했습니다.

이제 상태 진단 방법을 익혔으니, 다음은 한 Pod 안에 컨테이너를 의도적으로 여러 개 두는 설계로 넘어갑니다.

다음: Multi-container 패턴 #

이번 글에서 컨테이너 하나의 상태와 진단을 다뤘다면, 다음은 한 Pod 안에 여러 컨테이너를 함께 두는 패턴입니다.

#3 Multi-container 패턴: Init container, sidecar, ambassador, adapter에서는 본 컨테이너 전에 순차 실행되는 init container, 본 컨테이너 옆에서 보조하는 sidecar, 그리고 ambassador,adapter 패턴을 각각 언제 어떤 형태로 쓰는지 매니페스트와 함께 정리하겠습니다.

X