CronJob(ScheduledJob)


このエントリーは Kubernetes Advent Calendar 2016の4日目の記事です。
CronJobs について、キャッチアップしたことを書いて行きたいと思います。

from. http://kubernetes.io/docs/user-guide/cron-jobs/

## CronJob(ScheduledJob) とは?

時間ベースのジョブを管理するもの。
所謂Cronみたいに特定の一回だったり、繰り返し実行することができるものです。

## ユースケース

- メール送信
- バッチ処理
- 定期的に解析
- 夜間にバックアップ処理


## Kubernetes のバージョンによって名前が違う

- 1.4 Scheduledjob
- 1.5 CronJob


## 利用するための前提条件

> You need a working Kubernetes cluster at version >= 1.4 (for ScheduledJob), >= 1.5 (for CronJobs), with batch/v2alpha1 API turned on by passing --runtime-config=batch/v2alpha1 while bringing up the API server (see Turn on or off an API version for your cluster for more). You cannot use Cron Jobs on a hosted Kubernetes provider that has disabled alpha resources.

バージョンで名前が違うのは前述とおりです。
ここでは更にkubernetes の API Server を立ち上げる際に `--runtime-config=batch/v2alpha1` を付けることが必要です。


- 例: launch-k8s-on-aws.sh

```bash
#!/bin/bash

# Turned on v2aplpha1

export KUBE_RUNTIME_CONFIG="batch/v2alpha1=true"

# Target aws config

export KUBERNETES_PROVIDER=aws
export AWS_DEFAULT_PROFILE=default

export KUBE_AWS_ZONE=ap-northeast-1c
export AWS_S3_REGION=ap-northeast-1

export NODE_SIZE=c4.large
export MASTER_SIZE=c4.large
export AWS_S3_BUCKET=kubernetes-145

# Kubernetes config

export NUM_NODES=5
export KUBE_AWS_INSTANCE_PREFIX=k8s-145

# Disk size

export MASTER_DISK_SIZE=50
export MASTER_ROOT_DISK_SIZE=50
export NODE_ROOT_DISK_SIZE=50

# Download Kubernetes and automatically set up a default cluster

wget -q -O - https://get.k8s.io | bash
```

## 実際に manifest file を書いて実行してみよう

- hello world 的な物

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

- cronjob 作成

```bash
$ kubectl create -f examples/cronjob.yaml 
cronjob "hello" created
```

- cronjob 確認

```bash
$ kubectl get cronjob hello
NAME      SCHEDULE      SUSPEND   ACTIVE    LAST-SCHEDULE
hello     */1 * * * *   False     0         Mon, 29 Aug 2016 14:34:00 -0700
```

- job 確認

```bash
$ kubectl get job
NAME                         DESIRED   SUCCESSFUL   AGE
hello-1705740100             1         1            32d
```

- `kubectl describe job` するとさらに情報がわかる。

```bash
$ kubectl describe job hello-1705740100 
Name:		hello-1705740100
Namespace:	default
Image(s):	busybox
Selector:	controller-uid=93a273e1-9fdf-11e6-b40b-06daaf3c2469
Parallelism:	1
Completions:	1
Start Time:	Tue, 01 Nov 2016 12:02:08 +0900
Labels:		<none>
Pods Statuses:	0 Running / 1 Succeeded / 0 Failed
No volumes.
No events.
```

- `Selector` がわかるので、 Pod がわかる


```bash
$ kubectl get po -l controller-uid=93a273e1-9fdf-11e6-b40b-06daaf3c2469 --show-all
NAME                               READY     STATUS    RESTARTS   AGE
job-task-latest-1553958718-mg5r8   0/1       Error     0          45s
job-task-latest-1553958718-r65iv   0/1       Error     0          41s
```


- cronjob 削除

```bash
$ kubectl delete cronjob hello
cronjob "hello" deleted
```

- job 削除

```bash
$ kubectl get jobs
NAME               DESIRED   SUCCESSFUL   AGE
hello-1201907962   1         1            11m
hello-1202039034   1         1            8m
...

$ kubectl delete jobs hello-1201907962 hello-1202039034 ...
job "hello-1201907962" deleted
job "hello-1202039034" deleted
...
```

ref. https://github.com/kubernetes/kubernetes/blob/master/docs/proposals/scheduledjob.md


## オプション

### Cronjob

- `.spec.schedule`: cron format でスケジュールを指定する
- `.spec.jobTemplate`: Job を定義する
- `.spec.startingDeadlineSeconds`: CronJob で何かしら実行されなかった時にいつまで実行可能か期限を決める。
- `.spec.concurrencyPolicy`: 並列して実行するポリシーを決めるもの。前の job と被って実行されてもいいのか気を付ける。
	- `Allow`(default): 並列して実行を許可
	- `Forbid`: 前回がまだ終了していない場合、次回の実行をスキップする
	- `Replace`: 現在実行中のジョブをキャンセルし、新しいジョブに置き換えます
- `.spec.suspend`: trueに設定すると、次回以降に中断され実行されない。既に開始されているものはそのまま実行される。

### job

- `.spec.jobTemplate.spec.activeDeadlineSeconds`: 失敗する際にデッドラインを決める(デフォルトは無期限だから失敗するコンテナを書くと、ずっとrestartされる)

## job の考え方

ref. [Handling Pod and Container Failures](http://kubernetes.io/docs/user-guide/jobs/)

> A Container in a Pod may fail for a number of reasons, such as because the process in it exited with a non-zero exit code, or the Container was killed for exceeding a memory limit, etc. If this happens, and the .spec.template.containers[].restartPolicy = "OnFailure", then the Pod stays on the node, but the Container is re-run. Therefore, your program needs to handle the case when it is restarted locally, or else specify .spec.template.containers[].restartPolicy = "Never". See pods-states for more information on restartPolicy.

> An entire Pod can also fail, for a number of reasons, such as when the pod is kicked off the node (node is upgraded, rebooted, deleted, etc.), or if a container of the Pod fails and the .spec.template.containers[].restartPolicy = "Never". When a Pod fails, then the Job controller starts a new Pod. Therefore, your program needs to handle the case when it is restarted in a new pod. In particular, it needs to handle temporary files, locks, incomplete output and the like caused by previous runs.

> Note that even if you specify .spec.parallelism = 1 and .spec.completions = 1 and .spec.template.containers[].restartPolicy = "Never", the same program may sometimes be started twice.

> If you do specify .spec.parallelism and .spec.completions both greater than 1, then there may be multiple pods running at once. Therefore, your pods must also be tolerant of concurrency.



- 基本的に、Jobは何回実行しても結果が同じになることを期待している設計になっている。
- `.spec.template.containers[].restartPolicy = "Never"` はあくまでも Pod 内の話であり、Job 事態が完了していないのであれば、新しく Pod を作ってプログラムを実行する
- `.spec.parallelism = 1` と `.spec.completions = 1` と `.spec.template.containers[].restartPolicy = "Never"` を指定しても、同じプログラムが2回起動される可能性がある


## こんな使い方が出来る

### kubectl をクラスタ内部で実行

例えば `kubectl set` コマンドを使ってコンテナのイメージを替える

```yaml
apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  name: replace-task-latest
  labels:
    name: app-server-scheduledjob
    role: kubectl
  namespace: app-server
spec:
  schedule: "0 2,6,10,14,18,22 * * *" # every 4 hours (shifted by 2 hours)
  startingDeadlineSeconds: 30
  concurrencyPolicy: "Forbid"
  suspend: false
  jobTemplate:
    metadata:
      name: app-server-scheduledjob
      labels:
        name: app-server-scheduledjob
        role: kubectl
    spec:
      template:
        spec:
          containers:
          - name: app-server-scheduledjob
            image: koudaiii/kubectl
            command:
              - "sh"
              - "-c"
              - >
                echo "kubectl set image deployment/app-server app-server=app-server:latest --namespace=app-server --record";
                kubectl set image deployment/app-server app-server=app-server:latest --namespace=app-server --record
          restartPolicy: Never
```

### manifest file を更新する

- kubernetes/app-server.yaml

```yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    metadata:
      name: pi
    spec:
      containers:
      - name: pi
        image: perl
        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
      restartPolicy: Never
```

- 書いた manifest file を configmap に置く

```bash
kubectl create configmap app-server --from-file=kubernetes/app-server.yaml
```

- 定期的に実行させる

```yaml
apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  name: job-task-latest
  labels:
    name: app-server-scheduledjob
    role: kubectl
spec:
  schedule: "*/5 * * * *"
  startingDeadlineSeconds: 30
  concurrencyPolicy: "Forbid"
  suspend: false
  jobTemplate:
    metadata:
      name: app-server-scheduledjob
      labels:
        name: app-server-scheduledjob
        role: kubectl
    spec:
      template:
        spec:
          containers:
          - name: app-server-scheduledjob
            image: koudaiii/kubectl
            command: ["kubectl","replace","-f","tmp/app-server.yaml"]
            volumeMounts:
              - name: deployment
                mountPath: /tmp
                readOnly: true
          restartPolicy: Never
          volumes:
            - name: deployment
              configMap:
                name: app-server
```


## 気をつける点

ここからは、実際に使ってみてハマったものをまとめました。

### 実行はあくまでも UTC?

```
NAME                                 SCHEDULE     SUSPEND   ACTIVE    LAST-SCHEDULE
people--ios-event-appsflyer-screen   0 16 * * *   False     0         Sat, 03 Dec 2016 01:00:00 +0900
```

LASTをみると日本時間で表示されるが Scheduler が実行されるのはUTCになってる。

もしかすると、 get.k8s.io で作る場合はUTCなのかもしれない。get.k8s.ioで特に指定するところが GitHub の kubernetes/cluster/* からはなく、ここは自分で Cluster を作ってみないとわからない感じです。


### 二回目の `kubectl apply` で失敗する

- ref https://github.com/kubernetes/kubernetes/issues/35149

```
# scheduledjob.yaml
# From http://kubernetes.io/docs/user-guide/scheduled-jobs/#creating-a-scheduled-job
apiVersion: batch/v2alpha1
kind: ScheduledJob
metadata:
  name: hello
spec:
  schedule: 0/1 * * * ?
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure
```

- 1回目は成功

```
$ kubectl apply -f scheduledjob.yaml 
scheduledjob "hello" created
```

- 2回目に失敗する。 

```
$ kubectl apply -f scheduledjob.yaml
error: error when applying patch:

to:
&{0xc8200bd740 0xc8202f8ee0 sandbox hello manifests.yaml XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

Travis CI 経由で Job をまとめて `kubectl apply -f ./dir/` とかやろうとした際にこの不具合にあたった。

### Job が already exists で時々実行されない

- ref. https://github.com/kubernetes/kubernetes/pull/36883

```
Error creating job: jobs.batch "es-cleanup-1629652801" already exists
```

getTimeHash の実装が原因で hourly とか設定すると時々名前が被ってJobが失敗する
getTimeHash(scheduledTime)

| Date | Epoch | Hash | Hits |
|-----------------|------------|------------|---|
| Nov 14 00:00:00 | 1479081600 | **4175643772** | 1 |
| Nov 14 00:05:00 | 1479081900 | **4209853567** | 1 |
| Nov 14 00:10:00 | 1479082200 | **4141499513** | 1 |
| Nov 14 00:15:00 | 1479082500 | 4175709308 | 1 |
| Nov 14 00:20:00 | 1479082800 | 4209919103 | 1 |
| Nov 14 00:25:00 | 1479083100 | 4244128898 | 1 |
| Nov 14 00:30:00 | 1479083400 | 4072621171 | 1 |
| Nov 14 00:35:00 | 1479083700 | 4106830966 | 1 |
| Nov 14 00:40:00 | 1479084000 | 4141040761 | 1 |
| Nov 14 00:45:00 | 1479084300 | 4072686707 | 1 |
| Nov 14 00:50:00 | 1479084600 | 4106896502 | 1 |
| Nov 14 00:55:00 | 1479084900 | 4141106297 | 1 |
| Nov 14 01:00:00 | 1479085200 | 4072752243 | 1 |
| Nov 14 01:05:00 | 1479085500 | 4106962038 | 1 |
| Nov 14 01:10:00 | 1479085800 | 4141171833 | 1 |
| Nov 14 01:15:00 | 1479086100 | 4175381628 | 1 |
| Nov 14 01:20:00 | 1479086400 | 4107027574 | 1 |
| Nov 14 01:25:00 | 1479086700 | 4141237369 | 1 |
| Nov 14 01:30:00 | 1479087000 | 4175447164 | 1 |
| Nov 14 01:35:00 | 1479087300 | 4107093110 | 1 |
| Nov 14 01:40:00 | 1479087600 | 4141302905 | 1 |
| Nov 14 01:45:00 | 1479087900 | 4175512700 | 1 |
| Nov 14 01:50:00 | 1479088200 | 4107158646 | 1 |
| Nov 14 01:55:00 | 1479088500 | 4141368441 | 1 |
| Nov 14 02:00:00 | 1479088800 | 4175578236 | 1 |
| Nov 14 02:05:00 | 1479089100 | 4209788031 | 1 |
| Nov 14 02:10:00 | 1479089400 | 4141433977 | 1 |
| Nov 14 02:15:00 | 1479089700 | **4175643772** | 2 |
| Nov 14 02:20:00 | 1479090000 | **4209853567** | 2 |
| Nov 14 02:25:00 | 1479090300 | **4141499513** | 2 |


- https://github.com/kubernetes/kubernetes/blob/539165d899021064ceb98ef508756d7482f30035/pkg/controller/scheduledjob/utils.go#L188

```go
	// We want job names for a given nominal start time to have a deterministic name to avoid the same job being created twice
	name := fmt.Sprintf("%s-%d", sj.Name, getTimeHash(scheduledTime))
```


- https://github.com/kubernetes/kubernetes/pull/36812

Unix Epoch Time を返すように変更されている。次のリリースで治りそう。

## まとめ
	
実際に使ってみて、クラスタリング上で job が実行されるメリットは大きいです。

スケジュールを実行するため専用のサーバーを作ったりしていると思いますが、その必要がなくなりより効率的に全体のリソースを使えるようになるからです。

アプリケーションエンジニアの技術選定の幅がより広がるので、Cronjobは今後も追っていきたいと思います。