k8sのJobを用いたバッチ処理

kubernetesには、podの稼働を制御するコントローラーの機能があり、DeploymentやDaemonsetといったコントローラーが主に知られています。Jobもコントローラーの一種で、バッチ処理用途の機能を持ちます。目的のジョブを実行するときだけpod起動する点が特徴で、失敗時のリトライやタイムアウトなどの設定を追加できます。

なお、CIのような複数のプロセスを組み合わせる複雑なジョブを作る際には、k8sネイティブなジョブ基盤 Tektonを利用した方が良いでしょう。

kubernetes Jobの実行方法

Jobの起動方法は、他のコントローラーと同様、kubectlで実行できます。

$ kubectl create -f some_job.yaml

Job削除

Jobは実行完了後にjobとpodが消えずに残ります。この挙動の意図は、実行状況のログを確認する目的です。状況を確認したら、deleteします。以下のように、manifest、Job名いずれの指定でも操作できます。

$ kubectl delete -f some_job.yaml
$ kubectl delete job/JOB_NAME

spec.ttlSecondsAfterFinished というオプションを設定に追加することで完了後に自動でクリーンアップする挙動がv1.12で導入される予定です。

手軽な運用は毎回何も考えずに kubectl delete → kubectl create の2コマンドを連続実行することでしょう。

前回のJobが存在しない場合にはkubectl deleteはエラーになりますが、存在しないリソースの削除失敗に実害はありません。CIツールがexit 1で停止するのを避けたい場合には、コマンドに|| trueを追加することで正常終了を強制できます。

また、ジョブ実行後のpodについては気にせず放置となりますが、開発環境などでは実害はないでしょう。まじめに対応するならジョブの終了判定処理を開発することになりますが、非同期処理であるためそれなりの手間がかかると思います。

Jobの設定ファイル

Jobの設定ファイルの記載例は以下のようなものになります(抜粋)。

apiVersion: batch/v1
kind: Job
metadata:
  name: ridgepole
spec:
  template:
    metadata:
      name: ridgepole
    spec:
      containers:
      - name: ridgepole
        image: uniqrn/rails:5.2
        imagePullPolicy: IfNotPresent
        command: ["ridgepole"]
        args: ["-c", "database.yml", "-f", "Schemafile", "-a", "-E", "development"]
      restartPolicy: Never

この例では、ridgepoleを利用してRDBMSのスキーマを更新する処理を実行します。いくつかJob特有のキーワードがありますが、Jobを利用する際の主題は、commandとargsです。

Jobでは、コンテナイメージ内のコマンドやスクリプトを実行することになりますが、commandとargsの組み合わせで対象を指定します。

manifestにスクリプトを記述する

k8sを利用すると任意のコンテナ環境を準備でき実行環境の選択肢は広がります。最終的に実行スクリプトの定義が課題になりますが、以下のようにmanifestにシェルスクリプトを記述できます。

    spec:
      containers:
        command: ["bash", "-c"]
        args:
        - cd workdir/;
          git clone some-repo .;
          make; make install

YAMLのブロックの挙動により、argsの行は1行にまとめられ、commandbash -cに供給されます。以下の注意事項がありますが、他言語へのbashスクリプト組み込みと大きな違いはないでしょう。

  • インデントはYAMLの制約を受ける
  • 改行が除去されるため、行末のデリミタは必要。フローによっては&&などを利用する

ほかに、ConfigMapにスクリプトを定義する手もありますが、これはJob特有の手法ではないため割愛します。

埋め込みプログラム

CIツールから起動する場合、ヒアドキュメントを利用できればmanifestを埋め込んで起動できます。Jenkinsのpipelineでは以下のような定義が可能です。

manifest = """
apiVersion: batch/v1
kind: Job
spec:
  template:
...
"""

sh("""cat <<'EOF' | kubectl apply -f -
${manifest}
EOF
""")

sh()ステップ内のヒアドキュメントにmanifestを直接記述しても動作します。ただし、YAMLはインデントを死守しなくてはならないため、変数として分離した方が安全です。

ヒアドキュメントのデリミタは'EOF'のようにシングルクオート付きの指定が無難でしょう。YAMLにbashスクリプトを記述した場合、バッククオート等によるコマンド実行を抑止しないとsh()ステップのシェルが実行する挙動になります。コンテナ外のシェルに解釈させたいケースはほぼ無いでしょう。
コンテナに渡っているスクリプトは、kubectl describeコマンドの出力で確認・デバッグできます。

また、kubectl applyはジョブ起動とともに正常終了してしまいますが、以下のようにkubectl waitで完了待ちも可能です。ただし、conditionは1種類しか指定できないため、エラー時は常にタイムアウト待ちとなります。

sh("kubectl wait --for condition=complete job/job_name --timeout=180s")

また、ログはJob終了後に、kubectl logsで取り出す方法が手軽でしょう。

複数コンテナの連続実行

単機能のイメージを利用して、1つのジョブ中で複数のコンテナを動作させる構成も可能です。その場合、manifestのinitContainersにコンテナ定義を羅列します。manifestが長大になる点は宿命です。

また、コンテナ間でファイルを共有する必要がある場合には、emptyDirボリュームなどをコンテナ間で共有して読み書きします。

まとめ

Jobを利用することで、ビルドなどのバッチタスクが Infrastructure as Code となり、任意のソフトウェア・スタック上で実行できます。

manifestがプロセス定義に向いていない点など、使いにくい点は残っています。
CIツールに実装する場合、CIのプロセス定義・ジョブのmanifest・コンテナ上の実行スクリプトと3層のパラダイムが同居します。これは複雑ではあるものの冗長ではありません。1ファイルに集約できる点では、コンパクトでもあります。

また、もっともスパルタンな実装であり、CIツールのk8sインテグレーション機能と比べると圧倒的に動作が安定しているメリットがあります。

Jobコントローラーでは、起動時にコンテナ用のリソースが逼迫して確保できないというケースが起こり得ますが、この場合、稼働中のコンテナの安定性も犠牲になっているので、kubernetesのクラスタをギリギリのリソースで運用すること自体を避けるべきでしょう。

なお、本記事ではJobの基礎的な理解を優先してrestartPolicyなどサービスレベル設定について説明を割愛しました。公式ドキュメントの Job – Run to completionに利用例などを含め詳細な解説がまとまっており、参考になります。

中馬崇尋
Chuma Takahiro