소개
Deployment 는 서비스를 지속적으로 실행 상태로 유지합니다. 반면 배치 작업의 성공 조건은 다릅니다. 작업을 완료하고, 결과를 기록한 뒤, 실행을 멈춰야 합니다. Kubernetes 에서는 한 번만 실행되는 작업을 Job 으로, 반복 실행되는 작업을 CronJob 으로 모델링합니다.
이 실습에서는 Job 을 실행하고 해당 Pod 와 로그를 확인합니다. 또한 제어된 병렬 실행을 추가하고, CronJob 을 예약하며, 일시 중지와 기록 보존 한도를 활용해 반복 작업을 운영 가능한 상태로 관리하는 방법을 익힙니다.
이 실습은 성공 조건을 비교하는 과정이라고 생각해 보세요. Deployment 는 복제본을 계속 사용할 수 있는 상태로 유지하면 성공합니다. Job 은 필요한 수의 Pod 가 종료 코드 0으로 완료되면 성공합니다. CronJob 은 의도한 시간에 Job 을 생성하면 성공합니다. 이 세 계층을 분리해서 이해하면 YAML 의 구조도 훨씬 쉽게 읽을 수 있습니다.
일회성 Job 실행
이 단계에서는 Pod 가 보고서를 출력한 뒤 정상적으로 종료되는 Job 을 생성합니다. 여기 문서를 사용해 매니페스트를 작성합니다. 따옴표로 감싼 EOF는 YAML 내부의 문자가 셸에 의해 확장되지 않도록 합니다.
YAML 을 입력하기 전에 구조를 살펴보세요. kind: Job은 사용할 컨트롤러를 지정하고, spec.template은 컨트롤러가 생성할 Pod 를 정의하며, Pod 에는 짧은 시간 동안 실행되는 컨테이너 하나가 포함됩니다. Deployment 템플릿과 달리 Job 의 Pod 는 작업이 완료되면 종료되어야 하므로 restartPolicy: Never 또는 OnFailure를 사용해야 합니다.
cd /home/labex/project/batch-workloads
cat <<'EOF' > report-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: report-job
namespace: practice-batch
spec:
backoffLimit: 2
template:
metadata:
labels:
app: report-job
spec:
restartPolicy: Never
containers:
- name: reporter
image: busybox:1.36
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "echo report-complete"]
EOF
먼저 Kubernetes 에 파일을 로컬에서 검증하도록 요청합니다. --dry-run=client는 객체를 실제로 생성하지 않고 구조만 확인합니다. 새 매니페스트를 작성할 때 유용한 습관입니다.
kubectl apply --dry-run=client -f report-job.yaml
결과가 job.batch/report-job created (dry run)이라면 파일 구조가 올바른 것입니다. 이제 실제로 적용한 다음, 필요한 실행 시간을 추측하지 말고 Job 의 Complete 조건을 기다립니다.
kubectl apply -f report-job.yaml
kubectl wait --for=condition=Complete job/report-job -n practice-batch --timeout=60s
restartPolicy: Never를 사용하면 다른 Pod 가 필요한지 여부를 Job 컨트롤러가 결정합니다. backoffLimit: 2는 실패 시 재시도 횟수를 제한합니다. 완료된 Pod 의 출력을 컨트롤러를 통해 확인합니다.
kubectl logs job/report-job -n practice-batch
report-complete가 출력되어야 합니다. job/report-job을 지정하면 kubectl 이 해당 Job 의 Pod 를 자동으로 찾아 주므로, 생성된 Pod 이름을 직접 복사할 필요가 없습니다.
세 명령은 짧고 이해하기 쉬운 실행 흐름을 보여 줍니다.
job.batch/report-job created
job.batch/report-job condition met
report-complete
condition met는 Job 컨트롤러가 완료 상태를 기록했다는 뜻이고, 이어지는 로그는 애플리케이션의 출력입니다. 새로 시작한 로컬 클러스터에서는 네트워크가 안정화되는 동안 이처럼 작은 Job 도 완료까지 몇 초가 걸릴 수 있습니다. 일정 시간 동안 무조건 대기하는 대신 조건을 기다리면 실행 시간을 추측하지 않아도 안정적인 작업 흐름을 만들 수 있습니다.
Job 의 소유 관계와 완료 상태 추적
이 단계에서는 완료된 Job 과 계속 실행되는 Deployment 를 비교합니다. 완료된 Job 은 로그와 종료 상태를 확인할 수 있도록 해당 Pod 를 유지합니다.
먼저 일반적인 표 형식으로 확인합니다. Job 에는 성공적으로 완료된 작업이 하나 표시되고, Pod 에는 조회하는 열에 따라 Completed 또는 Succeeded 단계가 표시됩니다.
kubectl get jobs,pods -n practice-batch
kubectl get pods -n practice-batch -l job-name=report-job -o custom-columns='POD:.metadata.name,PHASE:.status.phase,OWNER:.metadata.ownerReferences[0].name,EXIT_CODE:.status.containerStatuses[0].state.terminated.exitCode'
kubectl describe job report-job -n practice-batch
Pod 단계는 Succeeded, 소유자는 report-job, 종료 코드는 0이어야 합니다. 필요한 수의 Pod 가 성공하면 Job 이 완료된 것으로 간주합니다. 성공적으로 종료된 컨테이너를 계속 실행 상태로 유지하기 위해 다시 시작하지는 않습니다.
사용자 정의 열 조회는 중첩된 네 개의 필드를 간단한 증거 표로 변환합니다. “이 Pod 의 소유자는 누구이며 프로세스가 성공했는가?”처럼 구체적인 질문에 답할 때는 전체 Pod YAML 을 읽는 것보다 훨씬 효율적입니다.
테스트 결과는 다음과 같았습니다.
POD PHASE OWNER EXIT_CODE
report-job-cd54c Succeeded report-job 0
생성되는 접미사는 환경에 따라 달라집니다. 행을 가로로 읽어 보세요. OWNER=report-job은 Pod 가 해당 컨트롤러에 연결되어 있음을 보여 주며, PHASE=Succeeded와 EXIT_CODE=0은 프로세스가 정상적으로 완료되었음을 증명합니다.
여러 완료 작업을 병렬로 실행
이 단계에서는 성공적으로 완료된 Pod 가 네 개 필요하고, 동시에 최대 두 개까지 실행할 수 있는 Job 을 생성합니다.
매니페스트를 작성하기 전에 서로 독립적인 다음 두 수치를 이해해야 합니다.
completions: 4는 전체적으로 네 개의 작업 단위를 성공적으로 완료해야 한다는 뜻입니다.parallelism: 2는 이 작업 단위 중 최대 두 개만 동시에 실행할 수 있다는 뜻입니다.
completionMode: Indexed는 작업 단위에 0부터 3까지의 인덱스를 할당합니다. 각 Pod 는 JOB_COMPLETION_INDEX 환경 변수를 통해 고유한 인덱스를 전달받습니다. 이를 활용하면 실제 배치 프로그램에서 모든 Pod 가 동일한 파티션을 처리하는 대신 데이터셋을 나누어 처리할 수 있습니다.
cat <<'EOF' > indexed-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: indexed-job
namespace: practice-batch
spec:
completionMode: Indexed
completions: 4
parallelism: 2
backoffLimitPerIndex: 1
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: busybox:1.36
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "echo worker-index=${JOB_COMPLETION_INDEX}; sleep 2"]
EOF
매니페스트를 적용하고 네 개의 인덱스가 모두 완료될 때까지 기다립니다.
마지막 명령의 -L 옵션은 batch.kubernetes.io/job-completion-index 레이블 값을 추가 열로 표시합니다. 따라서 각 Pod 의 전체 YAML 을 열어 보지 않아도 생성된 Pod 가 어떤 작업 인덱스에 해당하는지 확인할 수 있습니다.
kubectl apply -f indexed-job.yaml
kubectl wait --for=condition=Complete job/indexed-job -n practice-batch --timeout=90s
kubectl get pods -n practice-batch -l job-name=indexed-job -L batch.kubernetes.io/job-completion-index
completions는 성공적으로 완료해야 하는 전체 작업 수이고, parallelism은 동시에 실행할 수 있는 최대 Pod 수입니다. Indexed 완료 방식을 사용하면 각 Pod 에 JOB_COMPLETION_INDEX를 통해 안정적인 인덱스가 할당됩니다.
최종 표에는 서로 다른 인덱스 레이블을 가진 완료 상태의 Pod 네 개가 표시되어야 합니다. 실행이 동시에 진행되므로 완료 시점은 결정적이지 않으며, 숫자 순서대로 표시되지 않을 수도 있습니다.
예시는 다음과 같습니다.
NAME STATUS JOB-COMPLETION-INDEX
indexed-job-0-gw5wq Completed 0
indexed-job-1-vw4qk Completed 1
indexed-job-2-mpvnr Completed 2
indexed-job-3-8z5j8 Completed 3
네 개의 행이 보인다고 해서 네 개의 Pod 가 동시에 실행되었다는 뜻은 아닙니다. 네 개의 인덱스는 전체 작업량을 나타내고, parallelism: 2는 Kubernetes 가 한 번에 실행할 수 있는 Pod 수를 제한합니다.
CronJob 예약
이 단계에서는 매분 Job 을 시작하는 CronJob 을 생성합니다. Cron 형식은 분, 시, 일, 월, 요일의 다섯 필드로 구성됩니다.
*/1 * * * * 일정은“매분”을 의미합니다. 짧은 실습 시간 안에 동작을 확인할 수 있도록 의도적으로 빈번하게 설정한 것입니다. 실제 운영 환경에서는 보통 이보다 드문 주기를 사용합니다. 또한 매니페스트에는 중첩된 jobTemplate이 포함되어 있습니다. CronJob 이 Job 을 생성하고, 각 Job 이 다시 Pod 를 생성하기 때문입니다.
cat <<'EOF' > heartbeat-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: heartbeat
namespace: practice-batch
spec:
schedule: "*/1 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: heartbeat
image: busybox:1.36
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "date -u; echo heartbeat-complete"]
EOF
일정을 적용하고 요약 정보를 확인합니다.
kubectl apply -f heartbeat-cronjob.yaml
kubectl get cronjob heartbeat -n practice-batch
concurrencyPolicy: Forbid는 이전 실행이 아직 끝나지 않은 경우 새 실행이 겹쳐서 시작되지 않도록 합니다. 다음 분까지 기다리지 않고 즉시 CronJob 템플릿에서 Job 하나를 생성하여 확인해 보겠습니다.
kubectl create job --from=cronjob/heartbeat heartbeat-manual -n practice-batch
kubectl wait --for=condition=Complete job/heartbeat-manual -n practice-batch --timeout=60s
kubectl logs job/heartbeat-manual -n practice-batch
로그에는 UTC 타임스탬프와 그 뒤의 heartbeat-complete가 표시됩니다. 수동으로 생성한 Job 은 동일한 템플릿을 사용하지만 CronJob 의 이후 일정에는 영향을 주지 않습니다.
일반적인 출력은 다음과 같습니다.
job.batch/heartbeat-manual condition met
Thu Aug 13 07:48:29 UTC 2026
heartbeat-complete
타임스탬프는 환경에 따라 달라집니다. 컨테이너 명령에서 date -u를 사용하므로 UTC 가 표시된다는 점에 유의하세요. 이렇게 하면 클러스터의 예약 시간과 터미널의 현지 시간대가 혼동되는 것을 방지할 수 있습니다.
예약 작업 일시 중지 및 재개
이 단계에서는 정의 자체를 삭제하지 않고 CronJob 을 일시 중지합니다. 유지보수나 장애 대응 중에 유용한 방법입니다.
kubectl patch는 JSON 페이로드에 지정된 필드만 변경합니다. 여기서는 spec.suspend를 true로 설정합니다. CronJob 객체와 일정, 이미 생성된 Job 은 그대로 유지됩니다.
kubectl patch cronjob heartbeat -n practice-batch --type=merge -p '{"spec":{"suspend":true}}'
kubectl get cronjob heartbeat -n practice-batch -o custom-columns='NAME:.metadata.name,SCHEDULE:.spec.schedule,SUSPEND:.spec.suspend,LAST_SCHEDULE:.status.lastScheduleTime'
기존 Job 은 계속 실행됩니다. 일시 중지는 새로 예약된 Job 이 생성되는 것만 막습니다. 확인이 끝나면 다시 재개합니다.
kubectl patch cronjob heartbeat -n practice-batch --type=merge -p '{"spec":{"suspend":false}}'
kubectl get jobs -n practice-batch --sort-by=.metadata.creationTimestamp
이제 세 가지 계층을 구분할 수 있습니다. CronJob 은 일정을 정의하고, 일정에 따라 실행될 때마다 Job 을 생성하며, 각 Job 은 하나 이상의 Pod 를 생성합니다.
실습 전체가 다음 분 경계 전에 끝날 수도 있으므로 LAST_SCHEDULE이 여전히 <none>으로 표시될 수 있습니다. 이는 오류가 아닙니다. heartbeat-manual은 CronJob 스케줄러가 아니라 --from 옵션을 사용해 필요할 때 직접 생성했기 때문입니다. 최종적으로 중요한 값은 SUSPEND=false이며, 이 값이 있어야 이후의 예약 실행이 다시 허용됩니다.
요약
Job 을 사용해 한정된 작업을 실행하고, Indexed 완료 방식을 사용해 병렬 작업을 처리했으며, CronJob 으로 반복 일정을 구성했습니다. 또한 컨트롤러 소유 관계, 로그, 완료 상태, 동시성 정책, 기록 보존 한도, 일시 중지 기능을 확인했습니다. 이러한 실행 주기의 차이는 올바른 Kubernetes 워크로드 컨트롤러를 선택하는 데 핵심적인 기준입니다.


