K8s CronJob 프로덕션 운영 전략

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를 설정하세요. 스케줄 간격의 절반 정도가 적당합니다.

실패 복구 전략

backoffLimitrestartPolicy의 조합으로 실패 복구를 제어합니다:

# 패턴 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 병렬 배치도 함께 참고하세요.

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