티스토리 뷰

728x90
반응형

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

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

또한 OS 외 Kubernetes Level의 RBAC 권한 관리 방안으로 보다 안전하게 명령어 레벨을 실행할 수 있도록 제한할 수 있다.

CronJob

먼저 CronJob에 대해 알아보자.

CronJob은 CronTab에 정의된 한 줄의 Scheduling과 같다고 볼 수 있다. CronTab과 같이 주어진 시간에 따라 주기적으로 동작한다.

 

# 참고

모든 크론잡 일정: 시간은 kube-controller-manager의 시간대를 기준으로 한다.

 

CronJob은 백업 실행 또는 이메일 전송과 같은 정기적이고 반복적인 작업을 만드는데 유용하다.

다음은 CronJob으로 실행한 busybox 컨테이너 예시이다.

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

해당 CronJob은 busybox Container에서 실행되며, date를 출력하고 echo Message를 출력하도록 Job이 구성되어 있다.

보다 상세한 설명은 아래에서 다루기로 한다.

[root@kubemaster yaml]# kubectl apply -f busybox-deployment.yaml 
cronjob.batch/hello created
[root@kubemaster yaml]#

위와 같이 cronjob을 적용하면 cronjob은 scheduling 된 주기마다 한번씩 Pod를 생성하고 Job을 실행하고 Completion하는 과정을 반복한다.

다음은 CronJob과 Pod의 상태 변화이다.

<CronJob>

[root@kubemaster yaml]# kubectl get cronjob -w
NAME    SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hello   */1 * * * *   False     0        <none>          36s
hello   */1 * * * *   False     1        7s              39s
hello   */1 * * * *   False     0        17s             49s
hello   */1 * * * *   False     1        7s              99s
hello   */1 * * * *   False     0        17s             109s
hello   */1 * * * *   False     1        7s              2m39s
hello   */1 * * * *   False     0        17s             2m49s
hello   */1 * * * *   False     1        7s              3m39s
hello   */1 * * * *   False     0        17s             3m49s
hello   */1 * * * *   False     1        8s              4m40s
hello   */1 * * * *   False     0        18s             4m50s
...
...
...

<Pod>

[root@kubemaster yaml]# kubectl get pod -w
NAME                     READY   STATUS    RESTARTS   AGE
hello-1601171280-tjfsr   0/1     Pending   0          0s
hello-1601171280-tjfsr   0/1     Pending   0          0s
hello-1601171280-tjfsr   0/1     ContainerCreating   0          0s
hello-1601171280-tjfsr   0/1     Completed           0          5s
hello-1601171340-k94pc   0/1     Pending             0          0s
hello-1601171340-k94pc   0/1     Pending             0          0s
hello-1601171340-k94pc   0/1     ContainerCreating   0          0s
hello-1601171340-k94pc   0/1     Completed           0          5s
hello-1601171400-dlrzj   0/1     Pending             0          0s
hello-1601171400-dlrzj   0/1     Pending             0          0s
hello-1601171400-dlrzj   0/1     ContainerCreating   0          0s
hello-1601171400-dlrzj   0/1     Completed           0          6s
hello-1601171460-9k2vd   0/1     Pending             0          0s
hello-1601171460-9k2vd   0/1     Pending             0          0s
hello-1601171460-9k2vd   0/1     ContainerCreating   0          0s
hello-1601171460-9k2vd   0/1     Completed           0          6s
hello-1601171280-tjfsr   0/1     Terminating         0          3m11s
hello-1601171280-tjfsr   0/1     Terminating         0          3m11s
hello-1601171520-8mmt2   0/1     Pending             0          0s
hello-1601171520-8mmt2   0/1     Pending             0          0s
hello-1601171520-8mmt2   0/1     ContainerCreating   0          0s
hello-1601171520-8mmt2   0/1     Completed           0          4s
hello-1601171340-k94pc   0/1     Terminating         0          3m11s
hello-1601171340-k94pc   0/1     Terminating         0          3m11s
...
...
...
[root@kubemaster yaml]# kubectl get pod
NAME                     READY   STATUS      RESTARTS   AGE
hello-1601171700-hll62   0/1     Completed   0          2m11s
hello-1601171760-svnkr   0/1     Completed   0          71s
hello-1601171820-pgxwt   0/1     Completed   0          11s
[root@kubemaster yaml]#

CronJob은 매 1분에 한번씩 Pod를 기동하며, Pod는 Container를 기동하여 Job을 처리하고 Completed 상태로 변경된 후 대기한다. Job Pod는 3개가 유지되며, 4개째 Pod가 처리가 완료되고 대기하면, 오래된 Pod부터 종료된다.

kubectl logs -f [pod_name]을 확인해 보면

[root@kubemaster yaml]# kubectl logs -f hello-1601172480-z58q9
Sun Sep 27 02:08:13 UTC 2020
Hello from the Kubernetes cluster
[root@kubemaster yaml]# kubectl logs -f hello-1601172540-psh78
Sun Sep 27 02:09:13 UTC 2020
Hello from the Kubernetes cluster
[root@kubemaster yaml]# kubectl logs -f hello-1601172600-q7wzp
Sun Sep 27 02:10:13 UTC 2020
Hello from the Kubernetes cluster
[root@kubemaster yaml]#

정확히 1분에 한번씩 현재 Date와 Echo Message를 출력하고 있는 것을 볼 수 있다.

CronJob Yaml 파일 분석

앞서 살펴본데로 CronJob을 구성하는 Yaml 파일은 다음과 같은 주요 필드를 갖는다.

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

[.spec.schedule]
.spec.schedule은 필수 필드이다. 이는 해당 Job이 생성되고 실행되는 스케줄 시간으로

0 * * * *

@hourly

와 같이 크론 형식의 문자열을 받아들인다.
값은 범위(range)와 함께 사용할 수 있다. 범위 뒤에 /<number> 를 지정하여 범위 내에서 숫자만큼의 값을 건너뛴다. 예를 들어, 시간 필드에 0-23/2 를 사용하여 매 2시간마다 명령 실행을 지정할 수 있다(V7 표준의 대안은 0,2,4,6,8,10,12,14,16,18,20,22 이다). 별표(asterisk) 뒤에 붙이는 스텝도 허용되며, "2시간마다"라고 지정하고 싶으면, 간단히 */2 를 사용하면 된다.
참고: 스케줄에서 물음표(?)는 별표 * 와 동일한 의미를 가지며, 주어진 필드에 대해 사용할 수 있는 모든 값을 나타낸다.
[.spec.jobTemplate] 
.spec.jobTemplate은 Job에 대해 정의하며, 필수 필드다. 단일 Job을 정의하는 것과 동일한 스키마를 갖는다.

apiVersion: batch/v1
kind: Job
metadata:
  name: hello
spec:
  template:
    spec:
      containers:
      - name: hello
        image: busybox
        args:
        - /bin/sh
        - -c
        - date; echo Hello from the Kubernetes cluster
      restartPolicy: OnFailure
  backoffLimit: 4

위는 Job의 예시이다. apiVersion Kind, metadata 등의 yaml meta 정보 외의 CronJob의 .spec.jobTemplate.spec.template은 Job의 .spec.template과 정확하게 일치한다.

[.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 실행으로 대체한다.
[.spec.suspend]
.spec.suspend 필드도 선택 사항이다. true 로 설정되면, 모든 후속 실행이 일시 정지된다. 이 설정은 이미 시작된 실행에는 적용되지 않는다. 기본값은 false이다.

CronJob & Job

CronJob을 구성할 때 유의해야 할 부분 중 하나로 다음을 기억해야 한다.

"CronJob은 SCHEDULING되는 시점에 Job을 생성하는 책임이 있고, Job은 그 Job이 대표하는 Pod를 관리하는 책임이 있다는 점이다."

즉 CronJob은 Job을 Job은 Pod를 정의한다고 볼 수 있다.

주의사항

CronJob은 Fail에 대한 정의가 구성되어 있지 않을 경우 다음과 같은 default 값을 갖는다.

apiVersion: v1
items:
- apiVersion: batch/v1beta1
  kind: CronJob
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"batch/v1beta1","kind":"CronJob","metadata":{"annotations":{},"name":"hello","namespace":"default"},"spec":{"jobTemplate":{"spec":{"template":{"spec":{"containers":[{"args":["/bin/sh","-c","date; echo Hello from the Kubernetes cluster"],"image":"busybox","name":"hello"}],"restartPolicy":"OnFailure"}}}},"schedule":"*/1 * * * *"}}
    creationTimestamp: "2020-09-27T01:47:28Z"
    name: hello
    namespace: default
    resourceVersion: "112946"
    selfLink: /apis/batch/v1beta1/namespaces/default/cronjobs/hello
    uid: db3ac0e1-cb85-41f6-91ba-f9c54abe7a5a
  spec:
    concurrencyPolicy: Allow
    failedJobsHistoryLimit: 1
    jobTemplate:
      metadata:
        creationTimestamp: null
      spec:
        template:
          metadata:
            creationTimestamp: null
          spec:
            containers:
            - args:
              - /bin/sh
              - -c
              - date; echo Hello from the Kubernetes cluster
              image: busybox
              imagePullPolicy: Always
              name: hello
              resources: {}
              terminationMessagePath: /dev/termination-log
              terminationMessagePolicy: File
            dnsPolicy: ClusterFirst
            restartPolicy: OnFailure
            schedulerName: default-scheduler
            securityContext: {}
            terminationGracePeriodSeconds: 30
    schedule: '*/1 * * * *'
    successfulJobsHistoryLimit: 3
    suspend: false
  status:
    lastScheduleTime: "2020-09-27T03:25:00Z"
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""

- startingDeadlineSeconds : N/A

- concurrencyPolicy : Allow

위 두가지 필드는 실제 CronJob의 실패를 진단하는 기준이 된다.

<기본 설정>

기본 값은 startingDeadlineSeconds가 구성되어 있지 않고 concurrencyPolicy가 Allow 상태이다.

CronJob Controller는 LAST SCHEDULE로부터 지금까지 얼마나 많이 Job이 누락되었는지 확인한다. 만약 100회 이상의 일정이 누락되었다면, 더이상 Job을 실행하지 않고 아래와 같은 에러 로그를 남긴다.

Cannot determine if job needs to be started. Too many missed start time (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew. 

<startingDeadlineSeconds: 200 설정 추가>

startingDeadlineSeconds 필드가 설정이 되면, 컨트롤러는 LAST SCHEDULE부터 지금까지를 기준으로 하지 않고 startingDeadlineSeconds 값을 기준으로 몇 개의 Job이 누락되었는지 카운팅한다.

(startingDeadlineSeconds 가 200 이면, 컨트롤러는 최근 200초 내 몇 개의 Job이 누락되었는지 카운팅한다.)

<예시>

기본 설정 상태에서 CronJob은 08:30:00 에 시작하여 매 분마다 새로운 Job을 실행하도록 설정이 되었고, startingDeadlineSeconds 값이 설정되어 있지 않는다고 가정해 보자.

만약 CronJob Controller가 08:29:00 부터 10:21:00 까지 장애가 발생했다면, 일정을 놓친 작업 수가 100개를 초과하여 더 이상 Job이 실행되지 않을 것이다.

반대로 CronJob이 08:30:00 부터 매 분 실행되는 일정으로 설정되고, startingDeadlineSeconds 이 200이라고 가정했을때, 동일한 장애가 발생했다면 (08:29:00 부터 10:21:00 까지), Job은 10:22:00 부터 재 시작될 것이다. 이 경우, 컨트롤러가 마지막 일정부터 지금까지가 아니라, 최근 200초 안에 얼마나 Job을 놓쳤는지 체크하기 때문이다. (3분 20초 이므로 여기서는 3번 놓쳤다고 체크함.)

이 두가지 조합을 기반으로 CronJob의 Fail 상태를 관리할 수 있다.

CronJob 활용

CronJob이 활용될 수 있는 경우는 다양하게 있다.

먼저 CronTab과 동일한 경우를 예로들 수 있다.

매일 새벽 3시에 처리되어야 하는 배치잡, 매 시간 로그 파일을 백업해야 하는 경우 등이 일반적인 CronTab의 활용 방법이었다.

두번째, Kubernetes Cluster의 일부로써 Pod 내 Container가 Kubernetes API를 호출하는 경우이다.

예를 들어 다음과 같은 경우가 있을 수 있다.

매 시간마다, Pod의 상태가 Restarting 되는 정상적이지 않은 Pod를 강제로 종료하는 API 호출을 실행한다거나, pod의 상태를 체크하기 위해 ping-pong을 날려보고 그 후속 작업을 실행하도록 한다거나, 매 시간마다 배포된 코드를 자동으로 개발환경에 배포해 준다거나 다양한 적용 방법이 있을 수 있다.

특히 하루 정기 배포 시간을 정해 두고 오전 1회 오후 1회 야간 1회 자동으로 배치 Job을 실행 시켜 배포가 수행되도록 하는 Job을 만드는 것은 일반적이지만, 효과적은 CronJob의 형태가 되겠다.

이후 기회가 된다면 CronJob Advence로 함께 구성해 보도록 하자.

728x90
반응형