K8s 시리즈 02: Pod, Deployment, Job, CronJob — K8s 워크로드 총정리
이 글은 K8s 시리즈의 두 번째 글이다.
- 01: Kubernetes란? 컨테이너부터 클러스터까지
- 02: Pod, Deployment, Job, CronJob — K8s 워크로드 총정리 ← 현재 글
- 03: Service, Ingress — 트래픽 라우팅과 외부 접근
- 04: ConfigMap, Secret, Storage — 설정과 데이터 관리
- 05: Amazon EKS — 아키텍처와 Worker Node
- 06: EKS 네트워킹·보안·비용·운영
이전 글에서 Kubernetes의 큰 그림(클러스터 구조, 컨테이너 이미지, YAML 기본)을 살펴봤다. 이번 글에서는 실제로 앱을 실행하는 핵심 리소스 — Pod, Deployment, Job, CronJob — 을 다룬다.
1. 워크로드 종류 한눈에 보기
Kubernetes에서 “앱을 실행하는 방법”은 하나가 아니다. 용도에 따라 다른 워크로드 리소스를 사용한다.
| 리소스 | 용도 | 예시 |
|---|---|---|
| Deployment | 상시 실행되는 서비스 | vLLM 서빙 서버, FastAPI 백엔드, Annotation 툴 |
| StatefulSet | 순서와 고유성이 필요한 상태 유지 서비스 | PostgreSQL, Redis, Elasticsearch |
| DaemonSet | 모든 노드에 1개씩 실행 | 로그 수집기 (Fluent Bit), 모니터링 에이전트 |
| Job | 일회성 작업. 완료되면 종료 | 벤치마크 실행, 데이터 마이그레이션, 모델 평가 |
| CronJob | 주기적 반복 작업 | 매일 데이터 전처리, 주간 리포트, 정기 백업 |
이 글에서는 가장 많이 쓰는 Deployment(+ Pod)와 배치 작업인 Job/CronJob을 중점적으로 다루고, StatefulSet/DaemonSet은 간단히 소개한다.
2. Pod — 최소 실행 단위
2.1 Pod이란?
Pod은 Kubernetes에서 컨테이너가 실행되는 최소 단위다. 하나의 Pod에 하나 이상의 컨테이너가 포함된다. 대부분의 경우 1 Pod = 1 컨테이너로 사용한다.
| 특징 | 설명 |
|---|---|
| IP 할당 | 각 Pod은 고유한 IP를 가짐. 단, 재시작 시 IP가 변경됨 |
| 수명 | 일시적(Ephemeral). 언제든 삭제되고 재생성될 수 있음 |
| 단독 생성 | 직접 Pod을 만드는 것은 비권장. Deployment를 통해 관리 |
| 같은 Pod 내 컨테이너 | 네트워크(localhost)와 스토리지를 공유 |
2.2 Pod 생명주기
| 상태 | 의미 | 자주 보는 상황 |
|---|---|---|
| Pending | 노드 배정 대기 또는 이미지 Pull 중 | 리소스 부족, 이미지 다운로드 중 |
| Running | 컨테이너가 정상 실행 중 | 정상 상태 |
| Succeeded | 모든 컨테이너가 정상 종료 | Job 완료 |
| Failed | 컨테이너가 비정상 종료 | 코드 에러, OOM(메모리 초과) |
| Unknown | 노드와 통신 불가 | 노드 장애 |
Pod이 Pending에 빠지는 흔한 원인
LLM 엔지니어가 가장 자주 마주치는 문제다.
| 원인 | 증상 | 해결 |
|---|---|---|
| 리소스 부족 | CPU/Memory/GPU 요청이 노드 가용량 초과 | 노드 추가 또는 requests 줄이기 |
| 이미지 Pull 실패 | ImagePullBackOff | 이미지 이름·태그 확인, Registry 인증 확인 |
| 노드 Taint | GPU 노드에 Toleration 미설정 | Pod에 Toleration 추가 |
| PVC 바인딩 실패 | 스토리지 프로비저닝 대기 | StorageClass 확인 |
kubectl describe pod <pod-name>으로 Events 섹션을 확인하면 원인을 알 수 있다.
2.3 Pod YAML 예시
apiVersion: v1
kind: Pod
metadata:
name: vllm-server
labels:
app: vllm
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
ports:
- containerPort: 8000
resources:
requests:
cpu: "4"
memory: "16Gi"
nvidia.com/gpu: "1"
limits:
cpu: "8"
memory: "32Gi"
nvidia.com/gpu: "1"
실무에서는 Pod을 직접 만들지 않는다. Pod이 죽으면 아무도 되살려주지 않기 때문이다. 대신 Deployment를 통해 Pod을 관리하면, 죽어도 자동으로 재생성된다.
3. Resource Requests와 Limits
Kubernetes에서 Pod이 사용할 CPU, Memory, GPU를 명시적으로 지정할 수 있다. 이 설정이 잘못되면 Pod이 Pending에 빠지거나 OOM(Out of Memory)으로 죽는다.
3.1 Requests vs Limits
resources:
requests: # "최소한 이만큼은 필요하다" — 스케줄링 기준
cpu: "2"
memory: "8Gi"
limits: # "최대 이만큼까지 허용한다" — 초과 시 제한/종료
cpu: "4"
memory: "16Gi"
| 항목 | Requests | Limits |
|---|---|---|
| 역할 | Scheduler가 노드를 고를 때 기준 | 실행 중 리소스 사용 상한 |
| CPU 초과 시 | - | 쓰로틀링 (느려짐, 죽지는 않음) |
| Memory 초과 시 | - | OOMKilled (Pod 강제 종료) |
| 미설정 시 | 어디든 배치 (위험) | 무제한 사용 (위험) |
3.2 GPU 할당
LLM 학습·추론에서 핵심인 GPU 할당이다.
resources:
requests:
nvidia.com/gpu: "1" # GPU 1장 요청
limits:
nvidia.com/gpu: "1" # GPU는 requests = limits 필수
GPU 할당 규칙:
- GPU는 정수 단위로만 요청 가능 (0.5 GPU 같은 분할 불가, time-slicing 별도 설정 필요)
-
requests와limits를 동일하게 설정해야 한다 - GPU가 없는 노드에는 배치되지 않음 → GPU 노드에 배치하려면 별도 설정 불필요
- 여러 GPU가 필요하면:
nvidia.com/gpu: "4"
3.3 실무 가이드라인
| 워크로드 | requests 설정 | limits 설정 |
|---|---|---|
| 추론 서버 (vLLM) | 실제 사용량 기준 | requests의 1.5~2배 |
| 학습 Job | GPU 수 고정 | requests와 동일 |
| 경량 API 서버 | 낮게 (0.25 CPU, 256Mi) | 적절한 상한 (1 CPU, 1Gi) |
| 데이터 전처리 CronJob | 작업량에 따라 | 메모리 여유 확보 (OOM 방지) |
Memory limits는 반드시 설정하자. 미설정 시 하나의 Pod이 노드 전체 메모리를 소비해서 다른 Pod까지 죽일 수 있다.
4. Health Check — Probe
컨테이너가 Running 상태여도 앱이 실제로 정상인지는 다른 문제다. 모델 로딩에 5분이 걸리는 추론 서버가 시작 직후 트래픽을 받으면 에러가 난다. Probe는 이를 해결한다.
4.1 Probe 종류
| Probe | 역할 | 실패 시 동작 |
|---|---|---|
| livenessProbe | “앱이 살아있는가?” | Pod 재시작 |
| readinessProbe | “트래픽을 받을 준비가 되었는가?” | Service에서 제외 (트래픽 안 보냄) |
| startupProbe | “앱이 시작 완료되었는가?” | 완료될 때까지 liveness/readiness 비활성화 |
4.2 LLM 추론 서버 예시
모델 로딩에 시간이 걸리는 추론 서버에 적합한 설정이다.
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
ports:
- containerPort: 8000
# 시작 확인: 모델 로딩 완료까지 최대 10분 대기
startupProbe:
httpGet:
path: /health
port: 8000
failureThreshold: 60 # 60번 실패 허용
periodSeconds: 10 # 10초마다 확인 → 최대 600초(10분)
# 생존 확인: 앱이 멈추면 재시작
livenessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 30
failureThreshold: 3
# 준비 확인: 요청 처리 가능할 때만 트래픽 전달
readinessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 10
failureThreshold: 3
startupProbe가 없으면: 모델 로딩 중 livenessProbe가 실패 → Pod 재시작 → 또 로딩 → 무한 재시작 루프. 큰 LLM 모델을 서빙할 때 흔히 발생하는 문제이므로 반드시 설정하자.
4.3 Probe 방식
| 방식 | 설명 | 적합한 경우 |
|---|---|---|
httpGet | HTTP 엔드포인트 호출, 200~399면 성공 | 웹 서버, API |
tcpSocket | TCP 포트 연결 확인 | DB, Redis |
exec | 컨테이너 내 명령어 실행, exit 0이면 성공 | 커스텀 확인 로직 |
5. Deployment — 상시 서비스 배포
5.1 Deployment란?
Deployment는 Pod의 개수 유지, 롤링 업데이트, 롤백을 관리하는 컨트롤러다. 프로덕션 서비스 배포에 가장 많이 사용한다.
| 계층 | 역할 |
|---|---|
| Deployment | 전체 배포 전략 관리 (이미지 버전, 롤백, replicas) |
| ReplicaSet | 지정된 수의 Pod 복제본 유지. 직접 사용하지 않음 |
| Pod | 실제 컨테이너가 실행되는 최소 단위 |
5.2 Deployment YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-api
labels:
app: backend
spec:
replicas: 3 # Pod 3개 유지
selector:
matchLabels:
app: backend # 이 Label을 가진 Pod을 관리
template: # Pod 템플릿
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/backend:v2.1.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m" # 0.5 CPU
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
readinessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 10
핵심 포인트
-
replicas: 3: Pod 3개를 항상 유지. 하나가 죽으면 자동으로 새 Pod 생성 -
selector.matchLabels: Deployment가 관리할 Pod을 Label로 식별 -
template: 생성할 Pod의 스펙. 여기서 정의한 대로 Pod이 만들어짐 - 이미지 태그:
latest대신 버전 태그 사용 권장 (v2.1.0,abc1234)
5.3 롤링 업데이트
이미지 버전을 변경하면 Deployment가 무중단으로 Pod을 교체한다.
- 새 버전(v2) Pod을 하나 생성
- v2 Pod이 Ready가 되면 기존 v1 Pod 하나 제거
- 이 과정을 반복하여 모든 Pod이 v2로 교체
# 이미지 업데이트 (롤링 업데이트 시작)
kubectl set image deployment/backend-api backend=backend:v2.2.0
# 업데이트 상태 확인
kubectl rollout status deployment/backend-api
# 업데이트 이력 확인
kubectl rollout history deployment/backend-api
업데이트 전략 설정
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 원하는 수 이상으로 최대 1개 추가 가능
maxUnavailable: 0 # 항상 replicas 수만큼 가동 (무중단 보장)
| 설정 | 의미 | 권장 |
|---|---|---|
maxSurge: 1, maxUnavailable: 0 | 항상 기존 수만큼 가동, 1개씩 교체 | 무중단 필수 서비스 |
maxSurge: 0, maxUnavailable: 1 | 총 Pod 수 유지, 1개씩 교체 | 리소스 절약 |
maxSurge: 25%, maxUnavailable: 25% | 25%씩 교체 (기본값) | 일반적인 경우 |
5.4 롤백
배포 후 문제가 발생하면 즉시 이전 버전으로 되돌릴 수 있다.
# 바로 이전 버전으로 롤백
kubectl rollout undo deployment/backend-api
# 특정 리비전으로 롤백
kubectl rollout undo deployment/backend-api --to-revision=3
6. Job — 일회성 작업
6.1 Job이란?
Job은 한 번 실행하고 완료되면 종료되는 워크로드다. Deployment와 달리 Pod을 항상 유지하지 않고, 작업이 끝나면 Pod이 Succeeded 상태가 된다.
| 용도 | 예시 |
|---|---|
| 모델 평가 | 벤치마크 데이터셋으로 모델 성능 측정 |
| 데이터 마이그레이션 | DB 스키마 변경, 데이터 이전 |
| 일회성 처리 | 대량 데이터 변환, 임베딩 생성 |
6.2 Job YAML
apiVersion: batch/v1
kind: Job
metadata:
name: benchmark-llm
spec:
template:
spec:
containers:
- name: benchmark
image: my-benchmark:v1.0
command: ["python", "run_benchmark.py", "--model", "llama-3-70b"]
resources:
requests:
nvidia.com/gpu: "1"
limits:
nvidia.com/gpu: "1"
restartPolicy: Never # 실패해도 재시작하지 않음
backoffLimit: 3 # 최대 3번 재시도
ttlSecondsAfterFinished: 3600 # 완료 후 1시간 뒤 자동 정리
핵심 설정
| 필드 | 설명 |
|---|---|
restartPolicy | Never (재시작 안 함) 또는 OnFailure (실패 시 재시작) |
backoffLimit | 실패 시 최대 재시도 횟수 (기본: 6) |
ttlSecondsAfterFinished | 완료 후 자동 삭제까지 시간 (미설정 시 영구 보존) |
activeDeadlineSeconds | 전체 실행 시간 제한 (초과 시 강제 종료) |
6.3 병렬 실행
여러 모델을 동시에 평가하거나, 데이터를 분할 처리할 때 유용하다.
spec:
completions: 5 # 총 5개 작업을 완료해야 함
parallelism: 3 # 동시에 3개까지 병렬 실행
| 패턴 | completions | parallelism | 동작 |
|---|---|---|---|
| 단일 실행 | 1 (기본) | 1 | Pod 1개 실행, 완료되면 끝 |
| 고정 완료 수 | N | M | 총 N개 Pod을 M개씩 병렬 실행 |
| 작업 큐 | 미설정 | M | 각 Pod이 큐에서 작업을 가져감 |
7. CronJob — 주기적 배치 작업
7.1 CronJob이란?
CronJob은 정해진 스케줄에 따라 Job을 반복 생성한다. Linux의 crontab과 같은 개념이다.
| 용도 | 예시 |
|---|---|
| 데이터 전처리 | 매일 밤 새로운 데이터를 정제·변환 |
| 정기 백업 | 매일 DB 스냅샷 생성 |
| 주간 리포트 | 매주 모델 성능 리포트 생성 |
| 캐시 정리 | 매시간 오래된 캐시 삭제 |
7.2 CronJob YAML
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-data-pipeline
spec:
schedule: "0 2 * * *" # 매일 새벽 2시
concurrencyPolicy: Forbid # 이전 작업이 끝나야 새 작업 시작
successfulJobsHistoryLimit: 3 # 성공 이력 3개 보존
failedJobsHistoryLimit: 3 # 실패 이력 3개 보존
jobTemplate:
spec:
template:
spec:
containers:
- name: pipeline
image: data-pipeline:v1.0
command: ["python", "run_pipeline.py"]
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "8Gi"
restartPolicy: OnFailure
backoffLimit: 2
7.3 Cron 스케줄 문법
┌───────── 분 (0-59)
│ ┌───────── 시 (0-23)
│ │ ┌───────── 일 (1-31)
│ │ │ ┌───────── 월 (1-12)
│ │ │ │ ┌───────── 요일 (0-6, 0=일요일)
│ │ │ │ │
* * * * *
| 스케줄 | 의미 |
|---|---|
0 2 * * * | 매일 새벽 2시 |
*/30 * * * * | 30분마다 |
0 9 * * 1-5 | 평일 오전 9시 |
0 0 1 * * | 매월 1일 자정 |
0 */6 * * * | 6시간마다 |
7.4 동시 실행 정책
| concurrencyPolicy | 동작 | 적합한 경우 |
|---|---|---|
| Allow (기본) | 이전 Job이 실행 중이어도 새 Job 생성 | 독립적인 작업 |
| Forbid | 이전 Job이 실행 중이면 새 Job 스킵 | 데이터 파이프라인 (권장) |
| Replace | 이전 Job을 취소하고 새 Job 실행 | 최신 실행만 의미 있는 경우 |
데이터 전처리처럼 순서가 중요한 작업에는 Forbid를 사용하자. 이전 작업이 아직 돌고 있는데 새 작업이 시작되면 데이터 충돌이 날 수 있다.
8. StatefulSet과 DaemonSet
Deployment, Job, CronJob 외에도 특수한 용도의 워크로드가 있다.
8.1 StatefulSet
순서와 고유성이 보장되는 Pod 관리. 주로 데이터베이스에 사용한다.
| 특징 | Deployment | StatefulSet |
|---|---|---|
| Pod 이름 | 랜덤 (backend-7d8f4-xk2j9) | 순번 (postgres-0, postgres-1) |
| 시작/종료 순서 | 무순서 | 순서대로 (0 → 1 → 2) |
| 스토리지 | Pod 삭제 시 PVC도 삭제 가능 | Pod 삭제해도 PVC 유지 |
| 네트워크 | IP가 바뀜 | Headless Service로 고정 DNS |
사용 예: PostgreSQL, Redis Cluster, Elasticsearch, Kafka
8.2 DaemonSet
모든 노드에 Pod을 1개씩 실행한다. 노드가 추가되면 자동으로 해당 노드에도 Pod이 생성된다.
사용 예:
- 로그 수집: Fluent Bit, Filebeat → 각 노드의 컨테이너 로그를 수집
- 모니터링: Node Exporter → 각 노드의 메트릭 수집
- 네트워크: kube-proxy, VPC CNI → 모든 노드에서 실행 필요
StatefulSet과 DaemonSet은 직접 YAML을 작성하는 경우가 드물다. 대부분 Helm Chart로 설치하므로 (예:
helm install postgresql bitnami/postgresql), 개념만 알아두면 충분하다.
9. 실무 치트시트
자주 쓰는 명령어
# === Deployment ===
kubectl get deploy # Deployment 목록
kubectl describe deploy <name> # 상세 정보
kubectl scale deploy/<name> --replicas=5 # Pod 수 변경
kubectl set image deploy/<name> app=image:v2 # 이미지 업데이트
kubectl rollout undo deploy/<name> # 롤백
kubectl rollout status deploy/<name> # 배포 상태 확인
# === Pod ===
kubectl get pods # Pod 목록
kubectl get pods -o wide # IP, 노드 정보 포함
kubectl describe pod <name> # 상세 (Events 확인!)
kubectl logs <name> # 로그
kubectl logs <name> -f --tail=100 # 실시간 로그 (최근 100줄)
kubectl exec -it <name> -- bash # 쉘 접속
kubectl delete pod <name> # Pod 삭제 (Deployment가 재생성)
# === Job / CronJob ===
kubectl get jobs # Job 목록
kubectl get cronjobs # CronJob 목록
kubectl create job --from=cronjob/<name> manual # CronJob을 수동으로 즉시 실행
kubectl delete job <name> # Job 삭제 (Pod도 삭제)
워크로드 선택 가이드
앱이 항상 실행되어야 하는가?
├── Yes → 상태를 유지해야 하는가?
│ ├── Yes → StatefulSet (DB, Redis)
│ └── No → Deployment (API, 서빙 서버)
│
└── No → 주기적으로 반복하는가?
├── Yes → CronJob (매일 전처리, 주간 리포트)
└── No → Job (마이그레이션, 벤치마크)
다음 글
이번 글에서는 K8s에서 앱을 실행하는 핵심 워크로드(Pod, Deployment, Job, CronJob)와 Resource 관리, Health Check를 다뤘다. 다음 글에서는 실행 중인 앱에 어떻게 접근하는지 — Service와 Ingress를 다룬다.
참고 문헌
Enjoy Reading This Article?
Here are some more articles you might like to read next: