0. Contents

1. Overview

Kubernetes의 Pod 실행 방식들

방식특징
베어 Pod노드 장애 시 복구 안 됨 (개발/테스트용)
ReplicaSet계속 실행되어야 하는 서비스용 (웹서버 등)
DaemonSet모든 노드에 하나씩 (모니터링, 로그 등)
Job딱 한 번, 끝날 때까지 확실히 실행 ← 이번 챕터!

Batch Job 패턴

  • 격리된 원자적 작업 단위를 관리하기 위한 패턴이다
  • 분산 환경에서 완료될 때까지 단기 실행 Pod를 안정적으로 실행하는 Job 리소스를 기반으로 한다

2. Problem

기존 방식의 공통점: 장기 실행 프로세스

  • 지금까지 살펴본 Pod 실행 방식들은 모두 장기 실행 프로세스를 위한 것이다
    • ReplicaSet: 계속 살아있어야 함 (웹서버)
    • DaemonSet: 계속 살아있어야 함 (노드당 1개)
    • 베어 Pod: 장애 시 복구 안 됨 (신뢰성 없음)

현실의 다른 요구사항

  • 그런데 현실에는 이런 요구사항도 있다

“정해진 작업을 딱 한 번, 확실히 끝내고 종료하고 싶다.”

예시

  • 대용량 데이터 배치 처리
  • 이미지/영상 파일 변환
  • 월말 정산 작업

이런 작업의 조건

  • 완료되면 끝나야 하고, 실패하면 재시도되어야 한다
  • 기존 primitive로는 이 요구사항을 안정적으로 채울 수 없다

3. Solution: Job

Job의 동작

  • Kubernetes의 Job은 ReplicaSet과 비슷하게 하나 이상의 Pod를 생성하고 성공적으로 실행되도록 보장한다
  • 차이점: 예상된 수의 Pod가 성공적으로 종료되면 Job이 완료된 것으로 간주된다
    • 더 이상 Pod를 만들지 않는다
ReplicaSet계속 실행되어야 하는 서비스용Pod 실행Pod 종료다시 새 Pod 생성무한 반복Job딱 한 번, 끝날 때까지 확실히 실행Pod 실행Pod 종료 (성공)완료로 간주추가 생성 안 함
ReplicaSet vs Job: 종료된 Pod를 다시 만드나, 완료로 끝내나

Job Controller는 어디서 동작하나?

Job Controller의 위치

  • Job은 Pod가 아니다
  • Control Plane의 Controller Manager 안에서 동작하는 컨트롤러다
Control PlaneController ManagerJob ControllerPod 생성 요청API 서버Pod 객체 저장 (etcd)스케줄러Pod에 노드 지정① 생성미배정 Pod 감시② 노드 지정워커 노드kubelet자기 노드에 배정된 Pod 감시③ Pod 실행컨테이너 시작
Job Controller는 API 서버에 Pod를 만들 뿐, 노드 배정은 스케줄러가, 실행은 kubelet이 맡는다

Pod가 실행되기까지

  • Job Controller는 kubelet에 직접 지시하지 않는다
  • Job Controller는 API 서버에 Pod 객체를 만든다
  • 스케줄러가 아직 노드가 없는 Pod를 보고 노드를 정한다
  • 그 노드의 kubelet이 자기 노드에 배정된 Pod를 받아 실제로 실행한다

Example 7-1. Job 명세

apiVersion: batch/v1   # Job은 batch API 그룹 소속 (core v1이 아님)
kind: Job
metadata:
  name: random-generator
spec:
  completions: 5                  # 성공한 Pod가 5개가 될 때까지 Job은 미완료 상태
  parallelism: 2                  # 동시에 최대 2개까지만 Pod 실행 (나머지는 대기)
  ttlSecondsAfterFinished: 300    # Job/Pod가 Completed 된 뒤 300초 지나면 자동 정리(GC)
  template:                       # 여기부터는 일반 Pod spec과 동일한 템플릿
    metadata:
      name: random-generator
    spec:
      restartPolicy: OnFailure    # Job에서 허용되는 값은 OnFailure/Never뿐 (Always 불가)
      containers:
      - image: k8spatterns/random-generator:1.0
        name: random-generator
        command: [ "java", "RandomRunner", "/numbers.txt", "10000" ]
        # 이 컨테이너가 종료(exit)해야 Pod가 완료 처리됨 — 데몬처럼 무한 대기하면 안 됨

템플릿 복제

  • template.spec.containers에 정의된 명세는 5개 Pod 모두 동일하게 복제된다
템플릿random-generator:1.0Pod 1동일 명세Pod 2동일 명세Pod 3동일 명세Pod 4동일 명세Pod 5동일 명세
template.spec.containers 명세가 5개 Pod에 그대로 복제된다

완료 판정 기준: Exit Code

판단 기준: exit code

  • Kubernetes는 컨테이너 내부 로직을 알지 못한다
  • 오직 exit code로만 성공/실패를 판단한다
exit code판정
0성공 (Completed)
1~실패 (재시작 정책에 따라 처리)

여러 컨테이너가 있는 Pod

  • Pod는 안의 컨테이너가 모두 성공(exit code 0)으로 끝나야 완료된다
  • 사이드카를 일반 컨테이너(containers)로 넣으면, 메인이 끝나도 사이드카가 끝날 때까지 Pod가 완료되지 않는다
    • 사이드카가 0이 아닌 exit code로 끝나면 Pod는 실패로 처리된다
  • 메인 컨테이너의 exit code만 완료 기준이 되는 것은 네이티브 사이드카(아래)일 때뿐이다
    • 이때 사이드카는 Job 완료를 막지 않는다
completions: 10 인 Job의 Pod에 사이드카(로그 수집기)를 붙인다면일반 컨테이너로 추가containers[]에 main과 함께 넣음main완료 ✅sidecar계속 실행 중 🔄Pod 완료 안 됨모든 컨테이너가 끝나야 완료네이티브 사이드카initContainers + restartPolicy: Alwaysmain완료 ✅sidecarmain이 끝나면 kubelet이 종료Pod 완료 → completions +1main 컨테이너만 완료 기준일반 컨테이너 sidecar가 0이 아닌 exit code로 끝나면 Pod는 실패로 처리된다
사이드카가 Job 완료를 막지 않는 건 네이티브 사이드카일 때뿐이다

네이티브 사이드카

  • 사이드카가 메인보다 오래 살아있으면 Pod 자체가 끝나지 않는 문제가 있었다
  • Kubernetes 1.29부터는 initContainers에 restartPolicy: Always를 지정해 네이티브 사이드카로 선언할 수 있다
  • 이렇게 하면 메인 컨테이너가 완료될 때 사이드카도 함께 자동 종료된다

주의: Pod 레벨과 컨테이너 레벨의 restartPolicy

  • 이 restartPolicy: Always는 Pod 레벨 필드가 아니라 initContainers[].restartPolicy, 즉 컨테이너 레벨 필드다
  • Job의 Pod 레벨 restartPolicy(위 Example 7-1의 restartPolicy 줄)는 여전히 OnFailure/Never만 허용된다
    • Always를 쓸 수 없다
  • 이 둘은 이름은 같지만 서로 다른 필드다 → 충돌 없이 공존한다
spec:
  restartPolicy: OnFailure        # Pod 레벨 — Job이라 Always 불가
  initContainers:
    - name: sidecar-logger
      image: fluentd
      restartPolicy: Always       # 컨테이너 레벨 — 네이티브 사이드카 지정용, 별개 필드
  containers:
    - name: main-job

컨테이너 레벨 restartPolicy: Always의 두 가지 의미

  1. 죽으면 재시작한다
    • 사이드카 프로세스가 크래시해도 kubelet이 그 컨테이너만 독립적으로 재시작한다
    • Pod의 재시작 정책과 무관하게 동작한다
  2. 완료 대기 대상에서 제외된다
    • 일반 init container는 종료 코드 0으로 끝나야 다음 컨테이너로 진행된다
    • restartPolicy: Always가 붙은 init container는 (끝나는 대신) Ready 상태가 되는 순간 다음 컨테이너로 진행하도록 kubelet이 다르게 취급한다
    • envoy, fluentd처럼 원래 끝나지 않는 프로세스를 init container 자리에 넣을 수 있는 이유가 이것이다
    • 그리고 메인 컨테이너가 모두 종료되면 kubelet이 이 사이드카에 SIGTERM을 보내 정리한다

일반 init container와의 차이

일반 init containerrestartPolicy: Always init container
다음 컨테이너로 진행종료(exit code 0) 대기 후 다음 컨테이너로 진행Ready 상태가 되면 (안 끝나도) 진행
main 종료 시—kubelet이 SIGTERM으로 정리

완료된 Pod는 왜 바로 삭제하지 않을까?

완료 즉시 삭제한다면?완료로그 확인 불가, 디버깅 불가 😱그래서완료Completed 상태로 유지로그 확인 가능ttlSecondsAfterFinished만큼 대기자동 삭제자원 낭비 방지
ttlSecondsAfterFinished: 로그 볼 시간은 주고, 그 다음엔 치운다

절충안: ttlSecondsAfterFinished

  • ttlSecondsAfterFinished는 **“로그 볼 시간은 주고, 그 다음엔 치운다”**는 절충안이다

restartPolicy: OnFailure vs Never

Job에서 restartPolicy: Always를 못 쓰는 이유

  • Job에서는 restartPolicy: Always를 사용할 수 없다
  • 성공한 Pod를 계속 재시작하면 Job이 영원히 끝나지 않기 때문이다
OnFailure같은 Pod를 재활용Pod (유지)컨테이너실패컨테이너재시작실패 → 컨테이너만 재시작 (Pod 유지)NeverPod를 버리고 새로 생성실패한 Pod컨테이너실패새 Pod컨테이너실패 → 새 Pod 생성(실패 Pod는 기록으로 남음)
OnFailure는 컨테이너를 다시 띄우고, Never는 Pod를 새로 만든다

Figure 7-1 해석: parallelism은 “슬롯” 개념

Figure 7-1이 나타내는 것

  • 책의 Figure 7-1은 시간 순서가 아니라 동시 실행 슬롯을 나타낸 그림이다
  • completions: 5, parallelism: 2일 때의 실행 모습은 다음과 같다
completions: 5 · parallelism: 2슬롯2슬롯1Pod ✅Pod ✅Pod ✅Pod ✅Pod 🔄1, 23, 45(완료 순번)
Figure 7-1 다시 그리기: parallelism은 동시에 쓸 수 있는 슬롯 수

“1, 2 완료"의 의미

  • 첫 번째·두 번째로 성공한 Pod라는 뜻이다
  • 2개씩 병렬로 돌다가, 남은 completions가 1개뿐인 마지막 순간에는 슬롯 하나만 사용된다

단계별 진행

  1. 슬롯1, 슬롯2 동시 실행 → 2개 완료 (2/5)
  2. 슬롯1, 슬롯2 동시 실행 → 4개 완료 (4/5)
  3. 슬롯 하나만 실행 (남은 completions 1개뿐이므로) → 5개 완료 (5/5)

실제 동시 실행 Pod 수

  • parallelism이 항상 그 숫자만큼 꽉 채워 실행되는 건 아니다
  • 남은 completions 수, 리소스 쿼터, 스로틀링 등에 따라 실제 동시 실행 Pod 수는 더 적을 수 있다

4. 왜 베어 Pod 대신 Job인가?

베어 PodJob
클러스터 재시작 생존✓ (Pod 객체도 etcd에 저장)✓ (etcd에 영구 기록)
완료 후 기록 보존✓ (직접 지울 때까지 남음)✓
여러 번 실행 (completions)✗✓
병렬 실행 (parallelism)✗✓
일시 중지/재개 (suspend)✗✓
노드 장애 시 대체 Pod✗ (실패 상태로 방치, 새로 만드는 주체 없음)✓ (Job 컨트롤러가 새 Pod를 만들고 스케줄러가 다른 노드를 고름)

suspend 동작

  • suspend: true로 설정하면 실행 중인 Pod가 모두 삭제된다
  • suspend: false로 되돌리면 재개된다

4.1 “etcd에 영구 기록"의 정확한 의미

영구 기록의 뜻: durability

  • 위 표의 “영구 기록"은 무한정 저장된다는 뜻이 아니다
  • 프로세스가 재시작돼도 사라지지 않는다(durability)는 뜻이다
  • 저장 용량이 무한한지(capacity)는 완전히 다른 질문이다

두 질문의 구분

  • 질문 A: 재시작하면 사라지나? → durability
    • etcd의 답: “예, 안 사라짐”
  • 질문 B: 영원히, 무한정 쌓아도 되나? → capacity/retention
    • etcd의 답: “아니요, quota와 GC가 있음”
질문답근거
재시작해도 살아남나? (durability)✅디스크(bbolt)에 저장, WAL 로그로 복구
무한정 쌓아도 되나? (capacity)❌기본 quota 2GB(권장 최대 8GB), 초과 시 읽기 전용 전환
자동으로 정리되나?일부만MVCC로 리비전이 계속 쌓임 · compaction(오래된 리비전 삭제)은 API 서버가 자동으로 요청(기본 5분마다) · defrag(디스크 공간 회수)는 직접 실행해야 함

etcd 자체의 복제 구조

  • etcd 자체도 Raft 알고리즘으로 리더-팔로워 복제 구조를 가진 고가용성 시스템이다
  • 이건 Kubernetes의 Control Plane/Worker Node 구조와는 다른 층위의 이야기다
    • etcd는 Control Plane 구성 요소 중 하나다
    • 그 안에서 자체적으로 한 번 더 리더를 선출해 복제한다

5. Job의 종류

종류를 나누는 두 값

  • .spec.completions와 .spec.parallelism 두 값의 조합에 따라 Job의 종류가 나뉜다

Single Pod Job

Pod 1개 실행

  • 두 값을 모두 생략하거나 기본값(1)으로 둔다
  • Pod 1개만 실행하고 성공하면 즉시 완료된다

Fixed Completion Count Job

completions: 5, parallelism: 1Pod1Pod2Pod3Pod4Pod5완료completions: 5, parallelism: 2Pod1Pod2Pod3Pod4Pod5완료
같은 completions 5라도 parallelism에 따라 한 번에 도는 Pod 수가 다르다

적합한 경우

  • 작업 개수를 미리 알고 있을 때
  • 작업 1개당 Pod 1개를 쓸 만큼 처리 비용이 클 때

Indexed Job

completions: 5, completionMode: IndexedPod 0JOB_COMPLETION_INDEX=0자기 몫만 처리Pod 1JOB_COMPLETION_INDEX=1자기 몫만 처리Pod 2JOB_COMPLETION_INDEX=2자기 몫만 처리Pod 3JOB_COMPLETION_INDEX=3자기 몫만 처리Pod 4JOB_COMPLETION_INDEX=4자기 몫만 처리
Indexed Job: 인덱스가 곧 담당 구역

인덱스로 각자 계산

  • 각 Pod가 JOB_COMPLETION_INDEX 환경변수(또는 batch.kubernetes.io/job-completion-index 어노테이션)로 자신의 인덱스를 받는다
  • 외부 작업 큐 없이 자기가 처리할 작업을 스스로 계산한다

Pod 0~4끼리 통신/동기화 여부

  • ❌ 서로 통신 안 함
  • ❌ 서로 상태 모름
  • ✅ 각자 인덱스로 독립 계산 → 겹치지 않게 처음부터 설계됨

조율이 필요 없는 이유

  • 시험지 나눠주듯 “130번은 1조, 3160번은 2조"처럼 애초에 겹치지 않게 구간을 나눠주기 때문이다

Example 7-2. Indexed Job

apiVersion: batch/v1
kind: Job
metadata:
  name: file-split
spec:
  completionMode: Indexed          # 각 Pod에 0부터 시작하는 고유 인덱스 부여
  completions: 5                   # 인덱스는 0, 1, 2, 3, 4 (총 5개)
  parallelism: 5                   # 5개 전부 동시 실행 (인덱스별로 병렬 처리)
  template:
    spec:
      containers:
      - image: alpine
        name: split
        command:
        - "sh"
        - "-c"
        - |
          # JOB_COMPLETION_INDEX는 Kubernetes가 각 Pod에 자동 주입하는 환경변수
          # START: 이 Pod 구간 바로 앞 줄 번호, END: 이 Pod가 처리할 마지막 줄 번호
          START=$(expr $JOB_COMPLETION_INDEX \* 10000)
          END=$(expr $START + 10000)
          # NR: awk의 현재 줄 번호 변수 (1부터 시작) → START+1 ~ END 줄, 정확히 10,000줄
          # 결과를 인덱스별 파일로 저장
          awk -v s=$START -v e=$END 'NR>s && NR<=e' /logs/random.log \
            > /logs/random-$JOB_COMPLETION_INDEX.txt
        volumeMounts:
        - mountPath: /logs
          name: log-volume    # 모든 Pod가 같은 볼륨을 공유해야 원본 파일에 접근 가능
      restartPolicy: OnFailure

Example 7-2가 하는 일

  • 50,000줄짜리 random.log 파일을 5개 Pod가 동시에 10,000줄씩 나눠서 처리한다
random.log50,000줄Pod0random-0.txtPod1random-1.txtPod2random-2.txtPod3random-3.txtPod4random-4.txt5개 동시 실행
Example 7-2: 파일 하나를 인덱스별로 잘라 5개 Pod가 동시에 처리

awk의 NR 변수

  • NR은 awk 내부에서 현재 처리 중인 줄 번호를 나타내는 변수다
  • NR>s && NR<=e 조건으로 해당 구간만 잘라낸다
  • NR은 1부터 시작하므로 인덱스 0은 110,000번째 줄, 인덱스 1은 10,00120,000번째 줄을 맡는다

Work Queue Job

설정 방식

  • completions는 아예 지정하지 않는다
  • parallelism만 1보다 크게 설정한다

Job 종류별 설정 비교

종류completionsparallelismcompletionMode
Single Pod Job11—
Fixed Completion CountNM—
Indexed JobNMIndexed
Work Queue Job ← 이번 것없음!N (>1)—

completions를 못 정하는 이유

  • 작업이 외부 큐(Redis, RabbitMQ 등)에 쌓여 있다
  • 그 큐에 몇 개가 들어있는지 Kubernetes는 알 방법이 없다
  • 그래서 숫자로 된 목표치 대신, “큐가 비면 끝"이라는 신호를 Pod가 직접 판단해서 알려주는 방식을 쓴다

완료 조건

  1. 최소 1개 Pod가 “큐 비었음"을 확인하고 성공 종료
  2. 나머지 실행 중인 Pod들도 모두 종료
  • 두 조건을 모두 채우면 → Job 완료

실행 흐름

parallelism: 3동시에 3개 실행, 각자 큐에서 작업을 꺼내옴시간Pod1작업1작업2작업3Pod2작업4작업5작업6Pod3작업7작업8큐 비어있음 확인→ exit 0 (성공)계속 처리큐 비었음exit 0계속 처리큐 비었음exit 0Job 완료모든 Pod 종료
Work Queue Job: 먼저 빈 큐를 본 Pod가 완료 신호, Job은 모두 끝날 때까지 기다린다

완료 신호와 대기

  • 가장 먼저 큐가 빈 걸 발견한 Pod가 성공 종료로 완료 신호를 보낸다
  • Job 컨트롤러는 나머지 Pod들도 다 끝날 때까지 기다린다

Indexed Job과의 조율 방식 차이

조율 방식: 미리 나눈 구역 vs 실시간 결정

  • Indexed Job은 담당 구역이 인덱스로 미리 정해져 있다 → Pod끼리 조율이 필요 없다
  • Work Queue Job은 담당 구역이 없다
    • “누가 뭘 처리할지"를 Pod들이 큐를 보면서 실시간으로 정해야 한다
  • 이 조율은 외부 큐 시스템이 “이 작업은 이미 누가 가져갔다"를 관리해주기 때문에 가능하다

비유: 창구 배정

  • Indexed Job = 번호대로 창구 배정 (“110번은 1번 창구, 1120번은 2번 창구”)
  • Work Queue Job = 번호표 뽑아 아무 창구나 이용 (실시간 배정)

언제 쓰나

적합한 경우: 세밀한 작업 항목

  • 작업 항목이 세밀(granular) 할 때 쓴다
    • 항목 하나하나가 너무 작아서 Pod 하나씩 배정하면 낭비인 경우
  • 예: 큐에 작은 메시지 100만 개가 쌓여 있다면
    • Indexed Job으로 100만 개 Pod를 띄우는 대신
    • Work Queue Job으로 Pod 10개가 큐에서 계속 꺼내며 처리하는 쪽이 합리적이다
Indexed JobWork Queue Job
completions지정함(작업 개수를 미리 앎)지정 안 함
작업 분배인덱스로 미리 구간을 나눔공유 큐에서 실시간으로 꺼내감
Pod 간 조율불필요(인덱스로 이미 분리됨)필요(큐 시스템이 대신 관리)
완료 조건모든 인덱스 완료1개 이상 성공 + 나머지 종료
적합한 경우작업량을 미리 알고, 항목이 큼직함작업량 모름, 항목이 잘게 쪼개져 다수

Work Queue Job은 몇 개인지 모르는 작업 큐를 여러 Pod가 나눠 처리할 때 쓰는 방식이다. completions를 아예 빼는 대신, 큐가 비었는지를 Pod 스스로 판단해 Job 컨트롤러에게 완료를 알린다.


6. Partitioning의 한계: 전체 작업자 수를 모른다

Pod 스스로 나누는 Indexed Job

  • Work Queue Job은 외부 큐(Redis, RabbitMQ 등)가 이미 작업을 잘라준다
  • Indexed Job은 Pod 스스로 작업을 나눠야 한다
  • 이때 각 Pod는 다음을 알아야 한다
필요한 정보제공 여부
자기 인덱스✅ (JOB_COMPLETION_INDEX 로 제공됨)
전체 Pod 개수❌ (completions 값 — 2023년 기준 미지원!)
작업 전체 크기✅ (파일 크기 등, 별도로 알아야 함)

우회책 두 가지

  • JOB_COMPLETION_TOTAL 같은 환경변수가 있으면 편하겠지만 아직 지원되지 않는다
  • 대신 두 가지 우회책이 있다

방법 1: 하드코딩

int totalPods = 5;  // 전체 Pod 개수를 소스 코드에 직접 하드코딩
                     // completions 값과 반드시 일치해야 하지만 강제할 방법이 없음

코드와 YAML의 강한 결합

  • completions를 바꾸려면 코드도 수정하고 이미지도 새로 빌드해야 한다
  • 코드와 YAML이 강하게 결합된다

방법 2: 환경변수로 주입

spec:
  completions: 5           # 실제 Job의 완료 목표치
  template:
    spec:
      containers:
      - env:
        - name: JOB_COMPLETION_TOTAL
          value: "5"        # 위 completions 값을 사람이 손으로 그대로 복사해 넣음
                            # → 나중에 completions만 바꾸고 이 값을 깜빡하면 버그로 이어짐

남는 실수 여지

  • 이미지를 새로 빌드할 필요는 없다
  • 하지만 completions를 바꿀 때 YAML 안 두 곳(completions와 환경변수)을 모두 수정해야 한다
    • 실수 여지가 남는다
하드코딩환경변수 주입
이미지 재빌드 필요✅❌
수정 지점코드 + YAMLYAML 2곳
결합도높음중간
방법 1: 하드코딩결합도 높음소스 코드 (이미지 안)int totalPods = 5;Job YAMLcompletions: 5반드시 일치 · 강제할 방법 없음completions 변경 → 코드 수정 + 이미지 재빌드수정 지점: 코드 + YAML방법 2: 환경변수로 주입결합도 중간Job YAMLcompletions: 5- name: JOB_COMPLETION_TOTALvalue: "5"사람이 손으로 그대로 복사이미지 재빌드 불필요 · YAML 2곳 수정completions만 바꾸고 깜빡하면 버그
같은 숫자 5가 두 곳에 산다: 코드 + YAML(하드코딩) vs YAML 2곳(환경변수)

7. 작업 항목을 어떻게 Job/Pod에 매핑할까

선택은 개발자 몫

  • Job은 작업 항목을 어떻게 나눌지 강제하지 않는다
  • 개발자가 아래 두 전략 중 선택해야 한다
  • 핵심 차이는 “누가 작업을 나누고 관리하는가” 다

7.1 전략 1: 작업 항목 1개 = Job 1개

Kubernetes가 작업별로 관리

  • Kubernetes 자체가 각 작업을 별도의 Job으로 인식하고 관리한다
  • 이미지 100장을 변환한다면, 이미지 하나당 Job 하나를 만든다
# job-image-001.yaml (이미지 하나당 파일 하나, 총 100개 필요)
apiVersion: batch/v1
kind: Job
metadata:
  name: convert-image-001
spec:
  template:
    spec:
      containers:
      - image: image-converter
        args: ["--file", "image-001.jpg"]
      restartPolicy: OnFailure
kubectl get jobs

NAME                  COMPLETIONS   AGE
convert-image-001     1/1           5m
convert-image-002     0/1           5m   ← 이것만 실패!
convert-image-003     1/1           5m

실패한 것만 재실행

  • Kubernetes가 Job 단위로 성공/실패를 추적해준다
  • 그래서 실패한 것만 콕 집어 재실행할 수 있다
kubectl delete job convert-image-002
kubectl apply -f job-image-002.yaml   # 이것만 다시 실행

7.2 전략 2: 작업 항목 전체 = Job 1개

앱 코드가 나눠 처리

  • Kubernetes는 전체를 하나의 작업으로만 인식한다
  • 100개를 어떻게 나눠 처리할지는 **컨테이너 내부 코드(배치 프레임워크)**가 담당한다
# job-image-batch.yaml (딱 1개)
apiVersion: batch/v1
kind: Job
metadata:
  name: convert-all-images
spec:
  completions: 1
  template:
    spec:
      containers:
      - image: image-converter
        command: ["python", "batch_processor.py"]
      restartPolicy: OnFailure
# batch_processor.py — 컨테이너 안에서 실행되는 코드
images = get_all_images()  # 100개 이미지 목록

for image in images:
    try:
        convert(image)
        mark_as_done(image)   # 진행상황을 DB 등에 자체 기록
    except Exception as e:
        log_error(image, e)   # 실패해도 앱이 알아서 처리하고
        continue               # 다음 이미지로 계속 진행
kubectl get jobs

NAME                  COMPLETIONS   AGE
convert-all-images    1/1           5m

Kubernetes에 보이는 것: 성공 여부뿐

  • Kubernetes 입장에서는 “성공했다"만 보인다
  • 100개 중 몇 개가 실패했는지는 Kubernetes가 모른다
  • 실패 내역은 컨테이너 로그나 앱이 남긴 기록(DB 등)을 직접 확인해야 알 수 있다

7.3 두 전략 비교

전략 1Kubernetes가 100개를 각각 봄Job1Pod1 (image-001)Job2Pod2 (image-002)Job3Pod3 (image-003)· · ·Job100Pod100추적 기준"몇 번 Job이 실패했나"전략 2Kubernetes는 1개만 봄Job1Pod1컨테이너 내부 코드for image in 100개:알아서 처리Kubernetes는 이 안을 모른다추적 기준"컨테이너가 exit code 0으로 끝났나"
누가 작업을 나누고 관리하나: Kubernetes(전략 1) vs 컨테이너 안의 앱 코드(전략 2)
Job per 작업Job for 전체
Job 수많음1개
개별 추적✅❌
관리 주체Kubernetes앱 내부 (Spring Batch, JBeret 등)
적합한 경우복잡/중요단순/대량

7.4 선택 기준

전략 1이 맞는 경우

  • 작업 하나가 실패했을 때 그것만 정확히 찾아 재실행해야 할 때
  • 작업마다 우선순위·스케줄이 다를 때
  • 예: 사용자별 결제 처리
    • 각각 독립적으로 성공/실패를 추적해야 하는 경우

전략 2가 맞는 경우

  • 작업 개수가 수천~수만 개라 Job을 그만큼 만들면 API 서버에 부담이 될 때
  • 실패 처리를 앱이 이미 잘 하고 있을 때 (재시도 로직, 실패 큐 등)
  • 예: 로그 파일 100만 줄 처리
    • Job 100만 개보다 앱 내부에서 나눠 처리하는 편이 효율적이다

전략 1은 Kubernetes에게 각 작업을 낱개로 보여주고 관리를 맡기는 방식이고, 전략 2는 Kubernetes에게는 1개로 뭉뚱그려 보여주고, 나누는 일은 컨테이너 안의 앱 코드가 직접 하는 방식이다.


8. Discussion

Job의 범위: 최소한의 기본기

  • Job 프리미티브는 스케줄링의 최소한의 기본기만 제공한다
  • 복잡한 요구사항은 배치 프레임워크(Java 생태계의 Spring Batch, JBeret 등)와 조합해야 한다
Job이 해주는 것Job이 안 해주는 것
✅ Pod 실행 보장❌ 작업 항목 세부 관리
✅ 완료 횟수 관리❌ 진행률 추적
✅ 병렬 실행❌ 작업 간 의존성 관리
✅ 실패 시 재시작

실행 시점별 리소스

  • 모든 서비스가 항상 실행될 필요는 없다
실행 시점리소스
항상 실행ReplicaSet / Deployment
모든 노드DaemonSet
필요할 때만Job ← 이번 챕터
주기적으로CronJob (Job 기반)

짧은 작업을 Job으로 돌리는 이점

  • Job으로 짧은 수명의 작업을 처리하면, ReplicaSet처럼 계속 떠 있는 추상화를 쓰는 것보다 다른 워크로드를 위한 자원을 절약할 수 있다
  • Job은 노드의 가용 용량, Pod 배치 정책, 컨테이너 의존성을 고려해 스케줄링된다

Job은 “필요할 때만, 필요한 만큼” 실행되는 워크로드를 위한 primitive다. 다양한 워크로드를 지원하는 것이 Kubernetes의 강점이다.