Kubernetes Job·CronJob

왜 Job/CronJob 운영 설계가 중요한가

Kubernetes에서 배치 작업은 JobCronJob으로 관리합니다. Deployment와 달리 “실행 → 완료 → 정리”라는 생명주기를 가지기 때문에, 재시도·동시 실행·완료 후 정리 정책을 명시하지 않으면 장애가 조용히 누적됩니다. 이 글에서는 공식 문서에 명시된 스펙 필드를 기준으로, 실무에서 자주 놓치는 설정 세 가지—backoffLimit, concurrencyPolicy, ttlSecondsAfterFinished—를 파고듭니다.

Kubernetes Job 생명주기와 주요 제어 필드 도식
Kubernetes Job 생명주기: 재시도·동시 실행·TTL 제어 흐름

1. Job의 재시도 제어: backoffLimit과 activeDeadlineSeconds

backoffLimit — 실패 허용 횟수

Job의 .spec.backoffLimit은 Pod가 실패했을 때 최대 재시도 횟수를 지정합니다. 기본값은 6입니다. 재시도 간격은 지수 백오프(exponential back-off)로 10초, 20초, 40초… 최대 6분까지 증가합니다.

apiVersion: batch/v1
kind: Job
metadata:
  name: data-migration
spec:
  backoffLimit: 3          # 최대 3회 재시도 후 Failed
  template:
    spec:
      containers:
      - name: migrate
        image: myapp/migrate:v2.1
        command: ["python3", "migrate.py"]
      restartPolicy: Never   # OnFailure도 가능하지만 동작이 다름

restartPolicy에 따른 차이점:

  • restartPolicy: Never — Pod 실패 시 새 Pod를 생성합니다. 실패한 Pod는 남아 있어 로그를 확인할 수 있습니다.
  • restartPolicy: OnFailure — 같은 Pod 안에서 컨테이너를 재시작합니다. Pod가 하나만 유지되므로 리소스 효율적이지만, 이전 실행 로그가 덮어씌워질 수 있습니다.

공식 문서에 따르면, backoffLimit에 도달하면 Job 컨트롤러는 해당 Job을 Failed로 마킹합니다. 이 시점에 실행 중인 Pod가 있으면 종료(terminate)됩니다.

실무 팁: 데이터 마이그레이션처럼 멱등(idempotent)하지 않은 작업은 backoffLimit: 0으로 설정해 재시도 자체를 막고, 별도 알림으로 수동 개입하는 것이 안전합니다.

activeDeadlineSeconds — 전체 실행 시간 한도

.spec.activeDeadlineSeconds는 Job의 전체 실행 시간 상한을 초 단위로 지정합니다. backoffLimit과 독립적으로 동작하며, 이 시간이 경과하면 모든 Pod가 종료되고 Job은 reason: DeadlineExceeded로 실패합니다.

spec:
  backoffLimit: 3
  activeDeadlineSeconds: 600   # 10분 내에 끝나지 않으면 강제 종료

두 설정을 함께 사용하면 “최대 3번 재시도하되, 전체 10분을 넘기면 어떤 상황에서든 중단”이라는 예측 가능한 정책이 됩니다.

2. CronJob의 동시 실행 제어: concurrencyPolicy

CronJob은 .spec.concurrencyPolicy로 이전 실행이 아직 끝나지 않았을 때의 동작을 제어합니다. 공식 문서는 세 가지 옵션을 정의합니다:

정책 동작 적합한 시나리오
Allow (기본값) 동시에 여러 Job 실행 허용 독립적인 리포트 생성, 알림 발송
Forbid 이전 Job이 실행 중이면 새 실행을 건너뜀 DB 백업처럼 중복 실행이 위험한 작업
Replace 이전 Job을 삭제하고 새 Job을 시작 캐시 워밍처럼 최신 결과만 필요한 작업
apiVersion: batch/v1
kind: CronJob
metadata:
  name: db-backup
spec:
  schedule: "0 2 * * *"       # 매일 02:00
  concurrencyPolicy: Forbid   # 이전 백업이 끝나지 않았으면 건너뜀
  startingDeadlineSeconds: 300 # 5분 이내에 시작 못 하면 스킵
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 3600
      template:
        spec:
          containers:
          - name: backup
            image: myapp/db-backup:latest
            command: ["bash", "/scripts/backup.sh"]
          restartPolicy: OnFailure

startingDeadlineSeconds의 역할

.spec.startingDeadlineSeconds는 예약된 시간으로부터 얼마나 지연되어도 실행을 허용할지를 지정합니다. 공식 문서에 따르면, 이 시간이 지나면 해당 스케줄의 실행은 건너뜁니다. 또한, 이 기간 안에 100회 이상의 스케줄이 누락되면 CronJob 컨트롤러는 더 이상 해당 CronJob의 실행을 시도하지 않고 에러를 기록합니다.

실무 팁: Forbid 정책 사용 시 startingDeadlineSeconds를 반드시 함께 설정하세요. 설정하지 않으면, 이전 Job이 오래 걸릴 때 놓친 스케줄이 누적되어 100회 제한에 걸릴 수 있습니다.

3. 완료된 Job 자동 정리: ttlSecondsAfterFinished

Job이 완료(Complete 또는 Failed)된 후에도 API 서버에 오브젝트가 남아 있습니다. 장기간 운영하면 완료된 Job과 Pod가 수천 개까지 쌓여 etcd 성능과 kubectl get jobs 가독성에 영향을 줍니다.

.spec.ttlSecondsAfterFinished (Kubernetes v1.23부터 stable)는 Job 완료 후 자동 삭제까지의 대기 시간을 지정합니다. TTL-after-finished 컨트롤러가 이 값을 감시하고, 만료되면 해당 Job과 종속 Pod를 cascading deletion으로 삭제합니다.

spec:
  ttlSecondsAfterFinished: 86400   # 완료 후 24시간 뒤 삭제
  backoffLimit: 3
  template:
    spec:
      containers:
      - name: report
        image: myapp/report:v1
      restartPolicy: Never

주의사항 (공식 문서 Caveats):

  • 시간 왜곡(Time skew): 이 기능은 클러스터 노드 간 시간 동기화에 민감합니다. NTP가 정상 동작하는지 확인하세요.
  • TTL 연장: 이미 만료된 TTL을 나중에 늘려도, Kubernetes는 해당 Job의 보존을 보장하지 않습니다.
  • 0으로 설정하면 완료 즉시 삭제됩니다. 로그 확인이 필요하다면 최소 몇 시간의 여유를 두세요.

CronJob에서의 히스토리 관리와 TTL 조합

CronJob은 자체적으로 .spec.successfulJobsHistoryLimit(기본값 3)과 .spec.failedJobsHistoryLimit(기본값 1)을 가집니다. TTL과 히스토리 리밋은 독립적으로 동작하므로, 둘 다 설정하면 먼저 도달하는 조건이 삭제를 트리거합니다.

spec:
  schedule: "*/30 * * * *"
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      ttlSecondsAfterFinished: 172800  # 48시간
      backoffLimit: 2
      template:
        spec:
          containers:
          - name: sync
            image: myapp/sync:v3
          restartPolicy: Never

4. 실전 조합: 안전한 배치 운영 체크리스트

아래 체크리스트는 위에서 다룬 세 가지 설정을 실전 시나리오별로 조합한 것입니다.

시나리오 backoffLimit activeDeadline concurrencyPolicy TTL
DB 마이그레이션 (1회성) 0 1800s 604800 (7일)
야간 DB 백업 (매일) 2 3600s Forbid 172800 (48h)
캐시 워밍 (30분 간격) 1 900s Replace 7200 (2h)
리포트 생성 (매주) 3 7200s Allow 604800 (7일)
데이터 정합성 검증 (매시간) 1 1800s Forbid 86400 (24h)

핵심 원칙: 모든 Job에 activeDeadlineSeconds를 설정하세요. 설정하지 않으면 hang 상태의 Pod가 무한히 리소스를 점유할 수 있습니다.

5. 장애 디버깅: 자주 발생하는 문제와 원인

문제 1: CronJob이 실행되지 않음

  • concurrencyPolicy: Forbid + 이전 Job이 hang → 새 실행이 계속 건너뜀
  • 확인: kubectl get jobs --sort-by=.metadata.creationTimestamp로 이전 Job 상태 점검
  • 해결: activeDeadlineSeconds를 설정해 hang Job을 자동 종료

문제 2: 완료된 Job/Pod가 수천 개 누적

  • ttlSecondsAfterFinished 미설정 + 히스토리 리밋 기본값이 높은 경우
  • 확인: kubectl get jobs -A | wc -l
  • 해결: 기존 Job에도 TTL을 패치할 수 있습니다: kubectl patch job/old-job -p '{"spec":{"ttlSecondsAfterFinished":3600}}'

문제 3: Job이 backoffLimit에 도달했는데 알림이 없음

  • Job이 Failed로 마킹되지만, 별도 모니터링이 없으면 인지하기 어려움
  • 해결: Prometheus의 kube_job_status_failed 메트릭 + Alertmanager 룰 조합
# Prometheus alerting rule 예시
- alert: KubeJobFailed
  expr: kube_job_status_failed{job_name=~".+"} > 0
  for: 1m
  labels:
    severity: warning
  annotations:
    summary: "Job {{ $labels.job_name }}이(가) 실패했습니다"

마무리

Kubernetes의 Job/CronJob은 선언적 배치 관리의 핵심이지만, 기본값에 의존하면 재시도 폭주, 동시 실행 충돌, 오브젝트 누적이라는 세 가지 운영 함정에 빠집니다. backoffLimit+activeDeadlineSeconds로 재시도를 제한하고, concurrencyPolicy+startingDeadlineSeconds로 동시 실행을 통제하며, ttlSecondsAfterFinished로 정리를 자동화하면 배치 운영이 예측 가능해집니다.

참고 자료

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux