콘텐츠로 이동
Study NoteCKA

Pod — 배포의 최소 단위

컨테이너가 아니라 Pod이 단위인 이유

같은 노드에서, 같은 네트워크와 볼륨을 공유하며, 함께 뜨고 함께 죽는 컨테이너 묶음.

  • Kubernetes는 컨테이너를 직접 스케줄링하지 않는다. Pod을 스케줄링한다
  • Pod 하나에 컨테이너 하나가 대부분의 경우 맞다
  • 여러 개를 넣는 건 “떼어놓을 수 없을 때” 뿐이다

Pod은 일회용이다. 재시작되면 이름도 IP도 바뀐다. 그래서 Pod을 직접 만들지 않고 Deployment 같은 컨트롤러에 맡긴다 (워크로드).

Pod 안의 app·sidecar 컨테이너가 pause 컨테이너의 네트워크와 볼륨은 공유하지만 파일시스템은 각자 별개라는 구조

공유한다

네트워크 네임스페이스 — 같은 IP, 같은 포트 공간 → 서로를 localhost로 부른다. 볼륨 — spec.volumes를 각 컨테이너가 마운트. IPC 네임스페이스(기본)와 수명 — 함께 스케줄되고 함께 정리된다.

공유하지 않는다

파일시스템 — 컨테이너마다 별개 (볼륨으로 명시적으로 공유해야 한다). PID 네임스페이스 — 기본은 분리, shareProcessNamespace로 켤 수 있다. 리소스 request/limit은 컨테이너별로 정한다.

네트워크를 공유하니 같은 Pod 안에서 포트가 겹치면 안 된다. 두 컨테이너가 모두 8080을 열 수 없다.

pause 컨테이너 — 보이지 않는 세 번째 컨테이너

섹션 제목: “pause 컨테이너 — 보이지 않는 세 번째 컨테이너”
kubelet 이 pause 컨테이너를 먼저 만들어 Pod IP 를 쥐게 하고, app 컨테이너가 크래시 후 재시작해도 같은 네임스페이스에 합류해 IP 가 유지되는 순서
  • 모든 Pod에는 pause(sandbox) 컨테이너가 하나 더 있다
  • 이것이 네트워크 네임스페이스를 소유하고, 다른 컨테이너들이 거기에 합류한다
  • 그래서 앱 컨테이너가 재시작해도 Pod IP가 유지된다
  • kubectl get pods에는 안 보이고, 노드에서 crictl ps로 보면 보인다
터미널 창
sudo crictl pods # Pod 샌드박스 = pause 컨테이너

이걸 알면 “컨테이너는 죽었는데 Pod IP는 그대로”가 자연스러워진다. Pod IP가 바뀌는 것은 Pod 자체가 새로 만들어질 때다.

pause 자체가 문제로 나오는 일은 드물다 — “컨테이너 재시작 ≠ Pod 재생성”을 구분하고, 노드에서 crictl ps에 앱 컨테이너만 보이는 이유를 이해하는 배경 지식으로 이만큼이면 충분하다.

이 YAML이 시작하기 전에에서 말한 “선언된 상태”(spec) 그 자체다. kubectl apply로 API 서버에 넣는 순간 일이 끝나는 게 아니라 시작된다 — 스케줄러가 노드를 정하고 kubelet이 컨테이너를 띄우며, 실제 상태(status)를 여기 적힌 모습에 맞춰 간다.

apiVersion: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
env:
- name: LOG_LEVEL
value: debug
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
emptyDir: {}
restartPolicy: Always

command와 args — Dockerfile과의 대응

섹션 제목: “command와 args — Dockerfile과의 대응”
containers:
- name: app
image: busybox
command: ["sh", "-c", "echo hello; sleep 3600"]
KubernetesDockerfile생략하면
commandENTRYPOINT이미지의 ENTRYPOINT 사용
argsCMD이미지의 CMD 사용

어느 쪽을 지정했는지에 따라 실제로 실행되는 것이 달라진다. 공식 명령/인자 문서의 표를 요약하면:

commandargs실행되는 것
없음없음이미지 ENTRYPOINT + CMD
없음지정이미지 ENTRYPOINT + args (CMD 무시)
지정없음command만 (ENTRYPOINT·CMD 둘 다 무시)
지정지정command + args (둘 다 무시)
터미널 창
# 명령형으로 만들 때는 -- 뒤에 쓴다 (args로 들어간다)
kubectl run busy --image=busybox --restart=Never -- sleep 3600
kubectl run busy --image=busybox --restart=Never --command -- sleep 3600 # command로

사이드카 (sidecar)

주 컨테이너를 보조한다. 로그 수집기, 서비스 메시 프록시.

앰배서더 (ambassador)

바깥과의 통신을 대신한다. DB 프록시, 커넥션 풀.

어댑터 (adapter)

출력 형식을 변환한다. 앱 로그를 Prometheus 형식으로.

세 이름 중 커리큘럼에 올라 있는 것은 사이드카뿐이다 (아래 “네이티브 사이드카”). 앰배서더·어댑터는 분류용 이름이라 깊게 팔 필요는 없다 — 이름만 알아두자.

함께 넣을지는 세 질문으로 판단한다.

같은 노드·같은 볼륨과 localhost·같은 수명 세 조건을 모두 만족할 때만 한 Pod 에 넣고, 하나라도 아니면 별도 Pod 와 Service 로 나눈다

셋 다 “예”가 아니면 별도 Pod으로 나누고 Service로 연결하는 게 낫다.

initContainers — 앱보다 먼저 끝나야 하는 일

섹션 제목: “initContainers — 앱보다 먼저 끝나야 하는 일”
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nslookup db; do sleep 2; done']
containers:
- name: app
image: myapp:1.0
init 컨테이너가 순서대로 exit 0 으로 끝나야 containers 가 시작되고, 실패하면 restartPolicy 에 따라 재시도한다
  • 순서대로 하나씩 실행되고, 각각 성공(exit 0)해야 다음으로 넘어간다
  • 전부 끝나야 containers가 시작한다
  • 실패하면 restartPolicy에 따라 재시도한다 (Never면 Pod이 실패)
  • 상태 표시가 Init:0/2, Init:CrashLoopBackOff 처럼 나온다

네이티브 사이드카 (Kubernetes v1.33 GA)

섹션 제목: “네이티브 사이드카 (Kubernetes v1.33 GA)”

initContainers에 restartPolicy: Always 를 주면 사이드카가 된다.

spec:
initContainers:
- name: log-shipper
image: fluent-bit:3.0
restartPolicy: Always # ← 이 한 줄이 사이드카로 만든다
volumeMounts:
- name: logs
mountPath: /var/log/app
containers:
- name: app
image: myapp:1.0
네이티브 사이드카는 init 자리에서 먼저 시작해 app 보다 나중에 끝나므로 Job 도 정상적으로 완료된다
  • 일반 init 컨테이너와 달리 끝나지 않고 계속 돈다
  • 앱 컨테이너보다 먼저 시작하고 나중에 종료된다 — 순서가 보장된다
  • Job에서 특히 중요하다 — 예전에는 사이드카가 안 끝나서 Job이 완료되지 않았다

예전 방식(그냥 containers에 두 개)도 여전히 동작한다. 다만 순서 보장이 없다. 네이티브 사이드카는 커리큘럼에 이름이 올라 있는 항목이다 — “initContainers에 넣고 restartPolicy: Always를 준다”는 조합 자체를 기억해 두자. 시험 중 init 컨테이너·사이드카 YAML 뼈대가 필요하면 공식 Init Containers 문서의 예시를 복사해 이름·이미지·command만 바꾸는 게 가장 빠르다.

매니페스트가 “선언된 상태”라면 이 절은 전부 반대쪽, “실제 상태”(status) 이야기다 — 시작하기 전에의 루프에서 차이를 판정할 때 읽는 재료가 여기 모여 있다. 문제가 생기면 결국 이 status를 읽는 것부터 시작한다.

터미널 창
kubectl get pod web -o jsonpath='{.status.phase}'
Pod 이 Pending 에서 Running 으로 가고 Succeeded · Failed 로 끝나거나 통신 두절 시 Unknown 을 오가는 상태 전이
phase의미
Pending아직 스케줄되지 않았거나, 이미지를 받는 중
Running노드에 배정되었고 컨테이너가 최소 하나 이상 살아 있다
Succeeded모든 컨테이너가 성공 종료. 재시작하지 않는다
Failed모든 컨테이너가 종료되었고 하나 이상이 실패
Unknown노드와 통신이 안 되어 상태를 모른다

컨테이너 상태 — 진짜 정보는 여기 있다

섹션 제목: “컨테이너 상태 — 진짜 정보는 여기 있다”
터미널 창
kubectl get pod web -o jsonpath='{.status.containerStatuses[0].state}'
kubectl describe pod web # 사람이 읽기엔 이쪽

컨테이너는 셋 중 하나다: Waiting / Running / Terminated. 각각 reason이 붙는다.

컨테이너 state 가 Waiting · Running · Terminated 중 무엇이냐에 따라 어느 명령으로 원인을 찾는지
reason뜻볼 곳
ContainerCreating볼륨 마운트·네트워크 설정 중Events
ImagePullBackOff / ErrImagePull이미지를 못 받는다Events (이름·태그·인증)
CrashLoopBackOff시작했다가 계속 죽는다logs --previous
CreateContainerConfigErrorConfigMap/Secret이 없다Events
OOMKilled메모리 limit 초과로 커널이 죽였다describe 의 Last State
Error0이 아닌 코드로 종료logs
spec:
restartPolicy: Always # Always(기본) | OnFailure | Never
값재시작 조건쓰는 곳
Always종료 코드와 무관하게 항상Deployment/StatefulSet/DaemonSet의 Pod
OnFailure0이 아닌 코드로 끝났을 때만Job
Never재시작하지 않음일회성 작업, 디버그 Pod
  • Pod 단위 설정이다. 컨테이너별로 다르게 줄 수 없다 (네이티브 사이드카의 restartPolicy: Always만 예외)
  • 재시작은 같은 노드에서 컨테이너만 다시 만드는 것이다. Pod이 옮겨가지 않는다
  • Deployment의 Pod 템플릿에는 Always만 쓸 수 있다

에러 이름이 아니다. “재시작을 지연시키고 있다”는 상태다.

크래시가 반복되면 재시작 간격이 10·20·40초로 두 배씩 늘어 최대 5분에서 멈추고, 10분간 정상이면 카운터가 초기화된다

진단 순서

터미널 창
kubectl describe pod web # Last State / Exit Code / Reason 확인
kubectl logs web --previous # ★ 죽기 직전 로그
kubectl get pod web -o yaml # 정확한 exitCode
Exit Code대개의 원인
0정상 종료인데 restartPolicy: Always — 명령이 바로 끝난다
1 / 2애플리케이션 에러. 로그를 보라
137SIGKILL — OOMKilled 이거나 강제 종료
143SIGTERM — 정상적인 종료 신호를 받았다

프로브(probe) — kubelet이 던지는 질문

섹션 제목: “프로브(probe) — kubelet이 던지는 질문”

프로브가 없으면 kubelet은 프로세스가 살아 있는지만 안다 — 행에 걸려 응답을 못 해도 재시작되지 않고, 준비가 안 된 컨테이너에도 트래픽이 간다. “제대로 동작하는가”는 프로브로 kubelet에게 알려줘야 한다.

가장 중요한 구분: liveness는 죽이고, readiness는 트래픽만 끊는다.

kubelet 이 돌리는 세 프로브 — liveness 실패는 재시작, readiness 실패는 엔드포인트 제외, startup 은 성공할 때까지 나머지 둘을 멈춘다
livenessProbe:
httpGet: # 2xx~3xx면 성공
path: /healthz
port: 8080
httpHeaders:
- name: Custom-Header
value: check
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 1
failureThreshold: 3
successThreshold: 1
readinessProbe:
exec: # 종료 코드 0이면 성공
command: ["cat", "/tmp/ready"]
startupProbe:
tcpSocket: # 연결이 되면 성공
port: 8080
failureThreshold: 30
periodSeconds: 10 # 최대 300초까지 기다린다

grpc: 방식도 있다 (port + service). gRPC 헬스체크 규약을 쓰는 앱용. 시험에 주로 나오는 건 httpGet과 exec다 — grpc:는 이런 게 있다는 것만 알아두면 된다.

필드기본값의미
initialDelaySeconds0컨테이너 시작 후 첫 검사까지 대기
periodSeconds10검사 주기
timeoutSeconds1응답 대기 시간
failureThreshold3몇 번 연속 실패해야 실패로 볼 것인가
successThreshold1몇 번 연속 성공해야 복귀할 것인가 (liveness는 1 고정)
initialDelaySeconds 뒤 첫 검사가 돌고 periodSeconds 마다 반복해 failureThreshold 에 도달하면 실패로 판정한다

실제 반응 시간 = initialDelaySeconds + periodSeconds × failureThreshold

기본값이면 컨테이너가 죽고 나서 최대 30초 뒤에 재시작이 걸린다.

종료 흐름 — Pod이 죽을 때 벌어지는 일

섹션 제목: “종료 흐름 — Pod이 죽을 때 벌어지는 일”
Pod 삭제 시 엔드포인트 제거와 preStop 훅이 돌고 SIGTERM 뒤 유예 시간을 기다렸다가 초과하면 SIGKILL 로 강제 종료된다

1번과 2번은 동시에 시작한다. 그래서 엔드포인트 제거(Service의 대상 목록에서 빼는 것 — Service)가 전파되기 전에 SIGTERM이 도착할 수 있다 — preStop에 sleep 5 를 넣는 관행이 여기서 나온다.

lifecycle 훅시험 비중 낮음

섹션 제목: “lifecycle 훅”
containers:
- name: app
image: myapp:1.0
lifecycle:
postStart:
exec:
command: ["sh", "-c", "echo started > /tmp/status"]
preStop:
exec:
command: ["sh", "-c", "sleep 5; nginx -s quit"]
spec:
terminationGracePeriodSeconds: 60
  • postStart — 컨테이너 시작과 동시에 실행된다 (ENTRYPOINT보다 먼저라는 보장은 없다)
  • preStop — SIGTERM 전에 실행된다. 여기서 시간을 쓰면 grace period에서 차감된다
  • 훅이 실패하면 컨테이너가 죽는다

훅 자체를 깊게 묻는 일은 드물다 — 위 종료 흐름에서 preStop에 sleep을 넣는 관행이 왜 생겼는지 이해하는 정도면 충분하다.

환경변수 — 값을 넣는 여러 경로

섹션 제목: “환경변수 — 값을 넣는 여러 경로”
env:
- name: LOG_LEVEL # 직접
value: "debug"
- name: MY_NODE # Downward API — Pod 자신의 정보
valueFrom:
fieldRef:
fieldPath: spec.nodeName # status.podIP, metadata.name 등도 가능
- name: CPU_LIMIT # 자기 리소스 값
valueFrom:
resourceFieldRef:
containerName: app
resource: limits.cpu
- name: DB_PASSWORD # Secret에서 (설정과 리소스)
valueFrom:
secretKeyRef:
name: db-secret
key: password

fieldRef·resourceFieldRef로 값을 끌어오는 통로가 Downward API다 — Pod이 자기 자신의 정보(이름·노드·IP·리소스 값)를 API 서버에 물어보지 않고 환경변수나 파일로 받는 방법이다. 앱이 “내가 어느 노드에 떠 있나”를 로그에 남길 때 같은 데 쓴다. 깊게 팔 필요는 없다 — 이름과 “자기 정보를 받는 통로”라는 용도만 알아두자.

envFrom: 을 쓰면 ConfigMap/Secret의 모든 키를 한 번에 환경변수로 넣을 수 있다 (설정과 리소스).

spec:
securityContext: # Pod 수준 — 모든 컨테이너에 적용
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000 # 볼륨의 그룹 소유권
containers:
- name: app
image: myapp:1.0
securityContext: # 컨테이너 수준 — Pod 설정을 덮어쓴다
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
add: ["NET_ADMIN"]
drop: ["ALL"]
Pod 와 컨테이너의 securityContext 가 겹치면 컨테이너 쪽이 이기고, fsGroup 은 Pod 수준에만 capabilities 는 컨테이너 수준에만 있다
  • 컨테이너 수준이 Pod 수준을 이긴다
  • capabilities는 컨테이너 수준에만 있다
  • fsGroup은 Pod 수준에만 있다

스태틱 Pod은 kubelet이 API 서버 없이, 디렉터리에 놓인 매니페스트 파일만 보고 직접 만드는 Pod이다. 컨트롤 플레인 컴포넌트 자신이 이 방식으로 뜬다 (아키텍처). API 서버에 보이는 것은 kubelet이 보고용으로 올린 미러(mirror) Pod라서, API 쪽에서 지워도 kubelet이 파일을 보고 다시 만든다.

  1. 어느 디렉터리를 보는지 확인

    터미널 창
    sudo grep staticPodPath /var/lib/kubelet/config.yaml
  2. 그 디렉터리에 YAML을 놓는다 — 그게 전부다

    터미널 창
    sudo kubectl run web --image=nginx --dry-run=client -o yaml \
    | sudo tee /etc/kubernetes/manifests/web.yaml
  3. 잠시 후 확인 — 이름 뒤에 노드 이름이 붙는다

    터미널 창
    kubectl get pods
    # web-node01 1/1 Running
  • 삭제하려면 파일을 지운다. kubectl delete pod는 소용없다
  • kubelet이 디렉터리를 계속 지켜보므로 재시작도 필요 없다
  • 다른 노드에 만들라고 하면 그 노드에 ssh로 들어가서 만들어야 한다

리소스 in-place 변경 (Kubernetes v1.35 GA)

섹션 제목: “리소스 in-place 변경 (Kubernetes v1.35 GA)”

resources는 원래 만들고 나면 못 바꾸는 필드였다 — 조정하려면 Pod을 지우고 다시 만드는 수밖에 없었고, 그때마다 재시작이 따라왔다. 이제 Pod을 다시 만들지 않고 CPU·메모리를 바꿀 수 있다.

containers:
- name: app
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired # 재시작 없이 적용 (기본)
- resourceName: memory
restartPolicy: RestartContainer # 컨테이너를 다시 시작해서 적용
터미널 창
kubectl patch pod web --subresource=resize \
-p '{"spec":{"containers":[{"name":"app","resources":{"requests":{"cpu":"200m"}}}]}}'
  • CPU와 메모리만 가능하다. init/ephemeral 컨테이너는 안 된다
  • QoS 클래스는 바뀔 수 없다 (설정과 리소스)
  • status.containerStatuses[*].resources 에 실제 적용된 값이 보인다

v1.35에서 GA가 되며 커리큘럼에 올라온 항목이다 — kubectl patch에 --subresource=resize를 붙이는 것이 핵심이다.

  • Pod = 네트워크·볼륨·수명을 공유하는 컨테이너 묶음. 스케줄링 단위다
  • pause 컨테이너가 네트워크 네임스페이스를 쥐고 있어 컨테이너가 죽어도 IP가 유지된다
  • initContainers는 순서대로, 성공해야 다음. restartPolicy: Always를 주면 사이드카(v1.33 GA)
  • STATUS 열은 phase가 아니다 — 컨테이너 상태 + reason이 진짜 정보
  • liveness는 죽이고, readiness는 트래픽만 끊는다. 느린 앱엔 startupProbe
  • 종료는 엔드포인트 제거 + preStop → SIGTERM → grace → SIGKILL
  • CrashLoopBackOff는 에러가 아니라 지연 중이라는 뜻 → logs --previous
  • 스태틱 Pod은 파일을 놓으면 생기고, 지우면 사라진다