[Kubernetes] CronJobで .spec.suspend を TRUE に指定するときに注意すべきこと


# はじめに

CronJobを一時的に止めておきたかったので `.spec.suspend` を使ってみたのですが、自分的に予想外の動きをしてしまったので、対処方法をまとめておきます。

この `.spec.suspend` の注意点を見逃すと、**CronJobが一時的に停止した状態からJobを生成できなくなります。**

対処方法までまとめましたので、是非最後まで読んでいただければと思います。

本記事は、 `1.22.12-gke.300` で動作確認しました。

# やりたいこと

1分ごとに Hello! と言ってくれるCronJobを作成しました。社交的なCronJobですね。

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

このCronJobの実行を、ある期間だけ止めておきたい。
深夜の寝ている時間はうるさいから止めておきたい、といった感じです。

なので、次のコマンドのように `.spec.suspend` を **TRUE** にして止めておいて、

```shell
kubectl patch cronjob hello  -p '{ "spec": { "suspend": true } }'
```

動かしたいタイミングで、**FALSE** にしてまた動かそうと考えました。

```shell
kubectl patch cronjob hello  -p '{ "spec": { "suspend": false } }'
```


# `.spec.suspend` を TRUE に指定したことで何が起きたか

冒頭にお話した通り、CronJobがJobを生成してくれなくなりました。

流れ
1. 18:00, CronJobを作成。コマンドが実行される。
2. 21:00, 就寝するため、`.spec.suspend` を **TRUE** に設定。Jobが作成されなくなる。
3. 5:00, 起床。 `.spec.suspend` を **FALSE** に設定。しかしCronJobがJobを生成してくれない。

期待して起きたのに、Hello!と言ってもらえないのは悲しいですね。


# なぜ問題が起きたか
    
`.spec.suspend` を **TRUE** にすると内部で何が起きているのか、公式ドキュメントに記載されていました。


> 注意: スケジュールされた時間中にサスペンドされた実行は、見逃されたJob(missed job)としてカウントされます。starting deadlineが設定されていない既存のCronJob.spec.suspendがtrueからfalseに変更されると、見逃されたJobは即座にスケジュールされます。
https://kubernetes.io/ja/docs/tasks/job/automated-tasks-with-cron-jobs/#starting-deadline

つまり、停止中に実行されるはずだったJobは、失敗としてカウントされるようです。そして、CronJobは、生成するJobが100回連続して失敗すると、Jobを作成できなくなります。

公式ドキュメントに記載されていました。
> 最後にスケジュールされた時刻から現在までの間に、CronJobコントローラーはどれだけスケジュールが間に合わなかったのかをCronJobごとにチェックします。もし、100回以上スケジュールが失敗していると、ジョブは開始されずに、ログにエラーが記録されます。
https://kubernetes.io/ja/docs/concepts/workloads/controllers/cron-jobs/#cron-job-limitations


なぜ100回なのか、これはソースコードのコメントに記載されていました。
https://github.com/kubernetes/kubernetes/blob/release-1.25/pkg/controller/cronjob/utils.go#L96-L112

訳してみると、
> 金曜日の午後5時1分、皆が帰宅した時にコントローラが動かなくなった場合、火曜日の午前中に誰かが来て問題を発見し、コントローラを再起動すると、1時間毎のcronJobの80以上のジョブが、それ以上の介入なしに全て動き出すはずです。80個よりは多く、「たくさん」よりは少ないということで、多少恣意的に100個を選んでみた。

例えば30分に1回のように、毎時より高頻度で実行する場合や、3連休以上の休暇があった場合は、CronJobはJobを作成できなくなります。 **100回** というのはハードコーディングされているため、「設定で変更しよう」といったことは出来なさそうです。


今回の場合は、

```
停止時間(8h) * 実行間隔(60/h) = 480回
```
なので、480回失敗としてカウントされていたことになります。


# 対応したこと

素直に、CronJobを削除、再度作成するほうがよさそうです。

しかし、その他の制約上どうしても `.spec.suspend` を使いたい（CronJobを削除したくない）場合もあります。
そんなときは、 `.spec.schedule` を編集することでこの問題を回避することができます。実行頻度を減らして、停止中の失敗回数を減らす作戦です。

例えば、 **毎分実行** の場合100分で上限に達してしまいますが、**毎時実行** に変更すれば100時間未満まで停止できることになります。


なので、次のコマンドのように `.spec.suspend` を **TRUE** にする前に、 `.spec.schedule` を毎時0分実行に編集します。

```shell
kubectl patch cronjob hello -p '{ "spec": { "schedule": "0 * * * *" } }'
kubectl patch cronjob hello  -p '{ "spec": { "suspend": true } }'
```

そして、**FALSE** に戻す前に、次のコマンドで`.spec.schedule` を戻してまた動かします。


```shell
kubectl patch cronjob hello -p '{ "spec": { "schedule": "* * * * *" } }'
kubectl patch cronjob hello  -p '{ "spec": { "suspend": false } }'
```

これで、ひとまずCronJobがJobを作成してくれるようになりました。




# さいごに

停止中に実行できなかったJobを生成する方法もあります。 `.spec.startingDeadlineSeconds` を指定するやり方です。

こちらの記事が参考になると思います。

https://qiita.com/tmshn/items/aedf0d739a43a1d6423d#%E3%82%B1%E3%83%BC%E3%82%B93-startingdeadlineseconds




# 参考


https://kubernetes.io/ja/docs/concepts/workloads/controllers/cron-jobs/

https://kubernetes.io/ja/docs/tasks/job/automated-tasks-with-cron-jobs/#starting-deadline

https://engineering.mercari.com/blog/entry/k8s-cronjob-20200908/