본문 바로가기

회고

쿠버네티스 워크로드 CronJob 작성시 유의할 점 - 기초가 중요한 이유

새벽 1시 1분

책상 위에 스마트폰의 스크린이 들어오고 진동이 울립니다.

 

진동은 한번 울리고 멈추는가 싶더니 1시 2분, 3분이 지날 때마다 진동을 넘어서 책상을 "딱딱" 때리는 소리가 나기 시작했습니다.

 

멍한 상태로 스마트폰을 열어보니 10분 동안 알림이 50개 이상 쌓여있었습니다.

"Pod not healthy"

 

순간 잠이 확 달아나고 심장은 두근거리기 시작합니다.

 

'다행이다.'

운영 중인 Pod 배치 문제가 아닌 CronJob이 실패하여 Pod 상태가 정상적으로 종료되지 않았기 때문에 발생한 문제였습니다.

 

실패한 CronJob은 오전 1시, Backend에 업로드된 파일을  외부로 Rync 하여 백업하는 작업이었습니다.

 

에러 로그를 확인해 봅니다.

2025-09-25 01:01:22.100	OpenSSL version mismatch. Built against 3050003f, you have 30500010
2025-09-25 01:01:22.100	rsync: connection unexpectedly closed (0 bytes received so far) [sender]
2025-09-25 01:01:22.100	rsync error: unexplained error (code 255) at io.c(232) [sender=3.4.1]

 

확인 결과, rsync가 외부 SSH 서버로 원격 동기화를 수행하는 과정에서, CronJob으로 생성된 Pod의 컨테이너 이미지에 포함된 OpenSSL 라이브러리 버전rsync 실행 바이너리가 빌드될 당시 링크된 OpenSSL 버전이 서로 달라 OpenSSL version mismatch 오류가 발생한 것이 원인이었습니다.

이로 인해 rsync가 SSH 세션을 초기화하는 단계에서 실패하며, 데이터 전송이 시작되기도 전에 프로세스가 비정상 종료(connection unexpectedly closed)되는 현상이 발생하게 된 것입니다.

 

다음은 장애 알림이 발생한 CronJob의 내용입니다.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: rsync-backup
  namespace: default
spec:
  timeZone: "Asia/Seoul"
  schedule: "0 1 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: rsync
            image: alpine
            command:
            - /bin/sh
            - -c
            - |
              apk add --no-cache rsync openssh-client

              mkdir -p /home/backup/.ssh
              cp /ssh-config/known_hosts /home/backup/.ssh/known_hosts && cp /ssh-config/id_rsa /home/backup/.ssh/id_rsa
              chmod 600 /home/backup/.ssh/id_rsa
              chmod 644 /home/backup/.ssh/known_hosts
              chmod 700 /home/backup/.ssh

              rsync -avz --delete -e "ssh -p 50000" /data/ backup@10.20.0.100:/home/backup/backend/uploads
            volumeMounts:
            - name: storage
              mountPath: /data
            - name: ssh-key
              mountPath: /ssh-config
          volumes:
          - name: ssh-key
            secret:
              secretName: ssh-key-secret
              defaultMode: 0600
          - name: storage
            persistentVolumeClaim:
              claimName: erp-pvc
          restartPolicy: Never
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1

 

 

문제가 된 부분은 다음과 같이 image는 alpine 최신 버전을 사용하고 있음을 확인하였습니다.

containers:
  - name: rsync
    image: alpine # 최신 이미지 사용
    command:
    - /bin/sh
    - -c
    - |
      apk add --no-cache rsync openssh-client

      mkdir -p /home/backup/.ssh
      cp /ssh-config/known_hosts /home/backup/.ssh/known_hosts && cp /ssh-config/id_rsa /home/backup/.ssh/id_rsa
      chmod 600 /home/backup/.ssh/id_rsa
      chmod 644 /home/backup/.ssh/known_hosts
      chmod 700 /home/backup/.ssh

      rsync -avz --delete -e "ssh -p 50000" /data/ backup@10.20.0.100:/home/backup/backend/uploads

 

그 결과 imagePullPolicy는 자동으로 always 처리되어 CronJob으로 Pod가 생성될 때마다 alpine 이미지를 매번 pull 하게 됩니다.

이후 command 진행에 따라 매번 rsync, openssh-client를 매번 설치하게 됩니다.

 

즉 신규 배포된 alpine 이미지를 pull 완료 후 Pod가 생성된 후  최신 rsync, openssh-client를 설치하면서 문제가 발생하였던 것입니다.

 

alpine 이미지 버전을 지정하고, imagePullPolicyIfNotPresent로 지정하여 버전을 고정하여 해결했습니다.

containers:
  - name: rsync
    image: alpine:3.20 # 버전 고정
    imagePullPolicy: IfNotPresent # 이미지가 없는 경우만 이미지 pull
    # command는 동일

 

Pod 생성 후 기동시 command를 통해 apk를 add 하여 설치하게 되면, 해당 패키지의 버전이 바뀔 수 있는 문제가 발생할 수 있으므로

별도의 image로 만들거나, 버전을 고정하여 설치하는 것이 문제 재발의 여지를 좀 더 낮출 수 있습니다.

 

 

하지만 왜 작업이 실패하였는데 계속 시도하였던 것일까요?

 

비밀은 Job 속성 중 backoffLimit에 있었습니다.

 

다시 CronJob 내용을 확인해 보면 spec.jobTemplate.spec.backoffLimit가 없음을 알 수 있었습니다.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: rsync-backup
  namespace: default
spec:
  timeZone: "Asia/Seoul"
  schedule: "0 1 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: rsync
            image: alpine
            # command: 생략
            # volumeMounts: 생략
          # volumes: 생략
          restartPolicy: Never
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1

 

 

CronJob을 단순하게 Pod를 띄워 쉘스크립트를 작동하게 하는 것으로 오해하였고, restartPolicyNever로 지정하면 Pod가 성공 또는 실패 후 바로 종료되는 것으로 생각했습니다.

 

그 결과 backoffLimit 기본값인 6번 동안 CronJob을 반복 재실행하였던 것이고, 계속 실패하면서 알림이 발생할 수밖에 없는 구조였습니다.

 

최종적으로 다음과 같이 backoffLimit를 설정하여 만일 실패하게 된다면 재시도 1번 (실패하였을 때 총횟수로 보면 2번) 하도록 수정하여 Pod 배포 및 기동 실패가 반복되지 않도록 하였습니다. 

apiVersion: batch/v1
kind: CronJob
metadata:
  name: rsync-backup
  namespace: default
spec:
  timeZone: "Asia/Seoul"
  schedule: "0 1 * * *"
  jobTemplate:
    spec:
      backoffLimit: 1 # Job이 실패 할 경우 1번 재시도
      # template: 생략
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1

 

배운 점 및 CronJob 작성 시 유의할 점

 

1. CronJob의 일관성을 유지하기 위해 이미지 버전 고정, imagePullPolicyIfNotPresent, 좀 더 가능하다면 전용 이미지 생성.

2. CronJob 또는 Job 실패 시 재시도는 backoffLimit 설정을 통해 제어하고, restartPolicy는 생성되는 Pod에 해당하는 점.

3. CronJob의 timeZone이 없는 경우 UTC 시간으로 스케줄이 시작되는 것.

 

쿠버네티스 공식 홈페이지의 , 크론잡을 통해 더 자세한 내용을 확인할 수 있습니다.