K8s CronJob 운영이 어려운 이유
Kubernetes CronJob은 crontab처럼 간단해 보이지만, 분산 환경에서 동시 실행 제어, 실패 복구, 히스토리 관리, 리소스 제한 등 운영 이슈가 복잡합니다. 기본 설정으로 배포하면 Job이 중복 실행되거나, 실패한 Pod가 무한히 쌓이거나, 데드라인을 넘겨 아예 실행되지 않는 문제가 발생합니다.
이 글에서는 concurrencyPolicy 동작 원리, startingDeadlineSeconds 미실행 방지, 실패 복구 전략, 리소스 제한과 모니터링, 그리고 프로덕션 운영 패턴까지 심층적으로 다룹니다.
CronJob 스펙 완전 분석
프로덕션용 CronJob의 모든 필드를 이해해야 합니다:
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-report
namespace: batch
spec:
schedule: "0 2 * * *" # 매일 02:00 UTC
timeZone: "Asia/Seoul" # K8s 1.27+ 타임존 지정
# 동시 실행 정책
concurrencyPolicy: Forbid # Allow | Forbid | Replace
# 스케줄 놓침 허용 시간 (초)
startingDeadlineSeconds: 300 # 5분 내 시작 못하면 스킵
# 히스토리 보관
successfulJobsHistoryLimit: 3 # 성공 Job 3개 보관
failedJobsHistoryLimit: 5 # 실패 Job 5개 보관
# CronJob 일시 중지
suspend: false
jobTemplate:
spec:
# Job 레벨 설정
backoffLimit: 3 # 최대 재시도 3회
activeDeadlineSeconds: 3600 # 1시간 내 완료 안 되면 강제 종료
ttlSecondsAfterFinished: 86400 # 완료 후 24시간 뒤 자동 삭제
template:
spec:
restartPolicy: OnFailure # Never | OnFailure
containers:
- name: report
image: myapp/report:v1.2
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
concurrencyPolicy 동작 비교
이전 Job이 아직 실행 중일 때 다음 스케줄 시점이 되면 어떻게 할지 결정합니다:
| 정책 | 동작 | 적합한 경우 |
|---|---|---|
Allow |
이전 Job과 동시 실행 허용 | 독립적인 작업 (로그 수집, 메트릭) |
Forbid |
이전 Job 실행 중이면 새 Job 스킵 | DB 마이그레이션, 정산 (중복 실행 위험) |
Replace |
이전 Job 종료 후 새 Job 시작 | 최신 데이터만 필요한 리포트 |
# 시나리오: 매 5분 실행, 이전 Job이 7분 소요
# Allow → 5분 시점에 두 번째 Job 시작, 두 개 동시 실행
# Forbid → 5분 시점에 새 Job 스킵, 10분 시점에 시작
# Replace → 5분 시점에 이전 Job 종료, 새 Job 시작
# 대부분의 배치 작업에는 Forbid가 안전합니다
startingDeadlineSeconds 이해
이 설정은 스케줄 시점을 놓쳤을 때 얼마나 기다릴지 결정합니다. 노드 장애, 컨트롤러 재시작 등으로 스케줄이 밀릴 수 있습니다:
# startingDeadlineSeconds: 200
# schedule: "*/5 * * * *" (매 5분)
# 시나리오: 컨트롤러가 10:00~10:08 동안 다운
# - 10:00 스케줄 놓침
# - 10:05 스케줄 놓침
# - 10:08 컨트롤러 복구
# → 10:05 기준 200초(~3분 20초) 이내이므로 10:05 Job 실행
# → 10:00 기준 480초 → 200초 초과 → 10:00 Job은 스킵
# 설정하지 않으면?
# → 100개 이상 놓친 스케줄이 있으면 Job을 전혀 실행하지 않고
# "Cannot determine if job needs to be started" 에러 발생!
권장: 반드시 startingDeadlineSeconds를 설정하세요. 스케줄 간격의 절반 정도가 적당합니다.
실패 복구 전략
backoffLimit과 restartPolicy의 조합으로 실패 복구를 제어합니다:
# 패턴 1: 컨테이너 재시작 (일시적 에러)
spec:
backoffLimit: 3
template:
spec:
restartPolicy: OnFailure # 같은 Pod 내에서 컨테이너 재시작
# 재시작 간격: 10s → 20s → 40s → 80s ... (지수 백오프, 최대 5분)
# 패턴 2: 새 Pod 생성 (환경 문제 의심)
spec:
backoffLimit: 3
template:
spec:
restartPolicy: Never # 실패 시 새 Pod 생성
# 다른 노드에 스케줄링될 수 있어 노드 장애 우회 가능
# 패턴 3: 절대 재시도 안 함 (멱등성 보장 불가)
spec:
backoffLimit: 0 # 실패 즉시 Job 실패 처리
template:
spec:
restartPolicy: Never
Init Container로 선행 조건 검증
배치 작업 시작 전 DB 연결, 외부 API 상태 등을 확인합니다:
apiVersion: batch/v1
kind: CronJob
metadata:
name: data-sync
spec:
schedule: "0 */6 * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
initContainers:
# DB 연결 확인
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c',
'until nc -z mysql.database 3306; do
echo "waiting for DB...";
sleep 5;
done']
resources:
requests:
cpu: 50m
memory: 32Mi
# 외부 API 상태 확인
- name: check-api
image: curlimages/curl:8.5.0
command: ['sh', '-c',
'curl -sf https://api.external.com/health || exit 1']
resources:
requests:
cpu: 50m
memory: 32Mi
containers:
- name: sync
image: myapp/data-sync:v2.0
envFrom:
- secretRef:
name: data-sync-secrets
resources:
requests:
cpu: "1"
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
CronJob 모니터링
CronJob 실패를 빠르게 감지하려면 Prometheus 메트릭과 알림을 설정합니다:
# 유용한 kubectl 명령어
# 최근 CronJob 실행 상태
kubectl get cronjobs -n batch
kubectl get jobs -n batch --sort-by=.metadata.creationTimestamp
# 실패한 Job 확인
kubectl get jobs -n batch --field-selector status.successful=0
# Job의 Pod 로그 확인
kubectl logs job/daily-report-28450320 -n batch
# Prometheus 알림 규칙 (PrometheusRule)
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: cronjob-alerts
spec:
groups:
- name: cronjob
rules:
# CronJob 마지막 실행 실패
- alert: CronJobFailed
expr: |
kube_job_status_failed{namespace="batch"} > 0
for: 1m
labels:
severity: warning
annotations:
summary: "CronJob {{ $labels.job_name }} failed"
# CronJob이 예상 시간 내 실행되지 않음
- alert: CronJobNotScheduled
expr: |
time() - kube_cronjob_next_schedule_time > 3600
for: 10m
labels:
severity: critical
annotations:
summary: "CronJob {{ $labels.cronjob }} missed schedule"
# Job 실행 시간 초과
- alert: CronJobTooLong
expr: |
kube_job_status_active{namespace="batch"} == 1
and
time() - kube_job_status_start_time{namespace="batch"} > 7200
labels:
severity: warning
annotations:
summary: "Job {{ $labels.job_name }} running > 2 hours"
Sidecar로 결과 알림
Job 완료 후 Slack/Discord로 결과를 알려주는 패턴:
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-settlement
spec:
schedule: "0 3 * * *"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 600
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 7200
template:
spec:
restartPolicy: Never
shareProcessNamespace: true # 프로세스 상태 공유
containers:
- name: settlement
image: myapp/settlement:v3.1
command: ['/bin/sh', '-c']
args:
- |
/app/settle.sh 2>&1 | tee /shared/output.log
echo $? > /shared/exit_code
volumeMounts:
- name: shared
mountPath: /shared
- name: notifier
image: curlimages/curl:8.5.0
command: ['/bin/sh', '-c']
args:
- |
# 메인 컨테이너 종료 대기
while [ ! -f /shared/exit_code ]; do sleep 5; done
EXIT_CODE=$(cat /shared/exit_code)
RESULT=$(tail -5 /shared/output.log)
if [ "$EXIT_CODE" = "0" ]; then
MSG="✅ Settlement completedn$RESULT"
else
MSG="❌ Settlement FAILED (exit=$EXIT_CODE)n$RESULT"
fi
curl -X POST "$SLACK_WEBHOOK"
-H 'Content-Type: application/json'
-d "{"text":"$MSG"}"
env:
- name: SLACK_WEBHOOK
valueFrom:
secretKeyRef:
name: slack-webhook
key: url
volumeMounts:
- name: shared
mountPath: /shared
volumes:
- name: shared
emptyDir: {}
프로덕션 체크리스트
| 항목 | 권장 설정 |
|---|---|
concurrencyPolicy |
Forbid (대부분의 배치) |
startingDeadlineSeconds |
스케줄 간격의 50% (반드시 설정) |
activeDeadlineSeconds |
예상 소요 시간의 2~3배 |
backoffLimit |
2~3 (멱등성 보장 시) |
ttlSecondsAfterFinished |
86400 (24시간) — 디버깅 여유 |
| 리소스 requests/limits | 반드시 설정 (다른 워크로드 영향 방지) |
| 모니터링 알림 | 실패, 미실행, 장시간 실행 3가지 |
마무리
K8s CronJob은 단순한 스케줄러가 아니라 분산 환경의 배치 실행 엔진입니다. concurrencyPolicy: Forbid로 중복을 방지하고, startingDeadlineSeconds로 미실행을 감지하며, Prometheus 알림으로 실패를 모니터링하면 안정적인 배치 인프라를 구축할 수 있습니다.
관련 글로 K8s Job·CronJob 배치 처리와 K8s Indexed Job 병렬 배치도 함께 참고하세요.