Job と CronJob の仕組み

v1.37 時点(2026年10月25日 確認)

Kubernetes 基本仕様第16回 / 全45回 ・ 第2部 ワークロード
目次

この記事では終わりまで動いて止まる処理を Pod で動かす Job と、決まった時刻に Job を作る CronJob の仕組みを、図で説明します。Job が「終わった」と判断する決まりと、失敗した処理をどこまでやり直すかを決める backoffLimit の働きがわかります。CronJob がどのタイムゾーンの時刻でスケジュールを読むのかもわかります。後半では kind のクラスタで、成功する Job、失敗して再試行される Job、指定した時刻に Job を作る CronJob を動かす手順を示します。

Pod を直接作ると、消えたあと誰も作り直さないことを確かめた第8回の内容を前提にしています。まだの方は先にこちらをどうぞ。

基本仕様 ・ 第8回 Pod を動かして理解するKubernetes で作って管理できる最小の単位である Pod の仕組みを図で説明します。Pod とコンテナの関係、Pod の中のコンテナが共有するもの、Pod が 1 つのノードに置かれて移らないことを押さえ、kind のクラスタで Pod を直接作って削除し、もう戻らないことを確かめる手順を紹介します。

Job の Pod に書ける restartPolicy の値と、Pod のフェーズ(Succeeded、Failed など)は第9回で扱いました。この回でも使います。

基本仕様 ・ 第9回 Pod のライフサイクルと再起動の仕組みPod のフェーズ、コンテナの 3 つの状態、restartPolicy によるコンテナの再起動の仕組みを図で説明します。CrashLoopBackOff の意味と、再起動までの待ち時間が延びていく仕組みを押さえ、kind のクラスタでわざと失敗するコンテナを動かして、状態の移り変わりを観察する手順を紹介します。

以下の図は hello-cron という名前の CronJob が、指定した時刻に Job を作り、その Job が Pod を作る関係を表しています。後半の「手元で試す」で作る CronJob にあたります。

CronJob、Job、Pod の管理関係

CronJob が作るのは Pod ではなく Job です。Pod を作るのは Job のほうです。公式ドキュメントも、CronJob はスケジュールに合った Job を作ることだけを受け持ち、Job がその Pod を管理する、と説明しています。CronJob を受け持つのは kube-controller-manager の中の cronjob-controller、Job を受け持つのは job-controller です(第4回)。破線の「作る」(と後の図の「持つ」)は第6回と第11回の図と同じ意味で、持ち主と従属物の関係を表します。

参考:CronJob | Kubernetes、kube-controller-manager | Kubernetes、Owners and Dependents | Kubernetes

Job と CronJob とは

Job は 1 つ以上の Pod を作り、決まった数の Pod が成功して終わるまで、Pod の実行をやり直し続けるワークロードのリソース です。公式ドキュメントは Job を、一度きりの仕事を終わりまで実行するもの(one-off tasks that run to completion)と説明しています。Pod が成功して終わるたびに、Job はその成功の数を記録します。決まった数に達すると、仕事(Job)は完了です。Job を消すと、Job が作った Pod も片付けられます。

参考:Jobs | Kubernetes

いちばん簡単な使い方は 1 つの Pod を確実に終わりまで動かすために、Job を 1 つ作ることです。公式ドキュメントによると、最初の Pod が失敗したり消されたりすると(たとえばノードのハードウェアの故障や、ノードの再起動で)、Job は新しい Pod を作ります。複数の Pod を並べて同時に動かすこともできます。

参考:Jobs | Kubernetes

CronJob は 決まったスケジュールで、繰り返し Job を作るリソース です。公式ドキュメントはバックアップやレポートの作成のような、定期的に行う作業に使うものとしています。1 つの CronJob は Unix の crontab(cron table)のファイルの 1 行のようなもの、とも説明しています。cron は一般に、Unix 系の OS で決まった時刻にコマンドを動かす仕組みで、crontab はその時刻とコマンドを書いておく表です。CronJob のスケジュールも cron と同じ書き方(Cron 形式)で書きます。

参考:CronJob | Kubernetes

3 つのリソースの役割を、次の表にまとめます。

リソース作るもの受け持つコントローラー
CronJobスケジュールの時刻ごとに Job を作るcronjob-controller
Job決まった数の Pod が成功するまで Pod を作るjob-controller
Podコンテナを動かす。Job の Pod では、終わったコンテナを再起動するかどうかを restartPolicy の Never か OnFailure で決める(ノードの kubelet が動かす)

参考:CronJob | Kubernetes、Jobs | Kubernetes、kube-controller-manager | Kubernetes

なぜ Job と CronJob が必要なのか

第9回で見たとおり、コンテナで動かすプログラムには、動き続けるものと、仕事を終えたら止まるものがあります。これまでの回の Deployment や ReplicaSet は、動き続ける Pod のためのリソースで、Pod の restartPolicy には Always しか書けません。仕事を終えて止まったコンテナも再起動されるので、一度だけ動かしたい処理が何度も繰り返されてしまいます。公式ドキュメントも、終わることを想定しない Pod(Web サーバーなど)を管理するリソースと、終わることを想定する Pod(バッチ処理など)を管理する Job は、互いを補い合うものだと説明しています。バッチ処理は一般に、データをまとめて処理して終わる仕事のことです。

参考:Jobs | Kubernetes、Pod Lifecycle | Kubernetes、Deployments | Kubernetes

では、一度きりの処理なら、restartPolicy: Never の Pod を直接作ればよいのでしょうか。第8回で見たとおり、直接作った Pod は、ノードが故障したり再起動したりして終了すると、作り直されません。公式ドキュメントによると、Job は終了させられた Pod の代わりに新しい Pod を作ります。そのため公式ドキュメントは、Pod が 1 つしか要らない処理でも、Pod を直接作らず Job を使う よう勧めています。

参考:Jobs | Kubernetes、Pods | Kubernetes

一度きりの処理を Pod で動かす方法を、公式ドキュメントの説明に沿って次の表で比べます。

方法終わった Pod の扱いPod が失敗したり消えたりしたとき
Deployment(第12回)restartPolicy は Always だけなので、終わったコンテナも再起動される代わりの Pod が作られる
Pod を直接作る(第8回)Never なら、終わったコンテナは再起動されないノードの故障などで終了すると、作り直されない
JobNever か OnFailure を書く。成功した Pod の数を数え、決まった数に達したら完了決まった回数までやり直す(新しい Pod を作るか、同じ Pod の中でコンテナを再起動する)

参考:Jobs | Kubernetes、Pod Lifecycle | Kubernetes、Deployments | Kubernetes

Job があれば一度きりの処理は動かせますが、毎晩のバックアップのように、決まった時刻に繰り返したい処理もあります。そのたびに利用者が Job を作るのでは手間がかかり、作り忘れも起きると考えられます。この「決まった時刻に Job を作る」仕事を引き受けるのが CronJob です。

参考:CronJob | Kubernetes

Job と CronJob の仕組み

Job のマニフェストに書くフィールド

以下はメッセージを 1 行書き出し、5 秒待って成功で終わる Job のマニフェストです。公式ドキュメントの Job の例(controllers/job.yaml)は、perl:5.34.0 のイメージで円周率を 2000 桁計算するものです。この回では第9回と同じ busybox:1.28 のイメージを使い、短いシェルのコマンドを動かすようにしています。フィールドの形は公式ドキュメントの例に沿っています。後半の「手元で試す」でも、このマニフェストを使います。

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: hello-job
spec:
  template:
    spec:
      containers:
      - name: hello
        image: busybox:1.28
        command: ['sh', '-c', 'echo "Hello from the Job"; sleep 5']
      restartPolicy: Never

参考:Jobs | Kubernetes、Job | Kubernetes API Reference

apiVersion が batch/v1 であることに注意します。これまでの Deployment や DaemonSet は apps/v1 でしたが、Job と CronJob は batch という API グループ(第5回)に属しています。command の書き方は第9回と同じです。シェル(sh)が echo で 1 行書き出し、sleep 5 で 5 秒待ちます。最後のコマンドが成功するので、コンテナは終了コード 0(成功)で終わると考えられます。

参考:Job | Kubernetes API Reference、Define a Command and Arguments for a Container | Kubernetes

この回にかかわる Job のフィールドを、v1.37 時点の公式ドキュメントと API リファレンスに沿って次の表にまとめます。

フィールド内容既定値
spec.templatePod のひな形。spec の中で唯一の必須のフィールド。restartPolicy に書けるのは Never か OnFailure だけなし
spec.completionsJob が成功して終わらせるべき Pod の数(望ましい数)書かなければ「並列実行の 3 つの型」で説明
spec.parallelism同時に動かしたい Pod の数の上限1
spec.backoffLimitJob を失敗とみなすまでの再試行の数6
spec.activeDeadlineSecondsJob が動いていてよい秒数。Job が処理を始めた時刻(status.startTime)から数える。正の整数なし
spec.ttlSecondsAfterFinished終わった(Complete か Failed になった)Job を、何秒後に自動で消してよいかなし(自動では消さない)

参考:Job | Kubernetes API Reference、Jobs | Kubernetes

restartPolicy はひな形の中に必ず書きます。公式ドキュメントによると、restartPolicy を書かなければ既定値の Always になります。Job では Always を使えないので、書き忘れると Job は受け付けられないと考えられます。

参考:Jobs | Kubernetes、Jobs | Kubernetes

Deployment と違い、マニフェストに selector とラベルを書いていません。公式ドキュメントによると、Job の spec.selector は書かなくてよいフィールドで、ほとんどの場合、指定すべきではありません(should not specify it)。API リファレンスによると、ふつうはシステムが、その Job だけに付くラベルを選んで Pod のひな形に足し、セレクタも設定します。公式ドキュメントの例の kubectl describe job pi の表示では、Selector: が batch.kubernetes.io/controller-uid=<UID> で、Job の Labels: に batch.kubernetes.io/job-name=pi も並んでいます。同じページでは batch.kubernetes.io/job-name=pi のラベルセレクタで Job の Pod を一覧しています。そのため Job の Pod は batch.kubernetes.io/job-name=<Job の名前> のラベルセレクタで選べます。

参考:Jobs | Kubernetes、Jobs | Kubernetes、Jobs | Kubernetes、Job | Kubernetes API Reference

Job の Pod の名前は Job の名前をもとに付きます。公式ドキュメントの例では、pi という Job の Pod は pi-xf9p4 や pi-5rwd7 のように、Job の名前の後ろに文字列が付いた名前でした。そのため公式ドキュメントは、Job の名前は DNS ラベルの決まり(第8回)に従うのがよいとし、どんな場合も 63 文字以内でなければならない、としています。

参考:Jobs | Kubernetes、Jobs | Kubernetes

並列実行の 3 つの型

公式ドキュメントは Job で動かすのに向いた仕事を 3 つの型に分けています。completions と parallelism の書き方で型が決まります。3 つ目の型の「ワークキュー」は処理する仕事を並べておく待ち行列のことです。この型では、各 Pod が何を処理するかを Pod の側で決めます(詳しくは表のあとで説明します)。

型書き方完了するとき
並列でない Jobcompletions と parallelism を両方書かない(どちらも 1 になる)ふつうは Pod を 1 つだけ動かし、その Pod が成功して終わったら完了
完了の数を決めた並列の Jobcompletions に正の数を書く。parallelism は書かなければ 1成功した Pod の数が completions に達したら完了
ワークキューを使う並列の Jobcompletions を書かず、parallelism に 0 以上の整数を書くどれか 1 つの Pod が成功して終わり、すべての Pod が終わったら完了

参考:Jobs | Kubernetes、Job | Kubernetes API Reference

公式ドキュメントによると、ワークキューを使う型では、各 Pod が何を処理するかを Pod どうしか外部のサービスと調整して決めます。どれか 1 つの Pod が成功して終わると、新しい Pod は作られません。この回のマニフェストは 1 つ目の「並列でない Job」です。

参考:Jobs | Kubernetes

parallelism は上限で、いつもその数の Pod が動くとは限りません。API リファレンスによると、残りの仕事(completions から成功した数を引いたもの)が parallelism より少なくなると、動く Pod の数も parallelism より少なくなります。公式ドキュメントは、parallelism を 0 にすると、増やすまで Job は事実上止まる、とも書いています。表のワークキューの型で「0 以上の整数」としたのは公式ドキュメントの説明どおりで、0 を書くと、この止まった状態になります。

参考:Job | Kubernetes API Reference、Jobs | Kubernetes

completions を書いた Job では、完了の数え方を spec.completionMode で選べます。既定の NonIndexed では、どの Pod の成功も同じ 1 つの成功として数えます。Indexed にすると、各 Pod に 0 から completions - 1 までの番号(インデックス)が付き、番号ごとに 1 つずつ Pod が成功したら完了です。この回では既定の NonIndexed だけを使います。

参考:Jobs | Kubernetes、Job | Kubernetes API Reference

失敗したときの再試行

Job の Pod の中でコンテナが失敗したとき、誰がやり直すかは restartPolicy によって変わります。公式ドキュメントの説明を次の表にまとめます。

restartPolicyコンテナが失敗したときやり直す担当
OnFailurePod はノードに残り、同じ Pod の中でコンテナが動かし直されるkubelet(第9回の再起動)
Never失敗した Pod はそのまま残り、新しい Pod が作られるjob-controller

参考:Jobs | Kubernetes、Pod Lifecycle | Kubernetes

公式ドキュメントによると、restartPolicy が OnFailure の Job でも、Pod ごと失敗した場合(ノードから退避させられた場合など)は、job-controller が新しい Pod を作ります。

参考:Jobs | Kubernetes

以下の図は restartPolicy の値によって、やり直しが起きる場所がどう違うかを表しています。上下の段はそれぞれ別のマニフェストを渡した場合です。

restartPolicy による、やり直しが起きる場所の違い(Never と OnFailure)

Never では、失敗した Pod が消されずに並んでいくので、失敗のたびのログを Pod ごとに読めます。OnFailure では、Pod は 1 つのまま、その中でコンテナが入れ替わります。kubelet から出る破線は持ち主の関係ではなく、kubelet がコンテナを動かし直す管理の関係を表しています。上の段の「新しい Pod を作る」と下の段の「持つ」は、どちらも Job が Pod の持ち主になる同じ関係です。上の段は失敗のたびに Pod を作る動きを、下の段は 1 つの Pod を持ち続けることを強調して描いています。

公式ドキュメントによると、OnFailure では、同じノードの中で再起動される場合をプログラムが扱えるようにしておく必要があります。新しい Pod でやり直す場合も、前の実行が残した一時ファイルやロック(一般に、同じファイルなどをほかのプログラムが同時に使わないようにする印)、途中まで書いた出力などを、プログラムが扱えるようにしておく必要があります。

参考:Jobs | Kubernetes

やり直しが無限に続かないように、Job には再試行の上限があります。それが spec.backoffLimit で、既定値は 6 です。公式ドキュメントによると、job-controller は失敗した Pod を作り直すとき、10 秒、20 秒、40 秒……と倍々に延びる待ち時間を入れ、待ち時間は 10 分で頭打ちになります。前の Pod が失敗してから次の Pod が作られるまでに、この待ち時間が入ると読めます。再試行の数は次の 2 通りで数えます。

数え方数えるもの
失敗した Pod の数status.phase が Failed の Pod の数
コンテナの再試行の数(OnFailure のとき)フェーズが Pending か Running の Pod の、すべてのコンテナの再試行の数。kubelet がコンテナを再起動した回数も backoffLimit に数えられる、と読める

参考:Jobs | Kubernetes

API リファレンスは backoffLimit を「Job を失敗とみなすまでの再試行の数」と説明していて、最初の 1 回は再試行に数えないと読めます。公式ドキュメントの Pod の失敗の方針のページも、backoffLimit が 0 の Job では、Pod が 1 つ中断されて失敗しただけで Job 全体が終了させられる、と説明しています。インデックスごとの上限の例でも、再試行 1 回までの設定で、失敗したインデックスごとに失敗した Pod が 2 つ数えられています。そのため、backoffLimit が 2 なら、最初の 1 回と 2 回の再試行で、失敗した Pod が 3 つになったところで Job が失敗になると読めます。Jobs のページには「backoffLimit に達すると」と「backoffLimit を超えた」の 2 通りの書き方がありますが、これらの例に合うのは後者の読み方です。

参考:Job | Kubernetes API Reference、Handling retriable and non-retriable pod failures with Pod failure policy | Kubernetes、Jobs | Kubernetes、Jobs | Kubernetes、Jobs | Kubernetes

公式ドキュメントによると、backoffLimit に達すると、Job は失敗とされ、動いている Pod は終了させられます。restartPolicy が OnFailure の場合、Job の処理を動かしていた Pod も終了させられるので、プログラムの不具合を調べにくくなります。そのため公式ドキュメントは、Job を調べているあいだは restartPolicy を Never にするか、ログを集める仕組みを使うよう勧めています。

参考:Jobs | Kubernetes、Jobs | Kubernetes

もう 1 つの止め方が spec.activeDeadlineSeconds です。公式ドキュメントによると、この秒数は Pod をいくつ作ったかに関係なく、Job 全体が動いている時間に当てはまります。この秒数に達すると、動いている Pod はすべて終了させられ、Job は type: Failed、reason: DeadlineExceeded の状態になります。activeDeadlineSeconds は backoffLimit より優先されます。再試行の途中でも、時間切れになれば、backoffLimit に達していなくても新しい Pod は作られません。

参考:Jobs | Kubernetes

activeDeadlineSeconds という名前のフィールドは、Job の spec と、Pod のひな形の spec の両方にあります。公式ドキュメントは正しいほうに書くよう注意しています。Job 全体の制限時間にしたいなら、Job の spec の直下(template と同じ深さ)に書きます。Pod のひな形の spec(containers と同じ深さ)の activeDeadlineSeconds は、Pod の API リファレンスによると、Pod ごとの制限時間です。また、restartPolicy は Pod に当てはまるもので、Job そのものには当てはまりません。Job が一度 Failed になると、自動ではやり直されません。backoffLimit や activeDeadlineSeconds で Job が失敗したら、利用者が手で対応する必要があります。

参考:Jobs | Kubernetes、Pod | Kubernetes API Reference

Job の終わり方と status

公式ドキュメントによると、Job の終わりの状態は 2 つで、それぞれに Job の condition があります。condition は第5回で見た、オブジェクトの状態を表す status.conditions の項目です。

終わりの状態conditionそうなる主な理由
成功Complete成功した Pod の数が completions に達した
失敗FailedPod の失敗の数が backoffLimit を超えた。または、Job の実行時間が activeDeadlineSeconds を超えた

参考:Jobs | Kubernetes

公式ドキュメントは失敗の理由として、この表のほかに podFailurePolicy や backoffLimitPerIndex にかかわるものも、成功の理由として successPolicy(Indexed の Job の成功の条件)を満たした場合も挙げています。この回では使わないので、表には入れていません。

以下の図は restartPolicy: Never の Job が、成功か失敗で終わるまでの移り変わりを表しています。

restartPolicy: Never の Job の移り変わり(実行中、再試行、Complete、Failed)

Pod が失敗するたびに、待ち時間を置いて新しい Pod が作られ、実行中に戻ります。成功の数が completions に達すれば Complete、失敗の数が上限を超えるか時間切れになれば Failed で終わります。Failed から実行中へ戻る線は無く、自動ではやり直されません。図の「実行中」と「再試行を待つ」は説明のための名前で、Job の condition ではありません。「新しい Pod を作る」の破線は状態の移り変わりを表す線で、冒頭の図の破線(持ち主の関係)とは意味が違います。

公式ドキュメントによると、再試行を待っているあいだでも、activeDeadlineSeconds の時間切れになれば、backoffLimit に達していなくても新しい Pod は作られず、Job は Failed になります。図ではこの移り変わりを簡略化し、「実行中」からの線だけで描いています。

参考:Jobs | Kubernetes、Jobs | Kubernetes、Jobs | Kubernetes

終わりの condition が付くまでには、少し段階があります。公式ドキュメントによると、Job が成功か失敗の条件を満たすと、job-controller はまず SuccessCriteriaMet か FailureTarget の condition を付け、残っている Pod を終了させます。Kubernetes v1.31 からは、すべての Pod が終了したあとで、終わりの condition(Complete か Failed)を付けます。そのため、Pod の terminationGracePeriodSeconds(終了までの猶予の時間)などによって、条件を満たしてから Complete や Failed が付くまでに時間がかかることがあります。

参考:Jobs | Kubernetes、Jobs | Kubernetes

Job の status には、Pod の数を数えるフィールドが並びます。v1.37 時点の API リファレンスの説明を次の表にまとめます。表の説明は API リファレンスの定義の訳です。

フィールド数えるもの
status.activePending か Running の Pod のうち、終了中でない(deletionTimestamp が無い)Pod の数。終わった Job では 0
status.succeededフェーズが Succeeded になった Pod の数
status.failedフェーズが Failed になった Pod の数
status.readyactive の Pod のうち、Ready の condition を持ち、終了中でない Pod の数
status.terminating終了中(フェーズが Pending か Running で、deletionTimestamp がある)の Pod の数

参考:Job | Kubernetes API Reference

deletionTimestamp は、第6回で見た、削除が始まった Pod に kube-apiserver が書き込む削除の時刻です。API リファレンスによると、status.startTime は job-controller が Job の処理を始めた時刻、status.completionTime は Job が完了した時刻で、どちらも UTC(協定世界時)で表されます。completionTime が入るのは、Job が成功して終わったときだけです。

参考:Job | Kubernetes API Reference、Pod Lifecycle | Kubernetes、Finalizers | Kubernetes

公式ドキュメントによると、Job が完了すると、それ以上 Pod は作られませんが、Pod はふつう消されずに残ります。終わった Pod のログを読んで、エラーや警告を確かめられるようにするためです。Job のオブジェクトも status を見られるように残ります。古い Job を消すのは利用者の仕事です。kubectl delete jobs/pi のように kubectl で Job を消すと、Job が作った Pod もすべて消されます。

参考:Jobs | Kubernetes

終わった Job を自動で消すには、spec.ttlSecondsAfterFinished を書きます。公式ドキュメントによると、TTL(time to live、生きていてよい時間)の仕組みは v1.23 から正式で、ttl-after-finished-controller が受け持ちます。Job が終わってからこの秒数がたつと、Job は自動で消せる状態になり、Pod などの従属物と一緒に消されます。0 にすると、終わってすぐに消せる状態になります。書かなければ、この仕組みでは消されません。

参考:Jobs | Kubernetes、Automatic Cleanup for Finished Jobs | Kubernetes、kube-controller-manager | Kubernetes

CronJob のスケジュールとタイムゾーン

以下は毎日決まった時刻に Job を作る CronJob のマニフェストです。公式ドキュメントの CronJob の例(application/job/cronjob.yaml)は、毎分("* * * * *")、今の時刻とメッセージを書き出すものです。この回では指定した時刻に Job が作られることを確かめたいので、schedule を毎日 21 時 30 分にし、timeZone を足しています。名前も hello-cron に変えました。そのほかは公式ドキュメントの例と同じです。後半の「手元で試す」では schedule の時刻を書き換えて使います。

yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: hello-cron
spec:
  schedule: "30 21 * * *"
  timeZone: "Asia/Tokyo"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox:1.28
            imagePullPolicy: IfNotPresent
            command:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

参考:Running Automated Tasks with a CronJob | Kubernetes、CronJob | Kubernetes API Reference

spec.jobTemplate は CronJob が作る Job のひな形で、必須です。公式ドキュメントによると、apiVersion と kind が無いことを除けば、Job と同じ形です。そのため jobTemplate.spec の中は前の節の Job の spec と同じ書き方になります。マニフェストの入れ子の対応を次の表にまとめます。

マニフェストの場所何の設定かこの回のマニフェストで書いているもの
specCronJob の specschedule、timeZone、jobTemplate
spec.jobTemplate.specJob の spec(Job のマニフェストの spec と同じ)template
spec.jobTemplate.spec.templatePod のひな形spec
spec.jobTemplate.spec.template.specPod の speccontainers、restartPolicy

参考:CronJob | Kubernetes、CronJob | Kubernetes API Reference

imagePullPolicy: IfNotPresent は公式ドキュメントの例のままです。第5回で見たとおり、latest 以外のタグを付けたイメージでは、imagePullPolicy を書かなくても既定値が IfNotPresent になるので、この行が無くても同じです。

参考:Images | Kubernetes

spec.schedule も必須で、Cron 形式で書きます。公式ドキュメントの説明は次のとおりです。

# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday)
# │ │ │ │ │                                   OR sun, mon, tue, wed, thu, fri, sat
# │ │ │ │ │
# │ │ │ │ │
# * * * * *

参考:CronJob | Kubernetes

空白で区切った 5 つの欄は、左から分(0〜59)、時(0〜23)、日(1〜31)、月(1〜12)、曜日(0〜6。0 が日曜日)です。* はその欄のどの値でもよいことを表します。公式ドキュメントの例では、0 3 * * 1 は毎週月曜日の 3 時 0 分です。*/2 のように / を付けると、その数ごとに、という意味になります(時の欄なら 2 時間ごと)。この回のマニフェストの 30 21 * * * は、毎日 21 時 30 分という意味になります。

参考:CronJob | Kubernetes

5 つの欄の代わりに、@ で始まる短い書き方(マクロ)も使えます。公式ドキュメントの表を次にまとめます。

書き方意味同じ意味の 5 つの欄
@yearly(または @annually)1 年に 1 回、1 月 1 日の 0 時に動かす0 0 1 1 *
@monthly1 か月に 1 回、月の初日の 0 時に動かす0 0 1 * *
@weekly1 週間に 1 回、日曜日の 0 時に動かす0 0 * * 0
@daily(または @midnight)1 日に 1 回、0 時に動かす0 0 * * *
@hourly1 時間に 1 回、0 分に動かす0 * * * *

参考:CronJob | Kubernetes

ここで気を付けたいのが、スケジュールの時刻を、どのタイムゾーンで読むか です。公式ドキュメントによると、タイムゾーンを指定しない CronJob では、kube-controller-manager が自分のローカルのタイムゾーン(API リファレンスの言い方では、kube-controller-manager のプロセスのタイムゾーン)でスケジュールを読みます。つまり 30 21 * * * と書いても、日本時間の 21 時 30 分になるとは限りません。kube-controller-manager がどのタイムゾーンで動いているかは、クラスタによって違うと考えられます。

参考:CronJob | Kubernetes、CronJob | Kubernetes API Reference

タイムゾーンをはっきりさせるには、spec.timeZone に正しいタイムゾーンの名前を書きます。公式ドキュメントによると、timeZone は v1.27 から正式です。たとえば "Etc/UTC" と書くと、スケジュールは UTC で読まれます。名前は tz database(世界のタイムゾーンをまとめたデータ)の名前で、この回のマニフェストの "Asia/Tokyo" は日本時間を表します。

参考:CronJob | Kubernetes、CronJob | Kubernetes API Reference

タイムゾーンの名前の扱いを、API リファレンスと公式ドキュメントの説明に沿って次の表にまとめます。

場面起きること
CronJob を受け付けるときkube-apiserver が、システムのタイムゾーンのデータベースを使って名前を確かめる
スケジュールの時刻に Job を作るときkube-controller-manager が、システムのタイムゾーンのデータベースを使う
システムにタイムゾーンのデータベースが無いときKubernetes のプログラムに組み込まれた、Go の標準ライブラリのデータベースが代わりに使われる
CronJob を作ったあとで名前が使えなくなったときコントローラーは新しい Job を作るのをやめ、理由が UnknownTimeZone のイベントを記録する
schedule の中に CRON_TZ= や TZ= を書いたとき公式にはサポートされず、検証のエラーで受け付けられない

参考:CronJob | Kubernetes API Reference、CronJob | Kubernetes、CronJob | Kubernetes

表の「システムのタイムゾーンのデータベース」は API リファレンスの system-wide time zone database のことで、kube-apiserver や kube-controller-manager が動いている環境(OS)に入っている、タイムゾーンの名前と時差のデータを指すと考えられます。それが無い環境でも、プログラムに組み込まれたデータで名前を扱えます。Go はプログラミング言語の 1 つで、標準ライブラリは一般に、その言語に付属する部品の集まりのことです。公式ドキュメントによると、Go の標準ライブラリのタイムゾーンのデータベースが Kubernetes のプログラム(バイナリ)に含まれています。

参考:CronJob | Kubernetes API Reference、CronJob | Kubernetes

timeZone が変えるのは、スケジュールの読み方だけだと考えられます。Job の Pod のコンテナの中の時計が日本時間になるわけではないはずです。公式ドキュメントの CronJob の例では、コンテナの date の出力が UTC の時刻になっています。

参考:Running Automated Tasks with a CronJob | Kubernetes

CronJob が Job を作るときの決まり

CronJob が作る Job の名前は、CronJob の名前をもとに付きます。公式ドキュメントによると、cronjob-controller は利用者が付けた名前の後ろに自動で 11 文字を足し、Job の名前は 63 文字までと決まっています。そのため CronJob の名前は 52 文字以内 でなければなりません。この 11 文字は名前の長さの上限を決める理由として挙げられている値で、足される文字の中身は書かれていません。公式ドキュメントの CronJob の手順の例では、hello という CronJob が作った Job の名前は hello-4111706356 でした。Job の Pod の名前は、さらにその後ろに文字列が付くので、公式ドキュメントも Job の名前と Pod の名前は違う、と注意しています。

参考:CronJob | Kubernetes、Running Automated Tasks with a CronJob | Kubernetes

CronJob の動き方を決めるフィールドを、v1.37 時点の公式ドキュメントと API リファレンスに沿って次の表にまとめます。

フィールド内容既定値
spec.concurrencyPolicy前の Job が終わる前に次の時刻が来たときの扱い。Allow、Forbid、Replace のどれかAllow
spec.startingDeadlineSeconds何らかの理由で時刻どおりに Job を始められなかったとき、何秒遅れまでなら始めてよいかの期限。期限を過ぎた分は失敗として数えるなし(期限なし)
spec.suspendtrue にすると、これからの実行を止める。すでに始まった実行には当てはまらないfalse
spec.successfulJobsHistoryLimit残しておく、成功して終わった Job の数。0 以上の整数3
spec.failedJobsHistoryLimit残しておく、失敗して終わった Job の数。0 以上の整数1

参考:CronJob | Kubernetes API Reference、CronJob | Kubernetes

concurrencyPolicy の 3 つの値を、公式ドキュメントの説明に沿って次の表にまとめます。

値前の Job が終わる前に、次の時刻が来たとき
Allow(既定)次の Job も作り、同時に動かす
Forbid次の Job を作らずに飛ばす
Replace動いている Job を、新しい Job に置き換える

参考:CronJob | Kubernetes、CronJob | Kubernetes API Reference

公式ドキュメントによると、concurrencyPolicy が当てはまるのは、同じ CronJob が作った Job どうしだけです。別々の CronJob が作った Job どうしは、concurrencyPolicy に関係なく、いつでも同時に動くことが許されます。Forbid で飛ばした場合も、前の Job が終わったときに startingDeadlineSeconds の期限の中なら、新しい Job が作られることがあります。

参考:CronJob | Kubernetes

スケジュールどおりにいかない場合の決まりも、公式ドキュメントに書かれています。主なものを次の表にまとめます。

場面起きること
時刻どおりに Job を作れなかった作れなかった分は「逃した(missed)」実行として数える。Forbid で飛ばした分も逃した実行になる
startingDeadlineSeconds を書いていて、期限を過ぎたその回の実行を飛ばす。次からの時刻はそのまま予定される
前回の時刻から今までに逃した実行が 100 回を超えたJob を始めず、エラーを記録する。逃した分を取り戻す動きにだけ当てはまり、CronJob がずっと止まるわけではない。startingDeadlineSeconds を書いていれば、前回の時刻からではなく、その秒数のあいだに逃した数で数える
suspend が true のあいだに時刻が来た逃した実行として数える。期限の無い CronJob で suspend を false に戻すと、逃した Job がすぐに予定される
CronJob のひな形を書き換えた書き換えたあとに始まる新しい Job にだけ反映される。すでに始まった Job と Pod は、そのまま動き続ける

参考:CronJob | Kubernetes、CronJob | Kubernetes、CronJob | Kubernetes、CronJob | Kubernetes

公式ドキュメントは、startingDeadlineSeconds を 10 秒より短くすると、CronJob が予定されないことがある、と注意しています。cronjob-controller が 10 秒ごとに確かめているからです。このことから、Job はスケジュールの時刻ちょうどに作られるとは限らず、少し遅れることがあると考えられます。

参考:CronJob | Kubernetes

さらに公式ドキュメントは、CronJob が Job を作るのはスケジュールの時刻ごとに「およそ 1 回」だと説明しています。状況によっては、Job が 2 つ作られたり、1 つも作られなかったりすることがあるからです。Kubernetes はそれを避けようとしますが、完全には防げません。そのため Job の処理は冪等(べきとう)にしておくべきです。冪等は同じ処理を何度行っても、1 回行ったときと同じ結果になる性質のことです。Job そのものについても、公式ドキュメントは、parallelism と completions が 1 で restartPolicy が Never でも、同じプログラムが 2 回始まることがある、と書いています。

参考:CronJob | Kubernetes、Jobs | Kubernetes

Job がどの時刻の分として作られたかは、Job のアノテーションでわかります。アノテーションはリソースに付けるキーと値の組のうち、ラベルと違って選ぶためではなく情報を書いておくためのものです(第42回で扱います)。公式ドキュメントによると、Kubernetes v1.32 からは、CronJob が作った Job に batch.kubernetes.io/cronjob-scheduled-timestamp のアノテーションが付きます。値はその Job がもともと作られる予定だった時刻で、RFC3339 の形式(日付、時刻、UTC からのずれを並べた形式)です。

参考:CronJob | Kubernetes、Annotations | Kubernetes

CronJob の status には、次のフィールドがあります。v1.37 時点の API リファレンスの説明の訳です。

フィールド内容
status.active今動いている Job を指すものの一覧
status.lastScheduleTime最後に Job が予定どおり作られた時刻
status.lastSuccessfulTime最後に Job が成功して終わった時刻

参考:CronJob | Kubernetes API Reference

手元で試す

ここまでの内容を手元の kind のクラスタで試す手順を示します。まず成功する Job を動かし、Job が完了することを見ます。次に必ず失敗する Job を backoffLimit: 2 で動かし、Pod が作り直されたあと Job が失敗になることを見ます。最後に、数分後の時刻を指定した CronJob を作り、その時刻に Job が作られることを確かめます。コマンドは macOS や Linux のシェル(bash や zsh)で実行する前提で書いています。Windows では WSL(Windows の中で Linux を動かす仕組み)などを使ってください。試す環境は次のとおりです。

項目内容
kindv0.33.0
Kubernetesv1.37.0(kind v0.33.0 の既定のノードイメージ)
ノード構成シングルノード(クラスタ名 kind):コントロールプレーン 1
必要なものDocker、kubectl(v1.36〜v1.38)、テキストエディタ、端末(ターミナル)2 つ

参考:Release v0.33.0 · kubernetes-sigs/kind、Version Skew Policy | Kubernetes

kind でクラスタを作る手順は、以下の記事で解説しています。この回ではシングルノードのクラスタを使います。公式ドキュメントの CronJob の手順は、コントロールプレーンを兼ねないノードを 2 つ以上持つクラスタを勧めています。ただ、Pod がどのノードに置かれるかを見ないこの回の手順では、1 つで足りると考えられます。

基本仕様 ・ 第2回 kind で検証用のクラスタを作るDocker のコンテナをノードにして、手元のマシンに Kubernetes のクラスタを作るツール kind の仕組みを図で説明します。kind create cluster で何が起きるかを押さえ、シングルノードとマルチノードのクラスタを作って、ノードの正体が Docker のコンテナであることを確かめる手順を紹介します。

参考:Running Automated Tasks with a CronJob | Kubernetes

シングルノードのクラスタを用意する

まず、前の回のクラスタが残っていないかを確かめます。

console
$ kind get clusters

kind が表示されたら、作り直さずにそのまま使えるので、クラスタを作る次のコマンドは飛ばします。表示されなければ、クラスタを作ってから、もう一度一覧します。前の回の multi-node が表示されても、この回では使いません。手元のマシンの資源(メモリなど)に余裕がなければ、kind delete cluster --name multi-node で消してから進めてください。

console
$ kind create cluster
$ kind get clusters

kind get clusters に kind が表示されれば、クラスタができています。第3回で決めたとおり、ここからのコマンドには --context kind-kind を付けて、操作するクラスタをはっきりさせます。

参考:Quick Start | kind

続けて、ノードを一覧します。

console
$ kubectl get nodes --context kind-kind

ノードが 1 つ表示され、STATUS 列が Ready になっていれば準備ができています。Ready になっていなければ、少し待ってからもう一度実行します。

参考:kubectl get | Kubernetes、Node Status | Kubernetes

成功する Job を動かす

「Job のマニフェストに書くフィールド」のマニフェストを、hello-job.yaml という名前で、コマンドを実行するディレクトリに保存します。

ここからは端末を 2 つ使います。第11回・第13回・第14回・第15回と同じく、1 つ目の端末で変化を追い続け、2 つ目の端末でマニフェストを渡したり結果を確かめたりします。2 つ目の端末もマニフェストを保存したディレクトリで開いてください。Job の節で 1 つ目の端末が追うのは Pod です。

1 つ目の端末で、Job の Pod を監視します。-l batch.kubernetes.io/job-name は、値を問わず batch.kubernetes.io/job-name のキーのラベルを持つ Pod を選ぶ指定です(第4回で使った、キーだけのラベルセレクタと同じ形)。Job の Pod にはこのラベルが付くので、この回で作るすべての Job の Pod が表示されます。この監視は CronJob の節に進むまで止めずに続けます。

console
$ kubectl get pods --watch -l batch.kubernetes.io/job-name --context kind-kind

参考:kubectl get | Kubernetes、Labels and Selectors | Kubernetes、Jobs | Kubernetes

Pod がまだ無いので、何も表示されないか、見つからないことを知らせる 1 行が出たあと、止まったように見えると考えられます。

2 つ目の端末で、マニフェストを渡します。

console
$ kubectl apply -f hello-job.yaml --context kind-kind

公式ドキュメントの例(pi という Job)では、出力は次のとおりです。

job.batch/pi created

参考:Jobs | Kubernetes

手元では名前の部分が hello-job になった 1 行が出るはずです。

1 つ目の端末には、hello-job- で始まる名前の Pod の行が書き足されていくはずです。行は Pending、ContainerCreating、Running と移り、コンテナが 5 秒ほど動いて終わると、成功して終わったことを表す Completed のような値になると考えられます。クラスタを作ったばかりなら、イメージの取得に時間がかかるので、ContainerCreating のまましばらく止まって見えることがあります(第9回)。Pod の行は 1 つだけで、Completed になったあとも消えずに残るはずです。Job が完了しても、Pod はふつう消されないからです。

参考:Jobs | Kubernetes、Pod Lifecycle | Kubernetes

1 つ目の端末の表示が落ち着いたら、2 つ目の端末で、Job の status から Pod の数と condition の種類を取り出します。第5回で見た -o jsonpath を使い、succeeded、failed、conditions の各項目の type を 1 行ずつ表示します。

console
$ kubectl get job hello-job -o jsonpath='{.status.succeeded}{"\n"}{.status.failed}{"\n"}{.status.conditions[*].type}{"\n"}' --context kind-kind

参考:Job | Kubernetes API Reference、JSONPath Support | Kubernetes

1 行目は 1 になるはずです。フェーズが Succeeded になった Pod が 1 つ、という意味です。2 行目は失敗した Pod の数で、失敗した Pod が無いので、空の行か 0 になると考えられます。3 行目には Complete が含まれるはずです。「Job の終わり方と status」で見たとおり、その前に付けられる SuccessCriteriaMet も並んで表示されることがあると考えられます。3 行目に Complete が無ければ、job-controller が Pod の終了を待っていると考えられるので、少し待ってからもう一度実行します。

参考:Jobs | Kubernetes、Jobs | Kubernetes

続けて、Pod のログを表示します。公式ドキュメントによると、kubectl logs jobs/<Job の名前> で、Job の Pod のログを読めます。

console
$ kubectl logs jobs/hello-job --context kind-kind

参考:Jobs | Kubernetes

マニフェストの echo で書き出した Hello from the Job の 1 行が表示されるはずです。Pod が終わったあとでもログを読めるのは、Pod が残っているからです。

失敗して再試行される Job を動かす

以下は 5 秒動いたあと終了コード 1 で終わるコンテナを、backoffLimit: 2 の Job で動かすマニフェストです。fail-job.yaml という名前で保存します。restartPolicy は Never にして、失敗のたびに新しい Pod が作られるようにしています。

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: fail-job
spec:
  backoffLimit: 2
  template:
    spec:
      containers:
      - name: fail
        image: busybox:1.28
        command: ['sh', '-c', 'echo "Exiting with code 1 in 5 seconds."; sleep 5; exit 1']
      restartPolicy: Never

参考:Jobs | Kubernetes、Job | Kubernetes API Reference

backoffLimit を既定の 6 のままにしないのは、待ち時間が 10 秒、20 秒、40 秒……と延びていくので、失敗になるまでに時間がかかるからです。

2 つ目の端末で、マニフェストを渡します。

console
$ kubectl apply -f fail-job.yaml --context kind-kind

job.batch/fail-job が作られたことを知らせる 1 行が出るはずです。1 つ目の端末を見ると、行が書き足されていくはずです。起きると考えられることを、次の表にまとめます。

起きること1 つ目の端末の見え方
1 つ目の Pod が作られ、5 秒ほどで失敗するfail-job- で始まる名前の Pod の行が現れ、Running のあと、失敗を表す Error のような値になる
10 秒ほど待って、2 つ目の Pod が作られ、失敗する別の名前の Pod の行が現れ、同じく Error のような値になる
20 秒ほど待って、3 つ目の Pod が作られ、失敗するもう 1 つ別の名前の Pod の行が現れ、同じく Error のような値になる
Job が失敗になる新しい Pod の行は現れなくなる

参考:Jobs | Kubernetes、Jobs | Kubernetes

表は「失敗したときの再試行」で見た公式ドキュメントの説明から考えられる様子です。Error の表示は第9回の restartPolicy: Never の Pod と同じく推測です。失敗した Pod の行は消えずに残り、RESTARTS 列は 0 のままのはずです。Never なので kubelet はコンテナを再起動せず、job-controller が新しい Pod を作るからです。Pod の行の AGE 列を見比べると、Pod が作られる間隔が延びていくのがわかると考えられます。間隔には、コンテナが動いていた 5 秒も含まれます。

参考:Jobs | Kubernetes、Pod Lifecycle | Kubernetes

1 分ほどたって新しい Pod の行が現れなくなったら、2 つ目の端末で、この Job の Pod だけを一覧します。batch.kubernetes.io/job-name=fail-job は、ラベルの値まで指定して、fail-job の Pod だけを選ぶ指定です。

console
$ kubectl get pods -l batch.kubernetes.io/job-name=fail-job --context kind-kind

参考:Jobs | Kubernetes、kubectl get | Kubernetes

Pod の数を数えてください。Pod は 3 つで、どれも失敗して終わっているはずです。最初の 1 回と、backoffLimit の 2 回の再試行の分です。

続けて、Job の status を表示します。成功する Job のときと同じ形のコマンドです。

console
$ kubectl get job fail-job -o jsonpath='{.status.succeeded}{"\n"}{.status.failed}{"\n"}{.status.conditions[*].type}{"\n"}' --context kind-kind

1 行目は成功した Pod が無いので、空の行か 0 になると考えられます。2 行目は、フェーズが Failed になった Pod の数で、直前に数えた Pod の数と同じになるはずです。3 行目には Failed が含まれるはずです。その前に付けられる FailureTarget も並んで表示されることがあると考えられます。

参考:Job | Kubernetes API Reference、Jobs | Kubernetes、Jobs | Kubernetes、Handling retriable and non-retriable pod failures with Pod failure policy | Kubernetes

Failed の condition の理由も取り出します。公式ドキュメントの JSONPath のページにある [?()](条件に合う項目だけを選ぶ書き方)を使い、type が Failed の項目の reason(理由)と message(説明の文)を表示します。

console
$ kubectl get job fail-job -o jsonpath='{.status.conditions[?(@.type=="Failed")].reason}{"\n"}{.status.conditions[?(@.type=="Failed")].message}{"\n"}' --context kind-kind

参考:JSONPath Support | Kubernetes、Job | Kubernetes API Reference

公式ドキュメントの Jobs のページには、activeDeadlineSeconds で失敗したときの理由が DeadlineExceeded になることは書かれていますが、backoffLimit で失敗したときの値は書かれていません。手元では、backoffLimit を超えたことがわかる理由と説明の文が表示されると考えられます。

最後に、失敗した Pod のログを 1 つ読みます。<Pod の名前> には、直前の一覧に表示された名前を 1 つ入れます。

console
$ kubectl logs <Pod の名前> --context kind-kind

Exiting with code 1 in 5 seconds. の 1 行が表示されるはずです。restartPolicy: Never では失敗した Pod が残るので、どの回の実行のログも Pod ごとに読めます。

参考:Jobs | Kubernetes、kubectl logs | Kubernetes

指定した時刻に CronJob が Job を作るのを確かめる

1 つ目の端末で Ctrl+C を押して、Pod の監視を止めます。CronJob の節では公式ドキュメントの CronJob の手順と同じく、Job を監視します。

2 つ目の端末で、今の日本時間を表示します。一般に、コマンドの前に TZ=Asia/Tokyo を付けると、そのコマンドだけ日本時間で時刻を扱います。

console
$ TZ=Asia/Tokyo date

表示された時刻の 3 分ほどあとを、CronJob の時刻にします。「CronJob のスケジュールとタイムゾーン」のマニフェストを hello-cron.yaml という名前で保存し、schedule の分と時を書き換えます。たとえば表示が 21 時 27 分なら、"30 21 * * *"(21 時 30 分)のままでかまいません。14 時 5 分なら、"8 14 * * *"(14 時 8 分)にします。ほかの行は変えません。timeZone が "Asia/Tokyo" なので、この時刻は日本時間として読まれます。

参考:CronJob | Kubernetes、CronJob | Kubernetes

時刻をあまり近くにしないのは、マニフェストを渡す前にその時刻を過ぎてしまうと、Job が作られるのが翌日になると考えられるからです。過ぎてしまったときは、schedule をもう一度先の時刻に書き換えて、このあとの kubectl apply -f hello-cron.yaml --context kind-kind をやり直せばよいと考えられます。第7回で見たとおり、kubectl apply はすでにあるオブジェクトの変わった部分を更新します。

kind のクラスタの kube-controller-manager がどのタイムゾーンで動いているかは、公式ドキュメントでは確かめられませんでした。そのため、この回では timeZone を書いて、タイムゾーンをはっきりさせています。

1 つ目の端末で、Job を監視します。公式ドキュメントの CronJob の手順と同じコマンドです。この監視は片付けの前まで止めずに続けます。

console
$ kubectl get jobs --watch --context kind-kind

参考:Running Automated Tasks with a CronJob | Kubernetes

hello-job と fail-job の 2 行が表示されたあと、止まったように見えるはずです。

2 つ目の端末で、マニフェストを渡し、CronJob を表示します。

console
$ kubectl apply -f hello-cron.yaml --context kind-kind
$ kubectl get cronjob hello-cron --context kind-kind

公式ドキュメントの CronJob の手順の例(毎分動く hello という CronJob)では、作った直後の表示は次のとおりです。

NAME    SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hello   */1 * * * *   False     0        <none>          10s

参考:Running Automated Tasks with a CronJob | Kubernetes

公式ドキュメントは、この表示から、CronJob がまだ Job を予定も実行もしていないことがわかる、と説明しています。例の SCHEDULE 列の */1 * * * * は、例のマニフェストの "* * * * *" と書き方が違います。*/1(1 分ごと)と *(毎分)は同じ意味の書き方で、公式の例の出力とマニフェストで書き方が揃っていないだけと考えられます。手元では、SCHEDULE 列にマニフェストに書いた 30 21 * * * のような値が出ると考えられます。LAST SCHEDULE 列は、まだ Job を作っていないので <none> になるはずです。手元の表示は例より列が多いことがあると考えられます。SCHEDULE 列と LAST SCHEDULE 列を探して読んでください。

指定した時刻になるまで待ちます。時刻になると、1 つ目の端末に hello-cron- で始まる名前の Job の行が書き足されていくはずです。公式ドキュメントの例では、表示は次のとおりです。

NAME               COMPLETIONS   DURATION   AGE
hello-4111706356   0/1                      0s
hello-4111706356   0/1           0s         0s
hello-4111706356   1/1           5s         5s

参考:Running Automated Tasks with a CronJob | Kubernetes

手元では、名前が hello-cron- の後ろに数が付いた形になると考えられます。COMPLETIONS 列は成功した Pod の数と completions の値を / で並べたものと考えられ、0/1 から 1/1 に変わるはずです。cronjob-controller は 10 秒ごとに確かめているので、行が現れるのは指定した時刻ちょうどとは限らず、少し遅れることがあると考えられます。手元の表示は例より列が多いことがあると考えられます。

COMPLETIONS 列が 1/1 になったら、2 つ目の端末で、もう一度 CronJob を表示します。

console
$ kubectl get cronjob hello-cron --context kind-kind

公式ドキュメントの例では、Job を作ったあとの表示は次のとおりです。

NAME    SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hello   */1 * * * *   False     0        50s             75s

参考:Running Automated Tasks with a CronJob | Kubernetes

公式ドキュメントは、LAST SCHEDULE 列の時点で Job が予定どおり作られたこと、ACTIVE 列の 0 は Job が完了したか失敗して、動いている Job が無いことを表す、と説明しています。手元でも LAST SCHEDULE 列に、Job が作られてからの時間が出るはずです。

続けて、Job が作られた時刻を正確に確かめます。<Job の名前> には、1 つ目の端末に表示された hello-cron- で始まる名前を入れます。アノテーションのキーには . が含まれるので、JSONPath のページの例と同じく、\. と書いて区切りの記号ではないことを示します。1 行目は Job のアノテーション、2 行目は CronJob の status.lastScheduleTime です。

console
$ kubectl get job <Job の名前> -o jsonpath='{.metadata.annotations.batch\.kubernetes\.io/cronjob-scheduled-timestamp}{"\n"}' --context kind-kind
$ kubectl get cronjob hello-cron -o jsonpath='{.status.lastScheduleTime}{"\n"}' --context kind-kind

参考:CronJob | Kubernetes、CronJob | Kubernetes API Reference、JSONPath Support | Kubernetes

1 行目には、Job がもともと作られる予定だった時刻が RFC3339 の形式で表示されるはずです。RFC3339 の時刻は一般に、末尾が Z なら UTC、+09:00 なら UTC より 9 時間進んだ時刻(日本時間)を表します。末尾が Z で表示された場合、時刻は指定した日本時間より 9 時間前の値になるはずです。どちらの表記でも、日本時間に読み替えると schedule に書いた時刻の 0 秒を指しているはずです。2 行目の lastScheduleTime も同じ時刻を指していると考えられます。どちらの表記で表示されるかは、公式ドキュメントでは確かめられませんでした。

最後に、Job の Pod のログを表示します。

console
$ kubectl logs jobs/<Job の名前> --context kind-kind

公式ドキュメントの例では、出力は次のとおりです。

Fri Feb 22 11:02:09 UTC 2019
Hello from the Kubernetes cluster

参考:Running Automated Tasks with a CronJob | Kubernetes

手元でも 2 行が表示され、1 行目はコンテナの中の date が書き出した時刻になるはずです。例と同じく UTC の時刻が表示された場合は、指定した日本時間より 9 時間前の時刻になると考えられます。timeZone はスケジュールの読み方を決めるもので、コンテナの中の時計を変えるものではないからです。

schedule は毎日 1 回なので、このままにしておくと、翌日の同じ時刻にも Job が作られます。このあとの片付けで CronJob を消します。

片付け

1 つ目の端末で Ctrl+C を押して、監視を止めます。

2 つ目の端末で、作った 2 つの Job と CronJob を、マニフェストを指定して削除します。公式ドキュメントによると、kubectl で Job を消すと、Job が作った Pod もすべて消されます。CronJob を消すと、CronJob が作ったすべての Job と Pod が消され、新しい Job も作られなくなります。

console
$ kubectl delete -f hello-job.yaml -f fail-job.yaml -f hello-cron.yaml --context kind-kind

参考:Jobs | Kubernetes、Running Automated Tasks with a CronJob | Kubernetes、kubectl delete | Kubernetes

少し待ってから、残っていないかを確かめます。

console
$ kubectl get cronjobs,jobs,pods --context kind-kind

Pod が Terminating で表示されていたら、少し待ってからもう一度実行します。第8回と同じく、見つからないことを知らせる 1 行が出れば、削除できています。

最後に、クラスタを削除し、消えたことを確かめます。

console
$ kind delete cluster
$ kind get clusters

kind delete cluster は存在しないクラスタを指定してもエラーを返しません。kind get clusters に kind が表示されなくなったことを確かめます。kubeconfig に kind-kind のコンテキストが残っていたら、第3回の片付けと同じく kubectl config delete-context kind-kind で消します。

参考:Quick Start | kind、kubectl config | Kubernetes

注意点

Job と CronJob でつまずきやすい点と、バージョンで変わる点を、次の表にまとめます。

項目内容
restartPolicy は必ず書くJob の Pod に書けるのは Never か OnFailure だけ。書かなければ既定値の Always になる
selector は書かないJob の spec.selector は、ほとんどの場合、指定すべきではない。システムがその Job だけのラベルを選んで設定する
backoffLimit の既定は 6待ち時間は 10 秒、20 秒、40 秒……と延び、10 分で頭打ち。試すときは小さくする
OnFailure では Pod も止められるbackoffLimit に達すると、OnFailure の Pod も終了させられ、調べにくくなる。調べるあいだは Never にするか、ログを集める仕組みを使う
失敗した Job は自動ではやり直されないbackoffLimit や activeDeadlineSeconds で Failed になった Job は、利用者が手で対応する
終わった Job と Pod は残る古い Job を消すのは利用者の仕事。自動で消すなら ttlSecondsAfterFinished を書く
スケジュールのタイムゾーンtimeZone を書かなければ、kube-controller-manager のローカルのタイムゾーンで読まれる。schedule の中の CRON_TZ= や TZ= は受け付けられない
CronJob の名前は 52 文字までcronjob-controller が 11 文字を足し、Job の名前は 63 文字までのため
処理は冪等にしておくCronJob は時刻ごとに「およそ 1 回」Job を作る。Job も同じプログラムを 2 回始めることがある
ひな形の変更は新しい Job からCronJob を書き換えても、すでに始まった Job と Pod は変わらない

参考:Jobs | Kubernetes、Job | Kubernetes API Reference、CronJob | Kubernetes、CronJob | Kubernetes API Reference

公式ドキュメントは Job に ttlSecondsAfterFinished を書くことを勧めています。CronJob などを通さずに直接作った Job では、既定の削除の方針が orphanDependents(従属物を残す)になっていて、Job が消されたあとも Pod が残る原因になるからです。消された Job の Pod は、失敗か完了したあと、やがてコントロールプレーンが片付けます。それでも残った Pod がクラスタの性能を落とすことがある、と公式ドキュメントは注意しています。ある Namespace が使える資源の量に上限を設けるには LimitRange と ResourceQuota を使える、とも書かれています(第30回で扱います)。

一方、第6回で見たとおり、kubectl delete の --cascade の既定は background で、持ち主を消したあと、ガベージコレクターが従属物も消します。そのため、この回の手順のように kubectl で Job を消せば、Pod も消えます。orphanDependents が当てはまるのは、削除の方針を指定せずに kube-apiserver へ削除を直接頼む場合などだと考えられます。

参考:Jobs | Kubernetes、Jobs | Kubernetes、kubectl delete | Kubernetes、Garbage Collection | Kubernetes

TTL の仕組みには時計のずれについての注意もあります。公式ドキュメントによると、この機能はクラスタの中の時計のずれの影響を受け、間違った時刻に Job を片付けることがあります。0 以外の TTL を設定するときは、このことに気を付けるよう書かれています。

参考:Automatic Cleanup for Finished Jobs | Kubernetes

関連記事

アプリケーションの設定を Pod の外に分けて渡す ConfigMap と、環境変数とボリュームでの渡し方の違いについては、以下の記事で解説しています。

基本仕様 ・ 第17回 環境変数とボリュームで渡す ConfigMap の違い設定のデータをコンテナイメージから切り離して Pod に渡す ConfigMap の仕組みを図で説明します。環境変数とボリュームの 2 つの渡し方と、ConfigMap を書き換えたときにボリュームのファイルは後から更新され、環境変数は自動では更新されない理由を押さえ、kind のクラスタで確かめる手順を紹介します。

この回で Job の Pod を選ぶのに使ったラベルと、CronJob の予定の時刻が書かれていたアノテーションの違いについては、以下の記事で解説しています。

基本仕様 ・ 第42回 ラベルとアノテーションの違いリソースに付けるキーと値の組であるラベルとアノテーションの違いを図で説明します。選ぶためのラベルと選ぶためではないアノテーションの区別、書き方の決まり、推奨のラベル、複数の次元で分ける設計を押さえ、kind のクラスタでラベルで絞り込んでまとめて操作する手順を紹介します。

注意点で触れたとおり、公式ドキュメントは残った Pod に備える方法として LimitRange と ResourceQuota を挙げています。Namespace ごとに使える資源に上限を設けるこの 2 つについては、以下の記事で解説しています。

基本仕様 ・ 第30回 LimitRange と ResourceQuota の違いNamespace ごとに資源の使い方を制限する LimitRange と ResourceQuota の違いを図で説明します。1 つずつの既定値と範囲を決める LimitRange と、Namespace 全体の合計を決める ResourceQuota が、どちらもアドミッションで働くことを押さえ、kind のクラスタで上限を超える Pod の作成が拒否される様子を試す手順を紹介します。

Kubernetes 基本仕様の目次へ