completions만큼의 성공한 Pod 수이며, 실패·재시도는 컨트롤러가 관리completions로 끝나는 작업을 선언적으로 관리.backoffLimit, activeDeadlineSeconds, *podFailurePolicy로 세밀 제어.parallelism/completions, Indexed(샤딩)·워크큐 패턴 모두 지원.ttlSecondsAfterFinished로 완료 리소스 정리.apiVersion: batch/v1
kind: Job
metadata:
name: nginx-indexed-job
spec:
completions: 8 # 1
parallelism: 3 # 2
completionMode: Indexed # 3
backoffLimit: 3 # 4
activeDeadlineSeconds: 1800 # 5
ttlSecondsAfterFinished: 300 # 6
podFailurePolicy: # 7
rules:
- action: FailJob # 7-1
onExitCodes:
containerName: nginx-worker
operator: In
values: [2, 42]
- action: Ignore # 7-2
onPodConditions:
- type: DisruptionTarget
- action: Count # 7-3
onExitCodes:
containerName: nginx-worker
operator: In
values: [137]
template:
metadata:
labels:
app: nginx-batch
spec:
restartPolicy: Never
containers:
- name: nginx-worker
image: nginx:1.27-alpine
env:
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
- name: TOTAL_SHARDS
value: "8"
command: ["/bin/sh","-c"]
args:
- |
echo "[NGINX Job] shard index=${JOB_COMPLETION_INDEX}/${TOTAL_SHARDS}";
# (데모) 인덱스별 가짜 작업 수행 후 정상 종료
sleep 1
echo "Shard ${JOB_COMPLETION_INDEX} done."
.spec.completions: 총 성공 Pod 수(작업 개수).spec.parallelism: 동시 실행 Pod 수(동시성).spec.completionMode: Indexed → 각 Pod에 고유 인덱스 부여(샤딩).spec.completions 개수만큼 성공한 파드 수가 채워지면 Job 완료로 간주합니다. 워크 큐 패턴처럼 “같은 일을 하는 파드 여러 개”에 적합.spec.backoffLimit: Pod가 실패할 때 새 Pod 재시도 횟수.spec.activeDeadlineSeconds: 전체 작업의 최대 허용 시간(초).spec.ttlSecondsAfterFinished: 완료 후 자동 정리(가비지).spec.podFailurePolicy: 종료 코드/조건에 따라 재시도/실패 정책 세밀화backoffLimit)를 넘으면 잡을 실패로 보고 싶을 때.spec.completions / .spec.parallelism 두개의 파라미터를 기준으로 다음 Job으로 구분
.spec.completions / .spec.parallelism 을 생략하거나 기본 값으로 1으로 설정하는 경우.spec.completions 에 지정하면, 많은 파드가 성공해야 한다..spec.completions 설정 값과 같은 수의 파드가 성공적으로 완료되어야 해당 잡이 완료된 것으로 간주한다..spec.completions를 생략하고, .spec.parallelism를 1보다 큰 정수로 세팅하면, 병렬 잡에 대한 작업 큐를 가진다..spec.successPolicy(v1.33에서 GA) → 해당 조전을 만족하면 남은 파드도 모두 종료 됨spec:
completions: 8
completionMode: Indexed
successPolicy:
rules:
- succeededIndexes: 0 # 지정된 파드 리더 (0번)이 성공하는 경우
succeededCount: 1succeededIndexes : 해당 인덱스(샤드)들이 성공해야 Job을 성공으로 간주할지를 지정합니다.succeededCount : 지정된 파드의 성공 개수.spec.backoffLimitPerIndex: 인덱스별 재시도 한도..spec.maxFailedIndexes: 실패 인덱스가 이 값을 넘으면 Job 실패.restartPolicy: Never에서만 사용 가능.\spec:
completionMode: Indexed
backoffLimitPerIndex: 3
maxFailedIndexes: 1
template:
spec:
restartPolicy: Never
.spec.podReplacementPolicy: 파드 완전 종료 후 교체할지, 종료 시작(terminating)만 돼도 교체할지 선택.Failed(완전 실패 후 교체), TerminatingOrFailed(기존 동작).Failed이고 다른 값은 허용되지 않음—겹실행/중복 처리 위험을 줄입니다.예시:
spec:
podFailurePolicy: { ... }
podReplacementPolicy: Failed # PFP가 있으면 실제로는 Failed만 허용
.spec.managedBy로 내장 Job 컨트롤러 대신 외부 컨트롤러(예: Kueue)가 조정하도록 위임 가능.kubernetes.io/job-controller는 금지.spec:
managedBy: "kueue.x-k8s.io"kube-linter 적용(.kube-linter.yaml)
checks:
include:
- "unset-cpu-requirements"
- "unset-memory-requirements"
- "no-anti-affinity"
- "minimum-three-replicas"
- "mismatching-selector"
- "required-annotation-email"
- "no-read-only-root-fs"
쿠버네티스 버전 확인 ⇒ v1.34버전으로 테스트 진행
kubectl version

# 네임스페이스 생성
kubectl create ns job-lab
kubectl get ns 
configmap-worker-scripts.yaml)apiVersion: v1
kind: ConfigMap
metadata:
name: job-worker-scripts
namespace: job-lab
data:
worker.sh: |
#!/bin/sh
set -eu
idx="${JOB_COMPLETION_INDEX:-0}"
mode="${MODE:-ok}"
echo "[worker] mode=$mode index=$idx"
case "$mode" in
ok)
echo "OK shard $idx"
exit 0
;;
exit42)
# FAIL_INDEX와 인덱스가 같으면 42로 즉시 종료(비복구성 에러 시뮬)
if [ "${FAIL_INDEX:-3}" = "$idx" ]; then
echo "Failing index $idx with exit 42"
exit 42
fi
echo "OK shard $idx"
exit 0
;;
always_fail)
# 특정 인덱스는 항상 실패(재시도/백오프/FailIndex 실험용)
if [ "${FAIL_INDEX:-5}" = "$idx" ]; then
echo "Always failing index $idx"
exit 1
fi
echo "OK shard $idx"
exit 0
;;
*)
echo "unknown MODE=$mode"
exit 2
;;
esac
kubectl apply -f configmap-worker-scripts.yaml 
위 스크립트는 세 가지 모드를 지원합니다.
MODE=ok: 성공MODE=exit42+FAIL_INDEX=N: 특정 인덱스에서 exit 42MODE=always_fail+FAIL_INDEX=N: 특정 인덱스 항상 실패
job-indexed-successpolicy.yaml)# filename: job-indexed-successpolicy.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: indexed-successpolicy
namespace: job-lab
annotations:
owner: "신복호"
email: "gjrjr4545@gmail.com" # 요구되는 이메일 어노테이션
spec:
completions: 8
parallelism: 4
completionMode: Indexed
successPolicy:
rules:
- succeededCount: 6
backoffLimit: 2
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
app: sp-demo
spec:
restartPolicy: Never
securityContext:
seccompProfile:
type: RuntimeDefault
fsGroup: 1000
fsGroupChangePolicy: "OnRootMismatch"
containers:
- name: worker
image: alpine:3.20
imagePullPolicy: IfNotPresent
command: ["/scripts/worker.sh"]
env:
- name: MODE
value: "ok"
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
volumeMounts:
- name: scripts
mountPath: /scripts
readOnly: true
- name: tmp
mountPath: /tmp # RO 루트FS 대비 쓰기 가능한 tmp
volumes:
- name: scripts
configMap:
name: job-worker-scripts
defaultMode: 0755
- name: tmp
emptyDir: {}kubectl apply -f job-indexed-successpolicy.yamlkubectl -n job-lab get job indexed-successpolicy -o wide
kubectl -n job-lab get pods -l job-name=indexed-successpolicy -o wide
kubectl -n job-lab describe job indexed-successpolicy | sed -n '1,120p'
kubectl -n job-lab logs -l job-name=indexed-successpolicy --all-containers --prefix


실패 Job 코드 작성 (job-pfp-failjob-exit42.yaml)
# filename: job-pfp-failjob-exit42.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: pfp-failjob-exit42
namespace: job-lab
annotations:
owner: "신복호"
email: "owner@example.com" # ← 실제 이메일로 교체
spec:
completions: 8
parallelism: 3
completionMode: Indexed
backoffLimit: 3
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 300
# 특정 종료코드(42) → 즉시 Job 실패
podFailurePolicy:
rules:
- action: FailJob
onExitCodes:
containerName: worker
operator: In
values: [42]
template:
metadata:
labels:
app: pfp-failjob-exit42
spec:
restartPolicy: Never
securityContext:
seccompProfile:
type: RuntimeDefault
fsGroup: 1000
fsGroupChangePolicy: "OnRootMismatch"
containers:
- name: worker
image: alpine:3.20
imagePullPolicy: IfNotPresent
command: ["/scripts/worker.sh"]
env:
- name: MODE
value: "exit42"
- name: FAIL_INDEX
value: "3" # 3번 인덱스에서 exit 42 발생
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "100m" # ← CPU 요청 추가(린트 요구)
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
volumeMounts:
- name: scripts
mountPath: /scripts
readOnly: true
- name: tmp
mountPath: /tmp # RO 루트FS 대비 쓰기 가능한 tmp
volumes:
- name: scripts
configMap:
name: job-worker-scripts
defaultMode: 0755
- name: tmp
emptyDir: {}
kubectl apply -f job-pfp-failjob-exit42.yamlkubectl -n job-lab describe job pfp-failjob-exit42 | sed -n '1,200p'
# Conditions에 Failed, reason=PodFailurePolicy 유사 메시지가 찍힙니다.
kubectl -n job-lab get pods -l job-name=pfp-failjob-exit42
kubectl -n job-lab logs -l job-name=pfp-failjob-exit42 --all-containers --prefix
restartPolicy: Never + completionMode: Indexed가 전제.Job 코드 작성 (job-indexed-backoff-per-index.yaml)
# filename: job-indexed-backoff-per-index.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: indexed-backoff-per-index
namespace: job-lab
annotations:
owner: "신복호"
email: "owner@example.com" # <-- 요구된 이메일 어노테이션
spec:
completions: 8
parallelism: 4
completionMode: Indexed
backoffLimitPerIndex: 3
maxFailedIndexes: 1
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
app: indexed-backoff-per-index
spec:
restartPolicy: Never
securityContext:
seccompProfile:
type: RuntimeDefault
fsGroup: 1000
fsGroupChangePolicy: "OnRootMismatch"
containers:
- name: worker
image: alpine:3.20
imagePullPolicy: IfNotPresent
command: ["/scripts/worker.sh"]
env:
- name: MODE
value: "always_fail"
- name: FAIL_INDEX
value: "5" # 5번 인덱스는 늘 실패(3회 재시도 후 실패 인덱스로 집계)
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
volumeMounts:
- name: scripts
mountPath: /scripts # 실행 스크립트 (ConfigMap, read-only)
readOnly: true
- name: tmp
mountPath: /tmp # read-only rootfs 대비 쓰기 가능 tmp
volumes:
- name: scripts
configMap:
name: job-worker-scripts
defaultMode: 0755
- name: tmp
emptyDir: {} # /tmp 쓰기 가능 볼륨
배포
kubectl apply -f job-indexed-backoff-per-index.yaml
# 인덱스 5는 3회까지 재시도 후 '실패 인덱스'로 집계.
# maxFailedIndexes=1을 넘기면 잡이 실패합니다.
kubectl -n job-lab describe job indexed-backoff-per-index | sed -n '1,200p'
kubectl -n job-lab get pods -l job-name=indexed-backoff-per-index -o wide
kubectl -n job-lab logs -l job-name=indexed-backoff-per-index --all-containers --prefix
python:3.12-alpine 사용 + 메모리 limit을 작게 잡아 OOM을 유도합니다.코드 작성 (job-pfp-count-oom.yaml)
# filename: job-pfp-count-oom.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: pfp-count-oom
namespace: job-lab
annotations:
owner: "신복호"
email: "gjrjr4545@gmail.com" # ← 실제 이메일로 교체
spec:
completions: 4
parallelism: 2
completionMode: Indexed
backoffLimit: 2
activeDeadlineSeconds: 900
ttlSecondsAfterFinished: 300
# OOMKilled(137) → 실패 카운트 증가(Count) 후 backoff 전략 적용
podFailurePolicy:
rules:
- action: Count
onExitCodes:
containerName: py
operator: In
values: [137]
template:
metadata:
labels:
app: pfp-count-oom
spec:
restartPolicy: Never
securityContext:
seccompProfile:
type: RuntimeDefault
fsGroup: 1000
fsGroupChangePolicy: "OnRootMismatch"
containers:
- name: py
image: python:3.12-alpine
imagePullPolicy: IfNotPresent
command: ["python","-c"]
args:
- |
a=[]
# 일부러 메모리 사용을 늘려 OOMKilled 유도
while True:
a.append(bytearray(64*1024*1024))
env:
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "100m" # ← CPU 요청 추가(린트 요구)
memory: "64Mi"
limits:
cpu: "500m"
memory: "128Mi"
volumeMounts:
- name: tmp
mountPath: /tmp # RO 루트FS 대비 쓰기 가능한 tmp
volumes:
- name: tmp
emptyDir: {}
kubectl apply -f job-pfp-count-oom.yamlkubectl describe job pfp-count-oom -n job-lab | sed -n '1,200p'
kubectl get pods -n job-lab -l job-name=pfp-count-oom -o wide
kubectl describe pods pfp-count-oom-0-f6v59 -n job-lab


# 잡 진행 상태 실시간 확인
watch -n 2 kubectl -n job-lab get job,pod -o wide
# 잡 이벤트/컨디션 확인
kubectl -n job-lab describe job <JOB_NAME>
# 인덱스별 Pod와 어노테이션(인덱스) 확인
kubectl -n job-lab get pod -l job-name=<JOB_NAME> -o jsonpath='{range .items[*]}{.metadata.name}{" index="}{.metadata.annotations.batch\.kubernetes\.io/job-completion-index}{"\n"}{end}'
# 모든 파드 로그 보기
kubectl -n job-lab logs -l job-name=<JOB_NAME> --all-containers --prefix --tail=-1kubectl delete ns job-lab
안녕하세요. 글 잘 읽었습니다.
podReplacementPolicy가 TerminatingOrFailed 로 설정하면, pod가 terminating인 경우에도 새로운 pod를 생성하고, 이것이 기본동작인 것으로 이해했습니다.
job이 성공한 pod 개수를 확인할 수 있어야 할 텐데, pod가 terminating 상태여도 pod의 성공/실패 여부를 확인할 수 있는지 궁금합니다.