왜 컨테이너 하나만 띄우면 "Pod"라는 포대까지 필요한가
처음 Kubernetes를 배우는 사람은 거의 다 같은 의문을 갖는다. 컨테이너를 하나 띄우고 싶을 뿐인데, 왜 kind: Pod에 containers: 배열을 넣고 그 안에 컨테이너를 적어야 하나? Docker처럼 그냥 컨테이너를 실행하면 안 되나?
이 질문은 사소해 보이지만 Kubernetes의 가장 근본적인 설계 결정을 건드린다. 왜 Kubernetes는 컨테이너를 직접 다루지 않고 Pod라는 한 겹 더 두른 단위를 배포의 최소 단위로 삼았는가? 이 글의 답은 단순하다 — Pod는 컨테이너가 아니라 "함께 죽고 함께 사는 컨테이너들의 묶음"이며, 이 묶음이 공유하는 것(네트워크·스토리지·생명)이 Kubernetes의 모델을 만든다.
Pod는 컨테이너가 아니라 '파드(콩깍지)'다
먼저 이름부터. Pod는 고래 무리나 콩깍지를 뜻하는 영어다. 컨테이너 하나가 아니라 컨테이너들을 감싸는 껍질을 가리킨다. 이 비유가 핵심이다 — Pod는 그 안의 컨테이너들이 함께 있도록 보장하는 묶음이다.
flowchart TD
POD["Pod (배포의 최소 단위)"] --> C1["컨테이너 1<br/>(예: nginx)"]
POD --> C2["컨테이너 2<br/>(예: 로그 수집 사이드카)"]
POD --> NET["공유 네트워크<br/>(같은 IP, 같은 포트 공간)"]
POD --> VOL["공유 볼륨<br/>(같은 데이터 접근)"]
POD --> LIFE["공유 생명<br/>(같은 노드에, 같이 생성·소멸)"]
왜 굳이 컨테이너 여러 개를 묶어야 할까? 가장 흔한 사례는 사이드카(sidecar) 패턴이다. 메인 컨테이너(nginx)와 그 로그를 수집하는 보조 컨테이너(fluentd)가 항상 같이 있어야 한다면, 둘을 하나의 Pod에 넣는다. 둘은 같은 노드에 배치되고, 같은 네트워크 공간을 공유해 localhost로 통신하며, 같은 볼륨을 마운트해 로그 파일을 주고받는다.
Pod가 공유하는 세 가지 — 왜 이 설계인가
Pod 안의 컨테이너들은 세 가지를 공유한다. 이 공유가 "왜 컨테이너가 아니라 Pod인가"의 답이다.
1. 네트워크 — 같은 IP와 같은 포트 공간
같은 Pod 안의 컨테이너는 같은 IP 주소와 같은 포트 공간을 공유한다. 즉 서로를 localhost로 부를 수 있다. 다만 포트가 겹치면 안 된다 — nginx가 80을 쓰면 같은 Pod의 다른 컨테이너는 80을 못 쓴다.
# Kubernetes 1.36
apiVersion: v1
kind: Pod
metadata:
name: web-logger
spec:
containers:
- name: nginx
image: nginx:1.27
ports: [{containerPort: 80}]
- name: logger
image: busybox:1.36
command: ["sh", "-c", "while true; do wget -qO- localhost:80 || true; sleep 5; done"]
여기서 logger 컨테이너가 localhost:80으로 nginx에 접근한다 — 같은 Pod이므로 가능. 다른 Pod이나 외부에선 이 주소로 못 온다.
2. 볼륨 — 같은 데이터
Pod 수준에서 정의한 volumes를 컨테이너들이 각자 마운트해 같은 데이터를 본다. 로그 수집 사이드카가 메인 컨테이너가 쓴 로그 파일을 읽는 전형적 패턴.
3. 생명 — 같은 노드에, 같이 생성·소멸
Pod 안의 컨테이너들은 반드시 같은 노드에 배치되고 함께 생성·소멸한다. 절대로 둘로 쪼개져 다른 노드에 가지 않는다. 이것이 "묶음"의 본질이다.
왜 이렇게까지 묶었나? 컨테이너를 직접 배포 단위로 삼으면, "nginx와 logger를 항상 같은 노드에"라는 제약을 표현할 방법이 마땅치 않다. Pod라는 묶음이 그 제약을 구조적으로 보장한다. Google Borg 시절엔 Task라 불렀고, Kubernetes에선 Pod로 이름을 바꿨다 — (Borg, Omega, and Kubernetes, ACM Queue 2016).
Pause 컨테이너 — Pod의 보이지 않는 뼈대
Pod를 만들면 실제로 컨테이너가 하나 더 생긴다 — pause 컨테이너. 사용자가 정의한 컨테이너들이 아니라 Kubernetes(정확히는 kubelet)가 몰래 넣는 것이다.
pause의 역할: Pod의 네트워크 네임스페이스를 들고 있는 닻이다. 리눅스 네임스페이스는 "이 네임스페이스를 사용하는 프로세스가 하나라도 살아 있으면 유지된다"는 규칙을 갖는다. pause는 아무 일도 안 하고 영원히 sleep하는 가벼운 프로세스로, 네임스페이스가 사라지지 않게 잡고 있는다.
왜 필요한가? 사용자 컨테이너가 재시작·크래시되는 동안 네임스페이스가 사라지면(다른 컨테이너가 localhost로 연결하려다 실패) 곤란하다. pause가 항상 살아 있어, 메인 컨테이너가 죽었다 살아나도 Pod의 네트워크 정체성(IP, 포트 공간)이 유지된다.
# Kubernetes 1.36, kind 노드 — pause 컨테이너 직접 관찰
docker exec k8s-study-control-plane crictl ps 2>/dev/null | grep -i pause | head
확인할 것: pause 이미지의 컨테이너가 모든 Pod마다 하나씩 돌고 있다.
kind 환경 한정. 비-kind 노드에서도
crictl ps로 동일 관찰 가능. 미검증 환경은 노트.
CONTAINER IMAGE ... NAME
a1b2c3 registry.k8s.io/pause:3.x ... k8s-POD_nginx_default_...
Pod 생명주기 — phase가 말하는 것, 말하지 않는 것
Pod의 상태를 가장 먼저 보게 되는 필드가 phase다. 하지만 phase는 생각보다 적은 정보를 준다 — 이 함정을 모르면 장애를 오진한다.
phase의 가능한 값:
Pending: 생성됐지만 아직 컨테이너가 다 안 돌았다. (스케줄 대기, 이미지 받는 중, 등)Running: 바인딩됐고, 적어도 하나의 컨테이너가 돌고 있거나 시작/재시작 중.Succeeded: 모든 컨테이너가 정상 종료( exit 0). 재시작 안 함.Failed: 모든 컨테이너가 종료됐고 적어도 하나가 실패(비0 종료).Unknown: 보통 노드와 통신이 안 될 때.
kubectl get pod web-logger -o wide
NAME READY STATUS RESTARTS AGE
web-logger 2/2 Running 0 1m
여기서 STATUS 열이 phase고, READY 2/2는 "컨테이너 2개 중 readiness 통과 2개"다.
phase의 함정:
Running이어도 컨테이너가 계속 죽었다 살아나는 상태일 수 있다.CrashLoopBackOff는 phase가Running인데 STATUS로 표시되는 세부 상태다. phase만 보면 "정상"으로 오독한다. 진짜 진단은STATUS열과conditions, 컨테이너의state/lastState를 봐야 한다.
왜 Pod는 재시작 정책을 갖나
Pod에는 restartPolicy(Always/OnFailure/Never)가 있다. 컨테이너가 죽었을 때 kubelet이 다시 띄울지를 결정한다. 기본은 Always(Deployment 등이 만드는 Pod). Never/OnFailure는 주로 Job(07장)에서 쓴다.
재시작은 kubelet이 노드 로컬에서 한다. 컨트롤 플레인을 거치지 않는다 — kubelet이 컨테이너 런타임에 바로 "다시 띄워"라고 한다. 그래서 컨테이너 크래시 복구가 빠르다. 단, 같은 노드에서만 재시작되므로 노드 자체가 죽으면 그 Pod는 못 살리고, Node Controller가 다른 노드로 옮긴다(02장).
init 컨테이너 — 본 컨테이너보다 먼저, 순서대로
Pod에는 본 컨테이너(containers) 외에 init 컨테이너(initContainers)를 둘 수 있다. init 컨테이너는:
- 본 컨테이너보다 먼저 실행되고,
- 정의된 순서대로 하나씩 돌며,
- 각각 성공(종료 0)해야 다음으로 넘어간다.
전부 성공해야 본 컨테이너들이 시작된다. 용도: "본 앱이 뜨기 전에 설정 파일을 내려받아라", "DB 마이그레이션이 끝날 때까지 기다려라" 같은 사전 작업.
# Kubernetes 1.36
spec:
initContainers:
- name: init-config
image: busybox:1.36
command: ["sh", "-c", "until nslookup db; do echo waiting; sleep 2; done"]
containers:
- name: app
image: myapp:1.0
이 예에선 app이 뜨기 전에 db 서비스가 DNS에 풀릴 때까지 기다린다 — DB 준비 전에 app이 뜨는 경쟁 상태를 막는 전형 패턴.
native sidecar (1.29 beta→안정) — init과 본 컨테이너의 한계 극복
일반 사이드카를 containers에 넣으면 골치가 하나 생긴다 — Pod 종료 순서가 보장되지 않는다. 본 컨테이너가 먼저 죽으면 사이드카(예: 로그 수집)가 마지막 로그를 못 보낸 채로 같이 죽는다.
native sidecar(공식 명칭 restartPolicy: Always를 가진 init 컨테이너)가 이 문제를 푼다. 1.29에서 beta로 들어왔고, 이후 안정화됐다. 핵심: init 컨테이너 자리에 두되 restartPolicy: Always로 설정하면, 본 컨테이너보다 먼저 시작하고 본 컨테이너가 끝난 뒤에야 종료되는 사이드카가 된다.
정확한 GA/상태는 실측 시점의 Kubernetes CHANGELOG에서 확인. "1.29 beta" 이후 진행 상황은 문서에 구체 버전과 함께 명시할 것.
Pod는 결코 "재스케줄"되지 않는다 — 이 함정이 중요한 이유
가장 중요하고 가장 많이 오해되는 사실: Pod는 한 번 노드에 배정되면 그 노드를 떠나지 않는다. 노드가 죽으면 Pod는 다른 노드로 옮겨지는 게 아니라 삭제되고 (ReplicaSet이) 새 Pod를 다른 노드에 만든다.
이 차이가 왜 중요한가? Pod의 정체성*은 유지되지 않는다. Pod 이름, IP, 볼륨 바인딩이 달라질 수 있다. "이 Pod를 잠시 다른 노드로 옮겨라"는 동작 자체가 Kubernetes에 없다 — 그냥 죽이고 새로 만든다. 그래서 *상태를 가진 워크로드는 Pod가 아니라 StatefulSet(06장)으로 관리해야 정체성 보장을 받는다.
flowchart LR
P1["Pod A<br/>(노드 1)"] -->|"노드 1 사망"| X[삭제]
X -. 표면적으로 .-> P2["Pod A'<br/>(노드 2)"]
RS["ReplicaSet<br/>(replicas=1)"] -->|"빠진 만큼 새 Pod 생성"| P2
"옮겨진 것처럼" 보이지만 실제론 Pod A는 죽고, ReplicaSet이 새 Pod A'를 만든 것이다. IP도 다르고, 이름도 다르다(ReplicaSet 기반).
직접 확인하기
# Kubernetes 1.36 — 다중 컨테이너 Pod 만들고 localhost 통신 확인
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata: {name: web-logger}
spec:
containers:
- name: nginx
image: nginx:1.27
ports: [{containerPort: 80}]
- name: curl
image: curlimages/curl:8.11
command: ["sh","-c","while true; do curl -s localhost:80 | head -1; sleep 5; done"]
EOF
확인할 것: 같은 Pod의 curl 컨테이너가 localhost:80으로 nginx에 접근해 HTML을 받는다.
kubectl logs web-logger -c curl
<!DOCTYPE html>
<!DOCTYPE html>
...
# Pod 상세 상태 — phase 외에 conditions, containerStates 보기
kubectl describe pod web-logger | grep -A4 -E 'Status:|Conditions:|State:'
확인할 것: phase(Status) 외에 Conditions, 각 컨테이너의 State(Started/Ready/lastState)가 별도 필드로 있다. phase만으론 부족함을 확인.
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
| "Pod = 컨테이너" | Pod는 컨테이너들의 묶음(콩깍지). 보통 1개지만 여럿일 수 있다 |
| "Pod 안 컨테이너들은 서로 다른 IP" | 같은 IP, 같은 포트 공간. 서로 localhost로 통신 |
| "Pod가 죽으면 다른 노드로 옮겨진다" | 옮겨지는 게 아니라 삭제되고 새 Pod가 다른 노드에 생긴다. 정체성 유지 안 됨 |
| "phase=Running이면 정상" | 아니다. CrashLoopBackOff도 phase는 Running일 수 있다. STATUS/conditions를 봐라 |
| "pause 컨테이너는 내가 만든 거다" | kubelet이 자동으로 넣는 네임스페이스 닻. 사용자 정의 아님 |
| "init 컨테이너는 본 컨테이너와 동시에 돈다" | 아니다. 순서대로, 본 컨테이너 전에, 전부 성공해야 본 컨테이너 시작 |
| "사이드카는 Pod 종료 순서를 보장한다" | 일반 containers는 보장 안 함. native sidecar(1.29+)가 해결 |
요약 — 이 글의 결론
- Pod는 컨테이너가 아니라 묶음이다. 함께 죽고 함께 사는 컨테이너들을 감싸는 포대이자 배포의 최소 단위. 이 묶음 덕에 "항상 같이 있어야 하는 컨테이너"를 구조적으로 보장한다.
- 같은 Pod의 컨테이너는 세 가지를 공유 — 네트워크(같은 IP·포트), 볼륨(같은 데이터), 생명(같은 노드, 같이 생성·소멸). 사이드카 패턴의 토대.
- pause 컨테이너가 Pod의 네트워크 네임스페이스를 잡고 있는 닻. 메인 컨테이너가 죽었다 살아나도 Pod 정체성이 유지되는 이유.
- phase는 정보가 적다. Running이어도 크래시 루프일 수 있다. 진짜 진단은 STATUS·conditions·containerState에서.
- Pod는 절대 재스케줄되지 않는다. 노드가 죽으면 Pod는 삭제되고 ReplicaSet이 새 Pod를 만든다. 정체성 유지가 필요하면 StatefulSet(06장)을 써야 한다.
- native sidecar(1.29+)가 일반 사이드카의 종료 순서 함정을 해결한다 — init 자리의
restartPolicy: Always.
생각해 볼 문제
- 같은 Pod의 두 컨테이너가 같은 포트(80)를 원한다면 어떻게 되나? 왜 그렇게 설계됐는가?
- Pod가 노드 1에서 노드 2로 "옮겨갔다"고 표현하는 것이 왜 정확하지 않은가? 정확한 일어나는 일을 서술하라.
- pause 컨테이너가 없다면 어떤 경쟁 상태가 발생할 수 있는가?
- phase=Running인데
READY 0/1인 Pod가 있다. 가능한 원인을 3개 이상 말하라. - init 컨테이너가 실패하면 Pod는 어떤 상태가 되나?
restartPolicy에 따라 어떻게 달라지나? - 상태가 있는 DB를 일반 Pod(Deployment)로 돌리면 안 되는 이유를 "Pod는 재스케줄되지 않는다"는 사실로 설명하라.
참고
- Kubernetes 공식 문서 - Pods - 접근 2026-07-12 (Pod 정의, 공유 자원)
- Kubernetes 공식 문서 - Pod Lifecycle - 접근 2026-07-12 (phase, conditions, container states)
- Kubernetes 공식 문서 - Init Containers - 접근 2026-07-12
- Kubernetes 공식 문서 - Native sidecar containers - 접근 2026-07-12 (1.29+ restartPolicy: Always init)
- KEP-753 Native sidecar containers - 접근 2026-07-12 (설계 의도)
- Borg, Omega, and Kubernetes (ACM Queue 2016) - 접근 2026-07-12 (Pod/Task 단위 설계 사상)
- pause 컨테이너 - kubernetes/pause GitHub - 접근 2026-07-12
'Tech Artifacts > Kubernetes' 카테고리의 다른 글
| Kubernetes - 06. statefulset (0) | 2026.07.13 |
|---|---|
| Kubernetes - 05. deployment (0) | 2026.07.13 |
| Kubernetes - 03. api machinery (0) | 2026.07.13 |
| Kubernetes - 02. architecture (1) | 2026.07.13 |
| Kubernetes - 01. overview (1) | 2026.07.13 |
