エラー時にリトライさせないJobによるPodの1ショット実行［backoffLimit］

# Jobとは

常駐するアプリケーションでなく、単発で実行して実行完了後はそのまま終了するような用途にはJobを使用する。
ここではJobで起動したPodがエラーで停止した場合も、自動再実行せずにそのまま終了させたい場合の設定についての調査とまとめ。

以下の環境で確認

- Minikube v0.35.0 (Kubernetes v1.13.4)
- MiniShift v1.32.0 (OKD v3.11.0)

※ Jobは`restartPolicy`は`Never`前提とする

- 2019.04.02 精度について追記

# TL;DR

以下の設定を`spec`直下に追加してJobを作成する。

```
  backoffLimit: 0
```

指定がない場合はデフォルト6として、リトライ制御(Podの再作成)が行われる。

# Jobの作成と実行

## YAML

```yaml:job-sample-valid.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: perl-sample-job
spec:
  template:
    spec:
      containers:
      - name: perl-sample-job
        image: perl
        command: ["perl",  "-e", 'sleep 5; print "hello\n"; exit 0']
      restartPolicy: Never
```

## Jobリソースの作成

OpenShiftであれば`oc create -f <yaml-file>`

```
PS C:\Users\zaki\src\containers\job> kubectl apply -f job-sample-valid.yaml
job.batch/perl-sample-job created
PS C:\Users\zaki\src\containers\job> kubectl get job
NAME              COMPLETIONS   DURATION   AGE
perl-sample-job   0/1           3s         3s
PS C:\Users\zaki\src\containers\job> kubectl get pod
NAME                    READY   STATUS    RESTARTS   AGE
perl-sample-job-xrrmz   1/1     Running   0          6s
```

実行完了

```
PS C:\Users\zaki\src\containers\job> kubectl get pod
NAME                    READY   STATUS      RESTARTS   AGE
perl-sample-job-xrrmz   0/1     Completed   0          10s
```

## 実行ログ

STDOUTへの出力は常駐型の普通のPodと同様に、PodやJobに対して`logs`コマンドを使用すれば参照できる。

```
PS C:\Users\zaki\src\containers\job> kubectl logs pod/perl-sample-job-xrrmz
hello
```

```
PS C:\Users\zaki\src\containers\job> kubectl logs job/perl-sample-job
hello
```

## 終了ステータス

コンテナ内のプログラムの終了ステータスは、`describe`やYAML形式でPodの状態を出力すれば確認できる

```
PS C:\Users\zaki\src\containers\job> kubectl describe pod perl-sample-job
:
:
Containers:
  perl-sample-job:
:
:
    State:          Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Thu, 28 Mar 2019 21:10:21 +0900
      Finished:     Thu, 28 Mar 2019 21:10:26 +0900
```

# エラー時の動作

`restartPolicy: Never`が指定されているPodはエラー時にそのまま停止(STATUSがErrorとなる)するが、Jobから再度Podが起動される。

## エラーになるJobサンプル

```yaml:job-sample-invalid.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: perl-sample-errorjob
spec:
  template:
    spec:
      containers:
      - name: perl-sample-errorjob
        image: perl
        command: ["perl",  "-e", 'sleep 5; print "hello\n"; exit 1']
      restartPolicy: Never
```

```
PS C:\Users\zaki\src\containers\job> kubectl get pod -w
NAME                              READY   STATUS    RESTARTS   AGE
hello-minikube-6fd785d459-2xq6j   1/1     Running   1          112m
perl-sample-errorjob-bfd6w        0/1     Error     0          33s
perl-sample-errorjob-dp2c5        0/1     Error     0          43s
perl-sample-errorjob-jcgkw        0/1     Error     0          12s
:
:
```

```
PS C:\Users\zaki\src\containers\job> kubectl get pod
NAME                              READY   STATUS    RESTARTS   AGE
perl-sample-errorjob-2ztrv        0/1     Error     0          21m
perl-sample-errorjob-bfd6w        0/1     Error     0          26m
perl-sample-errorjob-dp2c5        0/1     Error     0          26m
perl-sample-errorjob-jcgkw        0/1     Error     0          25m
perl-sample-errorjob-jpjk9        0/1     Error     0          25m
perl-sample-errorjob-prfkz        0/1     Error     0          24m
```

6回のエラーで停止している

## backoffLimit

```yaml
PS C:\Users\zaki\src\containers\job> kubectl get job perl-sample-errorjob -o yaml
apiVersion: batch/v1
kind: Job
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"batch/v1","kind":"Job","metadata":{"annotations":{},"name":"perl-sample-errorjob","namespace":"default"},"spec":{"template":{"spec":{"containers":[{"command":["perl","-e","sleep 5; print \"hello\\n\"; exit 1"],"image":"perl","name":"perl-sample-errorjob"}],"restartPolicy":"Never"}}}}
  creationTimestamp: "2019-03-19T15:18:21Z"
  labels:
    controller-uid: 3b4b3b39-4a5a-11e9-b886-080027b9884e
    job-name: perl-sample-errorjob
  name: perl-sample-errorjob
  namespace: default
  resourceVersion: "9002"
  selfLink: /apis/batch/v1/namespaces/default/jobs/perl-sample-errorjob
  uid: 3b4b3b39-4a5a-11e9-b886-080027b9884e
spec:
  backoffLimit: 6
  completions: 1
  parallelism: 1
  selector:
    matchLabels:
      controller-uid: 3b4b3b39-4a5a-11e9-b886-080027b9884e
  template:
    metadata:
      creationTimestamp: null
      labels:
        controller-uid: 3b4b3b39-4a5a-11e9-b886-080027b9884e
        job-name: perl-sample-errorjob
    spec:
      containers:
      - command:
        - perl
        - -e
        - sleep 5; print "hello\n"; exit 1
        image: perl
        imagePullPolicy: Always
        name: perl-sample-errorjob
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Never
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
status:
  conditions:
  - lastProbeTime: "2019-03-19T15:28:32Z"
    lastTransitionTime: "2019-03-19T15:28:32Z"
    message: Job has reached the specified backoff limit
    reason: BackoffLimitExceeded
    status: "True"
    type: Failed
  failed: 6
  startTime: "2019-03-19T15:18:21Z"
```

`backoffLimit`の値が6(デフォルト値)になっているため。
この値が6だと、エラー時の繰り返しが行われても総実行回数が6回に制限される。

## backoffLimitの値を調整

```yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: perl-sample-errjob-1shot
spec:
  backoffLimit: 1
  template:
    spec:
      containers:
      - name: perl-sample-errjob-1shot
        image: perl
        command: ["perl",  "-e", 'sleep 5; print "hello\n"; exit 89']
      restartPolicy: Never
```

```
PS C:\Users\zaki\src\containers\job> kubectl apply -f job-sample-invalid-backofflimit.yaml
```

1だと2回実行される…
2だと3回…

```
PS C:\Users\zaki\src\containers\job> kubectl apply -f job-sample-invalid-backofflimit2.yaml
job.batch/perl-sample-errjob-1shot created
PS C:\Users\zaki\src\containers\job> kubectl get pod
NAME                             READY   STATUS   RESTARTS   AGE
perl-sample-errjob-1shot-7bpqd   0/1     Error    0          25s
perl-sample-errjob-1shot-7dncf   0/1     Error    0          35s
perl-sample-errjob-1shot-cqptf   0/1     Error    0          15s
PS C:\Users\zaki\src\containers\job> kubectl get pod
NAME                             READY   STATUS   RESTARTS   AGE
perl-sample-errjob-1shot-7bpqd   0/1     Error    0          59s
perl-sample-errjob-1shot-7dncf   0/1     Error    0          69s
perl-sample-errjob-1shot-cqptf   0/1     Error    0          49s
PS C:\Users\zaki\src\containers\job>
```

3にすれば3回になった。

```
PS C:\Users\zaki\src\containers\job> kubectl apply -f job-sample-invalid-backofflimit3.yaml
job.batch/perl-sample-errjob-1shot created
PS C:\Users\zaki\src\containers\job> kubectl get job
NAME                       COMPLETIONS   DURATION   AGE
perl-sample-errjob-1shot   0/1           4s         4s
PS C:\Users\zaki\src\containers\job> kubectl get pod
NAME                             READY   STATUS    RESTARTS   AGE
perl-sample-errjob-1shot-h5pqs   1/1     Running   0          7s
PS C:\Users\zaki\src\containers\job> kubectl get pod
NAME                             READY   STATUS   RESTARTS   AGE
perl-sample-errjob-1shot-7wbnj   0/1     Error    0          58s
perl-sample-errjob-1shot-8jpgn   0/1     Error    0          38s
perl-sample-errjob-1shot-h5pqs   0/1     Error    0          69s
PS C:\Users\zaki\src\containers\job>
```

`0`を指定すれば、初回は起動してそれっきりとなった。

```yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: perl-sample-errjob-1shot
spec:
  backoffLimit: 0
  template:
    spec:
      containers:
      - name: perl-sample-errjob-1shot
        image: perl
        command: ["perl",  "-e", 'sleep 5; print "hello\n"; exit 89']
      restartPolicy: Never
```

```
PS C:\Users\zaki\src\containers\job> kubectl get pod
NAME                             READY   STATUS   RESTARTS   AGE
perl-sample-errjob-1shot-t2d8w   0/1     Error    0          18s
PS C:\Users\zaki\src\containers\job> kubectl get job
NAME                       COMPLETIONS   DURATION   AGE
perl-sample-errjob-1shot   0/1           21s        21s
```

```yaml
PS C:\Users\zaki\src\containers\job> kubectl get job perl-sample-errjob-1shot -o yaml
apiVersion: v1
items:
- apiVersion: batch/v1
  kind: Job
  metadata:
:
:
  spec:
    backoffLimit: 0
    completions: 1
    parallelism: 1
:
:
  status:
    conditions:
    - lastProbeTime: "2019-03-20T13:36:59Z"
      lastTransitionTime: "2019-03-20T13:36:59Z"
      message: Job has reached the specified backoff limit
      reason: BackoffLimitExceeded
      status: "True"
      type: Failed
    failed: 1
    startTime: "2019-03-20T13:36:48Z"
:
```

```yaml
PS C:\Users\zaki\src\containers\job> kubectl get pod perl-sample-errjob-1shot-t2d8w -o yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2019-03-20T13:36:49Z"
  generateName: perl-sample-errjob-1shot-
  labels:
    controller-uid: 365cadf1-4b15-11e9-8e88-080027b9884e
    job-name: perl-sample-errjob-1shot
  name: perl-sample-errjob-1shot-t2d8w
  namespace: default
  ownerReferences:
  - apiVersion: batch/v1
    blockOwnerDeletion: true
    controller: true
    kind: Job
    name: perl-sample-errjob-1shot
    uid: 365cadf1-4b15-11e9-8e88-080027b9884e
  resourceVersion: "15779"
  selfLink: /api/v1/namespaces/default/pods/perl-sample-errjob-1shot-t2d8w
  uid: 36629733-4b15-11e9-8e88-080027b9884e
spec:
  containers:
  - command:
    - perl
    - -e
    - sleep 5; print "hello\n"; exit 89
    image: perl
    imagePullPolicy: Always
    name: perl-sample-errjob-1shot
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: default-token-qr2gn
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  nodeName: minikube
  priority: 0
  restartPolicy: Never
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: default
  serviceAccountName: default
  terminationGracePeriodSeconds: 30
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:
  - name: default-token-qr2gn
    secret:
      defaultMode: 420
      secretName: default-token-qr2gn
status:
  conditions:
  - lastProbeTime: null
    lastTransitionTime: "2019-03-20T13:36:49Z"
    status: "True"
    type: Initialized
  - lastProbeTime: null
    lastTransitionTime: "2019-03-20T13:36:59Z"
    message: 'containers with unready status: [perl-sample-errjob-1shot]'
    reason: ContainersNotReady
    status: "False"
    type: Ready
  - lastProbeTime: null
    lastTransitionTime: "2019-03-20T13:36:59Z"
    message: 'containers with unready status: [perl-sample-errjob-1shot]'
    reason: ContainersNotReady
    status: "False"
    type: ContainersReady
  - lastProbeTime: null
    lastTransitionTime: "2019-03-20T13:36:49Z"
    status: "True"
    type: PodScheduled
  containerStatuses:
  - containerID: docker://dadd972215e3f931ee99e06705a109ca60dc59029e5e2268794941eae98e69d1
    image: perl:latest
    imageID: docker-pullable://perl@sha256:457fd220e1bb557d2461bbb34a10a8cc3de72819c69b59f606b79f863cb7f420
    lastState: {}
    name: perl-sample-errjob-1shot
    ready: false
    restartCount: 0
    state:
      terminated:
        containerID: docker://dadd972215e3f931ee99e06705a109ca60dc59029e5e2268794941eae98e69d1
        exitCode: 89
        finishedAt: "2019-03-20T13:36:58Z"
        reason: Error
        startedAt: "2019-03-20T13:36:53Z"
  hostIP: 10.0.2.15
  phase: Failed
  podIP: 172.17.0.5
  qosClass: BestEffort
  startTime: "2019-03-20T13:36:49Z"
```

# まとめ

|backoffLimitの値|総実行回数|
|----------------|----------|
|0               |1         |
|1               |2         |
|2               |3         |
|3               |3         |
|4               |4         |
|5               |5         |
|6 (default)     |6         |

釈然としないけど、「Jobによるコンテナの1ショット実行/エラー発生してもリトライしない」動作が必要であれば、`backoffLimit`の値を0に指定する。

もしかすると、Error停止して`BackoffLimitExceeded`で処理終了される判定されるのにタイムラグがあって、次のPodがフライング的にリトライされているのかなぁ…？？


# (追記) backoffLimitの精度

何度か試してみたが、あまり精度はよくないようだ。
`0`指定の場合は今のところ1回しか実行されていないが、デフォルトの`6`の場合は総実行回数が4～8回ほど実行されたりして、振り幅が大きい。

回数に精度が求められる場合は注意したほうがよさそう。