kubectl get cronjob을 쳤더니 LAST SCHEDULE이 <none>이거나, kubectl get jobs에 COMPLETIONS 0/1인 Job이 쌓여 있다면, 스케줄은 정의됐는데 컨테이너가 제대로 끝나지 못하고 있다는 뜻입니다. CronJob은 Job을 만들고, Job은 Pod를 만듭니다. 그래서 실패의 진짜 원인은 항상 맨 아래 Pod에 있습니다. 위에서부터 한 단계씩 내려가며 좁힙니다.
1단계 — 어느 계층에서 막혔는지 본다
# CronJob이 Job을 만들고 있나
kubectl get cronjob my-job
# 그 CronJob이 만든 Job들
kubectl get jobs --selector=job-name
# Job 상세 — 백오프·실패 횟수
kubectl describe job <job-name>
# 실제로 죽은 Pod 로그
kubectl logs job/<job-name>
LAST SCHEDULE이 갱신되는데 Job이 실패하면 앱 문제, LAST SCHEDULE이 아예 안 갱신되면 스케줄·권한 문제입니다.
2단계 — 증상별 원인
| 증상 | 흔한 원인 | 확인 |
|---|---|---|
LAST SCHEDULE: <none> | 스케줄 표현식 오류·suspend: true | kubectl get cronjob -o yaml |
Job은 생기는데 0/1 | 컨테이너 Exit 1·이미지 못 받음 | kubectl logs job/... |
BackoffLimitExceeded | 재시도 한도 초과 후 포기 | describe job의 이벤트 |
| Job이 안 끝나고 멈춤 | activeDeadlineSeconds 없음·무한 대기 | Pod 상태·로그 |
원인 6가지와 해결
① 스케줄 표현식 오류 — schedule은 표준 5필드 cron입니다. "*/5 * * * *"처럼 따옴표로 감싸지 않으면 YAML이 잘못 파싱합니다. 분 단위가 최소 단위라 초 단위 실행은 불가능합니다.
② 일시정지(suspend) — spec.suspend: true면 스케줄이 와도 Job을 안 만듭니다. 운영 중 꺼두고 잊는 경우가 많습니다. kubectl patch cronjob my-job -p '{"spec":{"suspend":false}}'로 풉니다.
③ 컨테이너가 비정상 종료 — 앱이 Exit 1로 죽으면 Job은 backoffLimit(기본 6)만큼 재시도 후 BackoffLimitExceeded로 실패합니다. kubectl logs job/<name>의 스택트레이스가 1차 단서입니다.
④ 이미지·시크릿 누락 — ImagePullBackOff면 태그 오타나 imagePullSecrets 누락입니다. describe pod의 이벤트에 사유가 찍힙니다.
⑤ 동시 실행 충돌 — concurrencyPolicy가 기본 Allow라 이전 Job이 안 끝났는데 다음 스케줄이 겹쳐 중복 실행됩니다. 배치 작업은 Forbid로 막습니다.
⑥ 무한 대기 — 외부 API 응답을 기다리며 Pod가 안 끝나는 경우. activeDeadlineSeconds로 상한을 줘서 강제 종료시킵니다.
체크리스트
kubectl get cronjob my-job # suspend·LAST SCHEDULE
kubectl get jobs # COMPLETIONS·실패 Job
kubectl describe job <job-name> # backoff·이벤트
kubectl logs job/<job-name> # 죽은 컨테이너 로그
이 4개면 스케줄 계층인지 앱 계층인지 바로 갈립니다.
CronJob을 직접 만들어 일부러 실패시키고 백오프·동시성을 관찰하는 실습은 쿠버네티스 트랙에서 해볼 수 있습니다 — 회원가입 없이 무료로.