K8s 시리즈 02: Pod, Deployment, Job, CronJob — K8s 워크로드 총정리

이 글은 K8s 시리즈의 두 번째 글이다.

이전 글에서 Kubernetes의 큰 그림(클러스터 구조, 컨테이너 이미지, YAML 기본)을 살펴봤다. 이번 글에서는 실제로 앱을 실행하는 핵심 리소스 — Pod, Deployment, Job, CronJob — 을 다룬다.


1. 워크로드 종류 한눈에 보기

Kubernetes에서 “앱을 실행하는 방법”은 하나가 아니다. 용도에 따라 다른 워크로드 리소스를 사용한다.

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 생명주기

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 별도 설정 필요)
  • requestslimits동일하게 설정해야 한다
  • 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, ReplicaSet, 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을 교체한다.

롤링 업데이트 전략 다이어그램
  1. 새 버전(v2) Pod을 하나 생성
  2. v2 Pod이 Ready가 되면 기존 v1 Pod 하나 제거
  3. 이 과정을 반복하여 모든 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를 다룬다.

다음 글: K8s 시리즈 03: Service, Ingress — 트래픽 라우팅과 외부 접근


참고 문헌




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • K8s 시리즈 06: EKS 네트워킹·보안·비용·운영
  • K8s 시리즈 05: Amazon EKS — 아키텍처와 Worker Node
  • K8s 시리즈 04: ConfigMap, Secret, Storage — 설정과 데이터 관리
  • K8s 시리즈 03: Service, Ingress — 트래픽 라우팅과 외부 접근
  • K8s 시리즈 01: Kubernetes란? 컨테이너부터 클러스터까지