[Kubernetes] Cronjob 구성하기

안녕하세요? 정리하는 개발자 워니즈 입니다. 이번시간에는 k8s에서의 주기적 실행 요청인 cronjob에 대해서 구성을 해보도록 하겠습니다.

필자가 재직하는 회사에서는 여러개의 개발팀들이 있습니다. 개발팀에서는 batch성 작업을 하는데, 이부분을 기존에는 Jenkins를 통해서 기동을 하거나, 소스내에 직접 scheduling을 걸어서 수행하였습니다.

이러한 부분들을 k8s에서 제공해주는 cronjob을 통해서 cron schedule을 적용하고, pod로 관리되기 때문에 모니터링이라던지 로그시스템도 기존에 구축해놓은 그대로를 사용할 수 있기 때문에 적용하기로 했습니다.

운이 좋게도 k8s 1.21 버전부터 cronjob이 GA가 되어있고, 이를 적용할 수 있었습니다.

1. cronjob이란 무엇인가요?

Kubernetes에는 Linux / Unix Operation System에 존재하는 CronTab을 구현한 CronJob이 존재한다.

CronJob은 반복적이고 Scheduling 된 업무를 동작하게 하는 방법 중 하나로 CronTab이 항상 OS Level의 Configuration으로 남아 있는것과 달리 휴발성 있는 컨테이너에서 Job을 실행하고 종료하는 방식으로 보다 안전한 관리 방식을 제공한다.

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

위의 예시는 매 1분마다 command 수행을 하게 됩니다. pod를 생성하여 작업하고나서 종료되는 cycle을 갖고 있습니다.

2. cronjob helm chart 구성

보통 spring framework로 구성이 되어있고 1개의 어플리케이션 내에서 여러개의 batch성 작업을 구성하게 됩니다. 그러다보니 고민이 생겼습니다.

초기에 구성을 해뒀던것은 1개의 helm chart에 1개의 cronjob을 수행하게 했었습니다. 왜냐하면 batch app이 늘어날것이라고 예상을 했지만, 1개의 app에서 argument를 달리주어 여러개의 작업을 수행하도록 구성이 되었던 것입니다.

그래서 필자는 하나의 헬름차트 내에서 다수의 cronjob을 생성하도록 구성해야 했습니다.

#values file 예시
...
crons:
- name: cronjob
  list:
  - name: "test1"
    command: test1
    schedule: "*/5 * * * *"
    suspend: false
  - name: "test2"
    command: test2
    schedule: "0 * * * *"
    suspend: false
  - name: "test3-weekday"
    command: test3-weekday
    schedule: "0/10 8-20 * * 1-5"
    suspend: false
  - name: "test3-weekend"
    command: test3-weekend
    schedule: "0 8-21/2 * * 0,6"
    suspend: false
...

values 파일은 위와 같이 crons 하위로 list를 만들어서 여러개의 설정이 한번에 들어가도록 구성을 했습니다.

{{- range $cronjob := .Values.crons }}
{{- range $key, $value := $cronjob.list }}
apiVersion: cronjobber.hidde.co/v1alpha1
kind: TZCronJob
metadata:
  name: {{ $.Values.phase }}-{{ $value.name }}
  namespace: {{ $.Values.namespace }}
  labels:
    service.phase: {{ $.Values.phase }}
    service.name: {{ $.Values.namespace }}
    project.name: {{ $.Values.projectName }}
spec:
  schedule: {{ quote $value.schedule }}
  timezone: "Asia/Seoul"
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 3
  suspend: {{ $value.suspend }}
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            service.phase: {{ $.Values.phase }}
            service.name: {{ $.Values.namespace }}
            project.name: {{ $.Values.projectName }}
            deploy.version: {{ $.Values.deployImgVersion }}
          annotations:
            sidecar.istio.io/inject: {{ $.Values.istioInject | quote }}
            sidecar.istio.io/gracefulshutdown: {{ $.Values.istioGracefulshutdown | quote }}
            sidecar.istio.io/rewriteAppHTTPProbers: {{ $.Values.istioRewriteAppHTTPProbers | quote }}
        spec:
          affinity:
            nodeAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
                nodeSelectorTerms:
                - matchExpressions:
                  - key: networkType
                    operator: In
                    values:
                    - publicip-assignment
          containers:
          - name: {{ $.Values.phase }}-{{ $value.name }}
            image: {{ $.Values.dockerImgName }}:{{ $.Values.deployImgVersion }}
            resources:
              requests:
                memory: {{ $.Values.minMem }}
                cpu: {{ $.Values.minCpu }}
              limits:
                memory: {{ $.Values.maxMem }}
                cpu: {{ $.Values.maxCpu }}
            env:
            {{- range $key, $val := $.Values.extraEnvs }}
            - name: {{ $key }}
              value: "{{ $val }}"
            {{- end }}
            - name: JAVA_OPTS
              value:
                -Xms{{ $.Values.jvmXms }}
                -Xmx{{ $.Values.jvmXmx }}
                -Dspring.profiles.active={{ $.Values.phase }}
                -Dpinpoint.applicationName={{ $.Values.phase }}-dosi-payment-cronjob
            - name: BATCH_OPTS
              value: {{ $value.command }}
            - name: TZ
              value: Asia/Seoul
          restartPolicy: OnFailure
---
{{- end}}
{{- end}}

실제 cronjob.yaml 파일은 위와 같습니다. 앞서 values 파일에서 설정한 crons 정보를 기반으로 반복적으로 cron형태를 생성하게 됩니다. 그렇게 되면, 1개의 helm chart에서 다수의 cronjob을 생성하는것과 동일하게 구성이 됩니다.

3. cronjob timezone 이슈 해결

cronjob 구성을 마치고 나서, 배포를 수행했고 정상적으로 cron에 맞추어 작업이 되는지를 확인했습니다.

  - name: "test3-weekday"
    command: test3-weekday
    schedule: "0/10 8-20 * * 1-5"
    suspend: false
  - name: "test3-weekend"
    command: test3-weekend
    schedule: "0 8-21/2 * * 0,6"
    suspend: false

특히 위와 같이 주중, 주말에 실행이 되어야 하는 것처럼 주중 8-20시 사이 10분단위로 수행하도록 구성이 되었지만 이상하게도 +9시간씩의 차이가 발생을 했습니다. (UTC+9)인것을 확인하고 cronjob에 TIME ZONE까지 명시적으로 넣었지만 변경되는 것은 없었습니다.

확인을 해보니, 실제 cronjob이 수행이 되기 위해서는 kube-controller의 timzezone에 맞추어 진행이 되는데 이를 변경할수는 없어서 추가적인 cronjob conroller를 설치하기로 했습니다.

cronjobber

#설치
# Install CustomResourceDefinition
$ kubectl apply -f https://raw.githubusercontent.com/hiddeco/cronjobber/master/deploy/crd.yaml
# Setup service account and RBAC
$ kubectl apply -f https://raw.githubusercontent.com/hiddeco/cronjobber/master/deploy/rbac.yaml
# Deploy Cronjobber (using the timezone db from the node)
$ kubectl apply -f https://raw.githubusercontent.com/hiddeco/cronjobber/master/deploy/deploy.yaml
# Deploy Cronjobber (using the updatetz sidecar)
$ kubectl apply -f https://raw.githubusercontent.com/hiddeco/cronjobber/master/deploy/deploy-updatetz.yaml

설치는 굉장히 간다하고, 위의 내용을 설치한뒤, cronjob.yaml의 **timezone: “Asia/Seoul” **옵션을 부여하게 되면 해당 타임존으로 변경이 수행됩니다.

4. Cronjob option 값 조정

필자는 default를 중요시 하기때문에 별다른 설정을 변경하지는 않았고, 개발팀에서 필요로 하는 부분에 대한 설정만을 진행했습니다.

  schedule: {{ quote $value.schedule }}
  timezone: "Asia/Seoul"
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 3
  ...
        restartPolicy: OnFailure
  • 스케쥴에 대한 설정
  • 성공 이력 5건
  • 실패 이력 3건
  • 재실행 정책 : 실패시

위와 같이 설정을 수행했고, 추가적인 옵션으 아래와 같습니다.

[.spec.startingDeadlineSeconds]
.spec.startingDeadlineSeconds는 선택 필드이다. 어떤 이유로든 스케줄된 시간을 놓친 경우 Job의 시작 기한을 초 단위로 나타낸다. 이 필드를 지정하지 않으면, Job을 실행하는 기한이 없다.

[.spec.concurrencyPolicy]
.spec.concurrencyPolicy 필드도 선택 사항이다. CronJob에 의해 생성된 Job의 동시 실행을 처리하는 방법을 지정한다.
- Allow(기본값): CronJob은 동시에 실행되는 잡을 허용한다.
- Forbid: CronJob은 동시 실행을 허용하지 않는다. 새로운 Job을 실행할 시간이고 이전 Job 실행이 아직 완료되지 않은 경우, 크론 Job은 새로운 Job 실행을 건너뛴다.
- Replace: 새로운 Job을 실행할 시간이고 이전 잡 실행이 아직 완료되지 않은 경우, 크론 Job은 현재 실행 중인 Job 실행을 새로운 Job 실행으로 대체한다.

5. 마치며…

이번시간에는 cronjob 구성을 통해서 기존의 jenkins를 통해서 수행하거나 직접 소스상에 스케쥴링을 거는 부분들을 모두 k8s의 장점을 활용할 수 있도록 구성을 했다는 점에서 좋았던 것 같습니다.

cronjob의 pod도 모두 메트릭 수집의 대상이기 때문에 성공, 실패 이력에 대해서 대시보드로 표현이 가능하고 alert도 보내줄 수 있게 되었습니다. 나중에는 이러한 부분들까지 모두 구성을 하여 해당 batch에 대해서 notification구성이 될 수 있도록 진행해보겠습니다.

참고

https://kubernetes.io/ko/docs/concepts/workloads/controllers/cron-jobs/
https://arisu1000.tistory.com/27837
https://waspro.tistory.com/644

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다