0. Contents

1. Overview

Periodic Job 패턴의 위치

  • Job → 딱 한 번, 끝날 때까지 확실히 실행
  • CronJob → Job을 정해진 시간마다 반복 실행 ← 이번 챕터!

Batch Job 패턴의 확장

  • Periodic Job 패턴은 Batch Job 패턴을 확장한 것이다
  • 시간이라는 차원을 추가한다
    • 특정 시각 또는 주기라는 시간적 이벤트(temporal event) 에 의해 작업 실행이 트리거된다

한 줄 요약

Job = “이 작업을 실행해” CronJob = “이 작업을 언제언제 실행해”


2. Problem

실시간이 대세지만 스케줄링은 여전히 필요하다

실시간·이벤트 기반 트렌드

  • 요즘 개발 트렌드는 실시간·이벤트 기반 통신이 주류다
    • REST API(HTTP)
    • Kafka/RabbitMQ(경량 메시징)
  • 그러나 그 트렌드와 무관하게, 정해진 시간에 자동으로 반복 실행되는 작업의 필요성은 사라지지 않는다

전형적인 예시

분류예시
시스템 유지보수매일 새벽 DB 백업, 오래된 로그 정리·아카이빙
비즈니스 로직매주 뉴스레터 발송, 매달 정산 처리
시스템 연동파일 전송을 통한 B2B 연동, DB 폴링을 통한 애플리케이션 연동

기존 방식들과 각각의 문제점

1. Cron / 전용 스케줄링 소프트웨어

  • 리눅스에 원래 있던 cron은 그 서버 한 대에 종속된다
    • 서버가 죽으면 스케줄도 끝이다(단일 장애점)
  • 전문 스케줄링 소프트웨어는 단순한 용도에 비해 비용이 과하다

2. 앱 안에 스케줄러 직접 구현 (Quartz, Spring Batch 등)

  • 그래서 개발자들이 스케줄링 + 비즈니스 로직을 직접 구현하기 시작했다
  • 하지만 스케줄러가 앱 안에 있으니 앱 전체를 고가용성으로 만들어야 한다
  • 여러 인스턴스를 띄우면 동시에 여러 곳에서 실행될 수 있다
    • 그걸 막으려면 리더 선출 같은 복잡한 분산 시스템 문제를 해결해야 한다

결과: 문제보다 큰 해결책

  • 하루에 파일 몇 개 복사하는 단순한 작업 하나 때문에
    • 여러 노드 + 분산 리더 선출 + Failover 로직이 필요해진다
  • → 문제의 크기에 비해 해결책이 너무 과하다

CronJob 등장 배경

  • 어떻게 짜든 “하나만 실행되게” + “죽어도 살아나게” 를 동시에 만족시키기가 너무 힘들다
  • 이것이 Kubernetes CronJob이 등장한 배경이다

3. Solution: CronJob

CronJob은 Job 위에 시간 스케줄만 얹은 것

Pod실행단위Job완료보장CronJob시간스케줄
Pod 위에 Job, Job 위에 CronJob

별도 설치 없는 기본 리소스

  • CronJob은 Kubernetes가 기본으로 제공하는 리소스로, 별도 설치 없이 그냥 쓴다
  • Cron이라는 이름은 리눅스 crontab에서 따왔다
    • 표현식 문법(* * * * *)도 그대로 빌려썼다
  • 실제 동작은 완전히 다르다 — 클러스터 위에서 동작한다

알람시계 비유

  • 리눅스 Cron = 개인 알람시계 (서버 한 대)
  • Kubernetes CronJob = 알람시계 + 대신 실행해주는 팀 전체 (클러스터)

crontab 한 줄 = CronJob 인스턴스 하나

  • CronJob 인스턴스는 Unix crontab 한 줄과 같다고 볼 수 있다
  • 이를 Kubernetes 리소스로 만들었다
    • 시간 관리는 플랫폼이 담당한다
    • 개발자는 “뭘 실행할지"만 구현하면 된다

Example 8-1. CronJob 명세

apiVersion: batch/v1   # CronJob도 Job과 마찬가지로 batch API 그룹 소속
kind: CronJob
metadata:
  name: random-generator
spec:
  schedule: "*/3 * * * *"       # 3분마다 실행 (분 시 일 월 요일 순서의 Cron 표현식)
  jobTemplate:                  # 이 아래는 통째로 "일반 Job의 spec"과 동일한 템플릿
    spec:
      template:                 # Job의 template과 마찬가지로 Pod spec을 감싸는 한 겹
        spec:
          containers:
          - image: k8spatterns/random-generator:1.0
            name: random-generator
            command: [ "java", "-cp", "/", "RandomRunner", "/numbers.txt", "10000" ]
            # 이 컨테이너가 exit code 0으로 끝나야 해당 회차 Job이 Completed 처리됨
          restartPolicy: OnFailure   # Pod 레벨 정책 — Job(및 CronJob)은 Always 불가

구조 한눈에 보기

CronJobschedule: "*/3 * * * *"jobTemplateJobPodContainer실제 작업← 언제 실행?← 뭘 실행?
schedule 은 언제, jobTemplate 은 무엇을

Job을 찍어내는 공장

  • jobTemplate 안은 일반 Job spec과 동일하다
  • 즉 CronJob은 실행 시간마다 Job을 하나씩 새로 생성하는 공장이다
CronJob(하나)Job03분 실행Job06분 실행Job09분 실행Job12분 실행
CronJob 은 실행 시간마다 Job 을 하나씩 찍어 내는 공장

노드는 직접 골라야 하나?

노드 선택: 자동

  • 지가 알아서 찾는다
  • 실행 시간이 되면 Control Plane 안의 CronJob 컨트롤러(kube-controller-manager)가 Job을 만든다
  • 그 Job이 만든 Pod는 스케줄러(kube-scheduler)가 “어느 노드가 여유있나” 자동으로 골라서 띄워준다

실행 흐름

  1. CronJob 컨트롤러 → “새벽 2시 됐다”, Job 생성 → Job이 Pod 생성
  2. 스케줄러(kube-scheduler) → “Node 2가 여유있네”
  3. Node 2에서 자동 실행

조건으로 노드 제한

  • 직접 고르고 싶다면 조건을 걸 수 있다
nodeSelector:
  disktype: ssd    # SSD 달린 노드에서만 실행

기본 방식

  • 기본은 자동, 필요하면 조건 걸어서 제한하는 방식이다

서버가 죽어도 실행되나?

여러 노드로 이루어진 클러스터

  • 된다
  • Kubernetes는 여러 노드로 이루어진 클러스터이기 때문이다
Control Plane두뇌 (스케줄링 담당)Node 1죽음Node 2Pod여기서 자동 실행Node 3
Node 1 이 죽어도 Control Plane 이 Node 2 에서 실행한다

노드 장애 시 동작

  • CronJob 실행 시간이 되면 Control Plane이 명령한다
  • Node 1이 죽어있으면 Node 2에서 실행한다
  • 클러스터가 살아있는 한 실행은 보장된다
  • 기존 Cron이 단일 장애점이었던 문제를 플랫폼 차원에서 해결한다

4. CronJob 전용 필드

시간 관련 추가 필드

  • Job spec 외에, CronJob은 시간 관련 추가 필드들을 가진다

.spec.schedule

schedule: "0 * * * *"   # 매 시간 정각마다
schedule: "*/3 * * * *"  # 3분마다
schedule: "0 2 * * *"   # 매일 새벽 2시

표현식 문법

  • crontab 표현식을 그대로 사용한다

.spec.startingDeadlineSeconds

늦은 실행 허용 시간

  • 예정된 시간에 Job을 시작하지 못했을 때, 몇 초 안에는 늦게라도 실행할 것인가를 지정한다
    • 예: 컨트롤러가 잠시 멈춰 있었거나, Forbid 때문에 앞 Job이 끝나길 기다린 경우
    • Job을 제때 만들었는지만 본다 — Pod가 Pending으로 늦게 뜨는 건 해당 없다
오전 9시 실행 예정, startingDeadlineSeconds: 60 설정늦게라도 실행 (60초)9시 (예정)9시 01분시나리오 A컨트롤러 중단, 9시에 Job 못 만듦01분 안에 복구 → Job 생성시나리오 B9시 01분 넘어서 복구이번 회차 스킵
9시 예정 + startingDeadlineSeconds: 60 → 9시 01분까지만 늦은 실행 허용

늦으면 의미 없는 작업

  • 어떤 작업은 늦게 실행하면 의미가 없다
  • 오전 9시 주가 데이터 수집이 10시에 실행되면 이미 낡은 데이터다
  • 이럴 때 차라리 이번 회차를 건너뛰는 게 낫다

.spec.concurrencyPolicy

겹치는 실행의 처리

  • 이전 Job이 아직 실행 중인데 다음 실행 시간이 됐을 때 어떻게 할 것인지 지정한다

예시 상황

  • 3분마다 실행되는 CronJob인데 작업이 5분 걸린다면?
    • 3분·6분·9분에 실행 시간이 오고, 3분에 시작한 Job A는 8분에 끝난다
    • 아래 그림의 Forbid는 스킵한 B가 늦게 시작되지 않는다고 본다(startingDeadlineSeconds가 짧을 때) — 기한이 없으면 A가 끝난 8분에 B가 늦게 시작될 수 있다
옵션동작
Allow (기본값)이전 Job 실행 중이어도 새 Job 시작 → 동시에 쌓일 수 있음
Forbid이전 Job이 끝날 때까지 다음 실행 스킵
Replace이전 Job 강제 종료 후 새 Job 시작
3분6분9분결과AllowJob A 시작Job A 실행중Job B도 시작(동시 실행)Job A 완료(8분)Job B 실행중Job C도 시작2개씩 겹쳐 실행 ⚠ForbidJob A 시작Job A 실행중Job B 스킵Job A 완료(8분)Job C 시작안전하게 순서 보장ReplaceJob A 시작Job A 강제종료Job B 시작Job B 강제종료Job C 시작항상 최신 Job만 실행새 Job 시작실행 중완료스킵강제 종료
같은 상황, 세 가지 정책: 겹치거나, 건너뛰거나, 갈아 끼우거나

정책 선택 기준

정책상황
Allow작업이 독립적이고 겹쳐도 상관없을 때
ForbidDB 백업처럼 겹치면 안 될 때
Replace항상 최신 데이터로만 작업해야 할 때

주의: 같은 CronJob 안에서만 적용

  • 동시성 정책은 같은 CronJob 안에서만 적용된다
  • 서로 다른 CronJob끼리는 무조건 동시 실행이 허용된다

.spec.suspend

일시 정지 스위치

  • CronJob을 일시 정지한다
spec:
  suspend: true   # 이후 실행 예약 중단

영향 범위

  • true로 설정하면 이후 실행이 예약되지 않는다
  • 이미 실행 중인 Job에는 영향 없다
  • 점검이나 임시 중단이 필요할 때 유용하다

.spec.successfulJobsHistoryLimit / .spec.failedJobsHistoryLimit

Job 기록 보관 개수

  • 시간마다 Job이 하나씩 생성되므로 방치하면 계속 쌓인다
  • 성공/실패한 Job 기록을 몇 개나 보관할지 지정한다
successfulJobsHistoryLimit: 3   # 성공한 Job 3개만 보관
failedJobsHistoryLimit: 3       # 실패한 Job 3개만 보관

보관 개수 제한의 목적

  • 감사(auditing) 목적으로 기록은 남긴다
  • 자원 낭비를 막기 위해 개수를 제한한다

전체 필드 정리

필드역할
schedule언제 실행할지 (Cron 표현식)
startingDeadlineSeconds늦으면 얼마까지 허용할지
concurrencyPolicy동시 실행을 어떻게 처리할지
suspend일시 정지
successfulJobsHistoryLimit성공 기록 보관 수
failedJobsHistoryLimit실패 기록 보관 수

5. 실전 예제: 매일 새벽 DB 백업

apiVersion: batch/v1
kind: CronJob
metadata:
  name: db-backup
spec:
  schedule: "0 2 * * *"           # 매일 새벽 2시 (트래픽 적은 시간)
  concurrencyPolicy: Forbid       # 이전 백업이 안 끝났으면 이번 회차는 스킵 (겹치면 데이터 꼬일 수 있음)
  successfulJobsHistoryLimit: 3   # 성공 기록 3개까지만 남기고 이후는 자동 정리
  failedJobsHistoryLimit: 3       # 실패 기록도 3개까지만 (원인 추적용으로만 보관)
  jobTemplate:                    # 실행될 때마다 여기 정의대로 Job이 새로 생성됨
    spec:
      template:
        spec:
          containers:
          - name: db-backup
            image: postgres:14   # pg_dump 클라이언트가 포함된 이미지 (DB 서버와 별개)
            command:
            - /bin/sh
            - -c
            - |
              # 파일명에 날짜를 박아 매일 다른 파일로 저장 (덮어쓰기 방지)
              pg_dump -h $DB_HOST -U $DB_USER $DB_NAME > /backup/backup-$(date +%Y%m%d).sql
            env:
            - name: DB_HOST
              value: "postgres-service"   # 같은 클러스터 내 Service 이름으로 DNS 접근
            - name: DB_USER
              value: "admin"
            - name: DB_NAME
              value: "mydb"
            - name: PGPASSWORD              # pg_dump가 참조하는 표준 환경변수명 (임의로 못 바꿈)
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: password      # 비밀번호를 평문(value)이 아닌 Secret 참조로 주입
            volumeMounts:
            - name: backup-storage
              mountPath: /backup    # 위 pg_dump 출력 경로와 반드시 일치해야 함
          restartPolicy: OnFailure   # 백업 실패 시 컨테이너만 재시작 (Pod는 유지)
          volumes:
          - name: backup-storage
            persistentVolumeClaim:
              claimName: backup-pvc  # 컨테이너/Pod가 사라져도 백업 파일은 PV에 남음

외부 스토리지 업로드

  • 실제로는 백업 파일을 S3 같은 외부 스토리지로 올리는 명령어를 추가하는 게 일반적이다
pg_dump ... > /tmp/backup.sql && aws s3 cp /tmp/backup.sql s3://my-bucket/

6. 실패 처리: 누가 뭘 담당하나

DB 연결이 끊겼을 때

비즈니스 로직의 견고함

  • Kubernetes가 다 해주는 것처럼 보여도, 비즈니스 로직의 견고함은 개발자 책임이다
#!/bin/bash
MAX_RETRY=3
COUNT=0

until pg_dump -h $DB_HOST -U $DB_USER $DB_NAME > /backup/backup.sql
do
  COUNT=$((COUNT+1))
  if [ $COUNT -eq $MAX_RETRY ]; then
    echo "백업 실패 - 재시도 횟수 초과"
    exit 1   # 0이 아닌 코드로 종료 → restartPolicy: OnFailure라 컨테이너 재시작, backoffLimit(기본 6)만큼 재시도해도 실패하면 Job이 실패로 기록
  fi
  echo "$COUNT 번째 실패, 10초 후 재시도"
  sleep 10   # DB 일시 장애(재기동, 네트워크 순단)를 감안한 대기
done

방어선 두 겹

  • 스크립트가 1차 방어선, Kubernetes가 2차 방어선이다
1차 방어선: 스크립트직접 구현 · 연결 끊겼을 때 재시도pg_dump실패10초 후 재시도sleep 10MAX_RETRY=3까지 반복재시도 횟수 초과exit 1죽으면2차 방어선: Kubernetes스크립트 자체가 죽으면 재시작restartPolicy: OnFailure컨테이너 재시작
연결 끊김은 스크립트가 재시도하고, 스크립트가 죽으면 Kubernetes 가 재시작한다
역할담당
연결 끊겼을 때 재시도스크립트 (직접 구현)
스크립트 자체가 죽으면 재시작Kubernetes (restartPolicy: OnFailure)

반드시 고려해야 할 예외 상황들

직접 처리할 예외 케이스

  • CronJob 컨테이너를 구현할 때 아래 케이스들을 직접 처리해야 한다
예외 상황설명
duplicate runs같은 작업이 중복 실행됨
no runs예정된 실행이 아예 안 됨
parallel runs동시에 여러 개 실행됨
cancellations실행 도중 취소됨

7. Job vs CronJob 선택 기준

유일한 기준

  • 기준은 단 하나다

반복 실행이 필요하냐 아니냐

실행 횟수별 선택

  • 한 번 → Job
  • 반복 → CronJob
JobCronJob
실행 방식수동 트리거 또는 1회시간 스케줄에 따라 자동 반복
적합한 경우DB 마이그레이션, 일괄 데이터 처리DB 백업, 리포트 발송, 로그 정리

8. Discussion

단순한 CronJob, 강력한 조합

  • CronJob 자체는 단순하다
    • Job에 시간 스케줄을 얹은 것이다
  • 그런데 다른 Kubernetes 기능들과 조합하면 강력한 Job 스케줄링 시스템이 된다
    • Pod
    • 컨테이너 리소스 격리
    • Automated Placement
    • Health Probe

핵심: 관심사의 분리

예전: 앱 안에 스케줄러 직접 구현개발자 몫스케줄링고가용성Failover리더 선출현재: Kubernetes CronJob플랫폼이 담당스케줄링고가용성Failover개발자비즈니스 로직만 집중
스케줄링 · 고가용성 · Failover 가 개발자 몫에서 플랫폼 몫으로

플랫폼 차원 스케줄링의 이점

  • 스케줄링이 앱 밖에서, 플랫폼 차원에서 처리되기 때문에 얻는 이점
    • 고가용성: 노드가 죽어도 클러스터가 다른 노드에서 실행
    • 리소스 효율: 실행할 때만 Pod 생성, 끝나면 삭제
    • 정책 기반 배치: 어느 노드에서 실행할지 플랫폼이 결정

시간적 차원 전용 도구

  • CronJob은 시간적 차원이 필요한 작업에만 쓰는 특수한 도구다
  • 범용은 아니다
  • 하지만 Kubernetes 기능들이 서로 위에 쌓이며 발전하는 방식을 잘 보여주는 좋은 예시이기도 하다

기능이 쌓이는 구조

  • Pod
    • Job (완료 보장)
      • CronJob (시간 스케줄)

CronJob이 해결해주는 문제들이 원래 복잡했던 것이지, CronJob 자체가 복잡한 게 아니다. 그 복잡한 걸 단순하게 만들어준 게 핵심이다.