워크로드 — 파드부터 Deployment까지
파드를 직접 만드는 일은 실무에서 거의 없다. 왜 그 위에 컨트롤러가 겹겹이 쌓여 있는지를 따라가면 각각의 존재 이유가 보인다.
1. 파드 — 배포의 최소 단위
파드는 '컨테이너 하나' 가 아니라 '함께 사는 컨테이너 묶음' 이다
같은 파드 안 컨테이너들은
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: app
image: myapp:1.4.2
ports: [{ containerPort: 8080 }]
왜 파드라는 껍데기가 필요한가
컨테이너를 직접 스케줄하면 '반드시 붙어 있어야 하는 둘' 을 표현할 수 없다
이 관계를 표현하려면 '함께 배치되는 단위' 가 필요하다. 그것이 파드다
파드는 일회용이다
파드는 고쳐지지 않는다. 문제가 생기면 지우고 새로 만든다
"파드를 고친다" 는 개념이 없다는 것이 설계의 출발점이다
2. 초기화 컨테이너와 사이드카
spec:
initContainers:
- name: wait-db
image: busybox:1.36
command: ["sh", "-c", "until nc -z db 5432; do sleep 2; done"]
containers:
- name: app
image: myapp:1.4.2
-
initContainer — 앱 컨테이너보다 '먼저' 순서대로 실행되고, 끝나야 다음이 뜬다
- 준비 작업 — 마이그레이션 대기 · 설정 렌더링 · 권한 조정
- 실패하면 재시작되며 앱은 시작되지 않는다
-
사이드카 — 앱과 '함께' 도는 보조 컨테이너
- restartPolicy: Always 를 가진 initContainer 로 선언하면
- 앱보다 먼저 시작되고 나중에 종료되는 사이드카가 된다
- 로그 수집기가 앱보다 먼저 죽어 마지막 로그를 놓치는 문제가 해결된다
3. 컨트롤러 계층
-
ReplicaSet — "라벨이 맞는 파드가 N개 있어야 한다" 만 책임진다
-
Deployment — ReplicaSet 을 여러 세대 관리해 '전환' 과 '롤백' 을 책임진다
-
직접 ReplicaSet 을 만들지 않는다 — Deployment 가 대신 만든다
롤링 업데이트가 실제로 하는 일
이미지를 바꾸면
- ① 새 ReplicaSet 을 만든다 (replicas: 0 에서 시작)
- ② 새 것을 늘리고 옛 것을 줄인다 — maxSurge · maxUnavailable 규칙대로
- ③ 다 넘어가면 옛 ReplicaSet 은 replicas: 0 으로 남는다 (롤백용)
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25% # 원하는 수보다 얼마나 더 띄울 수 있나
maxUnavailable: 25% # 얼마나 부족해도 되나
revisionHistoryLimit: 10 # 남겨 둘 옛 ReplicaSet 수
maxUnavailable: 0 + maxSurge: 1
- 항상 원하는 수를 유지하며 하나씩 교체한다 (가장 안전, 가장 느리다)
maxSurge: 0 + maxUnavailable: 1
- 자원을 더 쓰지 않지만 잠시 용량이 준다
자원 여유가 있으면 앞쪽을, 빡빡하면 뒤쪽을 고른다
kubectl rollout status deploy/web
kubectl rollout history deploy/web
kubectl rollout undo deploy/web --to-revision=3
kubectl rollout restart deploy/web # 이미지 그대로 파드만 교체
rollout restart는 실무에서 자주 쓴다. 설정(ConfigMap)이 바뀌었을 때 파드를 새로 띄워 다시 읽게 하는 표준 방법이다.
롤아웃이 멈추는 경우
새 파드가 Ready 가 안 되면 롤아웃은 거기서 멈춘다
- 옛 파드는 살아 있으므로 서비스는 계속된다 (좋은 설계다)
- progressDeadlineSeconds(기본 600초)를 넘기면 실패로 표시된다
kubectl rollout status 가 안 끝나면
- kubectl get pods 로 새 파드의 상태를 본다
- Pending · ImagePullBackOff · CrashLoopBackOff · Ready 안 됨
4. 다른 워크로드 종류
| 종류 | 언제 쓰나 | 특징 |
|---|---|---|
| Deployment | 상태 없는 애플리케이션 | 파드가 서로 대체 가능하다 |
| StatefulSet | 상태 있는 애플리케이션 | 안정된 이름·순서·전용 볼륨 |
| DaemonSet | 노드마다 하나씩 | 로그 수집·모니터링·CNI |
| Job | 한 번 끝나는 작업 | 완료되면 끝 |
| CronJob | 주기적 작업 | Job 을 일정대로 만든다 |
StatefulSet 이 Deployment 와 다른 점
-
이름 — web-0 · web-1 · web-2 — 순서가 있고 재생성돼도 같은 이름
-
순서 — 0 → 1 → 2 순으로 만들고, 역순으로 지운다
-
볼륨 — volumeClaimTemplates 로 파드마다 전용 PVC 를 만든다
- 파드가 재생성돼도 같은 볼륨에 다시 붙는다
-
네트워크 — 헤드리스 서비스와 함께 쓰면 web-0.svc 로 개별 접근이 가능하다
-
클러스터 멤버가 서로를 '개체' 로 인식해야 하는 것들에 쓴다
- (DB 복제 세트 · Kafka · ZooKeeper · Elasticsearch)
주의 — StatefulSet 을 지워도 PVC 는 남는다 (의도된 동작이다)
- 데이터를 지키기 위해서이고, 그래서 정리를 잊으면 볼륨이 쌓인다
Job 과 CronJob
apiVersion: batch/v1
kind: Job
spec:
completions: 5 # 총 5번 성공해야 완료
parallelism: 2 # 동시에 2개씩
backoffLimit: 3 # 실패 재시도 횟수
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: OnFailure # Job 은 Always 를 못 쓴다
containers: [...]
CronJob 의 함정
- concurrencyPolicy 기본이 Allow — 이전 실행이 안 끝났는데 또 시작한다
- Forbid(건너뛴다) · Replace(교체) 중 하나를 명시하는 편이 안전하다
- startingDeadlineSeconds 를 안 주면 컨트롤러 정지 후 밀린 작업이 한꺼번에 돈다
- 시간대는 클러스터 기준이다 (timeZone 필드로 지정 가능)
- 완료된 Job 이 쌓인다 → successfulJobsHistoryLimit 로 제한한다
5. 프로브 — 살아 있는가 vs 받을 수 있는가
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 5
startupProbe:
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30
periodSeconds: 10 # 최대 300초까지 기동을 기다린다
- liveness — 실패하면 '컨테이너를 재시작' 한다
- 재시작으로 나아질 문제만 넣는다 (데드락 · 무한 루프)
- readiness — 실패하면 '엔드포인트에서 뺀다' (재시작하지 않는다)
- 의존 자원 확인은 여기에
- startup — 통과할 때까지 liveness·readiness 를 유예한다
- 기동이 느린 애플리케이션에 필수
가장 흔한 사고 두 가지
-
① liveness 에 DB 확인을 넣었다
- DB 장애 → 모든 파드가 재시작 → 재시작해도 DB 는 여전히 죽어 있다
- CrashLoopBackOff 무한 루프. 상황만 악화된다
-
② startupProbe 없이 initialDelaySeconds 로 버틴다
- 기동 시간이 들쭉날쭉하면 어떤 파드는 기동 중에 죽는다
- startupProbe 로 '기동 완료' 를 명시적으로 다룬다
liveness 는 자기 자신만, readiness 는 필수 의존까지. 이 한 줄이 가용성 설계의 핵심이다. 공통 의존을 liveness 에 넣으면 전 인스턴스가 동시에 죽는다.
6. 종료 절차
파드를 지우면
- ① 엔드포인트에서 제거된다 (새 트래픽이 안 온다)
- ② preStop 훅이 실행된다
- ③ 컨테이너에 SIGTERM
- ④ terminationGracePeriodSeconds(기본 30초) 대기
- ⑤ 남아 있으면 SIGKILL
주의 — ①과 ③은 '동시에' 시작된다. 순서가 보장되지 않는다
- 엔드포인트 전파(kube-proxy 규칙 갱신)에 시간이 걸리기 때문에
- SIGTERM 을 받은 뒤에도 잠시 트래픽이 올 수 있다
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"] # 전파를 기다린다
preStop 에 짧은 sleep 을 두는 것이 관용적 해법이다
- 그 사이 엔드포인트 제거가 전파된다
- 그다음 SIGTERM 이 가서 애플리케이션이 안전하게 종료한다
grace period 는 '최장 요청 처리 시간 + preStop sleep + 여유' 로 잡는다
한눈에 정리
- 파드 — 함께 사는 컨테이너 묶음. localhost 공유. 일회용이다
- init/사이드카 — 준비 작업은 initContainer, 보조 프로세스는 사이드카
- Deployment — ReplicaSet 을 세대 관리해 롤링 업데이트·롤백을 한다
- 롤링 — maxSurge/maxUnavailable. 새 파드가 Ready 안 되면 멈춘다(안전)
- StatefulSet — 안정된 이름·순서·전용 볼륨. 지워도 PVC 는 남는다
- Job/CronJob — concurrencyPolicy 기본 Allow 주의. restartPolicy 는 OnFailure
- 프로브 — liveness=재시작(자기 문제만) · readiness=트래픽 제외
- startup 으로 기동을 유예한다
- 종료 — 엔드포인트 제거와 SIGTERM 이 동시다 → preStop sleep 으로 메운다
출처 — Kubernetes Docs: "Pods" · "Deployments" · "StatefulSets" · "Jobs" · "Sidecar Containers" · "Configure Liveness, Readiness and Startup Probes" · "Pod Lifecycle" · CNCF CKA Curriculum v1.35 (Workloads and Scheduling 15%)