こんにちは。lyluckです。
複数の似たようなJobやCronJobを記述するとほとんどの設定がコピペになり、管理が非効率になる問題を解決するOSSを作りました。 github.com
背景
オンプレミスで稼働しているバッチ処理をKubernetes(以下、k8s)へ移行する場合、シンプルに実現する場合はJob(以下、bJob)やCronJob(以下、bCronJob)をクラスター上に作成することになります。
移行対象のバッチ処理は一つや二つではなく、大量にありました。大部分は同じ内容なのですが、コマンドの引数など細かい差異がありました。
マニフェスト管理には基本的にkustomizeを使っています。環境ごとの差分は簡単に書けますが、元(base)となるマニフェストはバッチ処理ごとに必要でした。
シェル芸でなんとかする方法(テンプレートのyamlファイルの特定の箇所を引数によって別の値に置換)も考えましたが
- 差異の種類が多く、スクリプトの引数で対応しきることが難しい
- 差異は単純な文字列だけではなく、構造を持つものがある(例えばresources)
といったことから、結局手でマニフェストを編集する必要があり、あまり効率がよくありません。
Helmチャートの作成も考えましたが、テキストテンプレートで頑張ってテンプレートを書くので、記述量が多くて大変だったため見送りました。
まとめ
大量の類似しているバッチ処理をk8sへ効率よく移行するため、複数のbJobやbCronJobを共通部分と差分のペアの形式で、効率的に生成できるCRDを実装しました。 github.com
PodProfile
: bJobやbCronJobが起動するPodのテンプレートとなるPodTemplateSpecを共通部分として定義するJob
: bJobを生成する。bJobパラメータ(ここではbJob.specのtemplate以外のこと。activeDeadlineSecondsなど)とPodProfileに対するパッチを含むCronJob
: bCronJobを生成する。bCronJobパラメータ(ここではbCronJob.specのjobTemplate以外のこと。scheduleなど)とPodProfileに対するパッチを含む
例えばこのようにPodProfileとCronJobを定義すると
apiVersion: pixiv.net/v1 kind: PodProfile metadata: name: podprofile-sample spec: template: spec: containers: - name: pi image: perl:5.34.0 command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(500)"] restartPolicy: Never --- apiVersion: pixiv.net/v1 kind: CronJob metadata: name: cronjob-sample spec: schedule: "* * * * *" jobProfile: podProfileRef: podprofile-sample patches: - op: replace path: /spec/containers/0/command value: ["perl", "-Mbignum=bpi", "-wle", "print bpi(10)"] jobParams: activeDeadlineSeconds: 120
以下のようなbCronJobを生成します。
apiVersion: batch/v1 kind: CronJob metadata: name: cronjob-sample-pxvcjob ownerReferences: - apiVersion: pixiv.net/v1 blockOwnerDeletion: true controller: true kind: CronJob name: cronjob-sample uid: 6b35de70-fcd0-4a6d-a35d-5f8cfe08a5de uid: 364495ba-0239-4e7b-9911-f85059726f68 spec: concurrencyPolicy: Allow failedJobsHistoryLimit: 1 jobTemplate: metadata: creationTimestamp: null spec: activeDeadlineSeconds: 120 backoffLimit: 6 completionMode: NonIndexed suspend: false template: metadata: creationTimestamp: null spec: containers: - command: - perl - -Mbignum=bpi - -wle - print bpi(10) image: perl:5.34.0 imagePullPolicy: IfNotPresent name: pi resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst restartPolicy: Never schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 schedule: '* * * * *' successfulJobsHistoryLimit: 3 suspend: false
詳細
CRDとは
カスタムリソースの定義のことです。カスタムリソースとはk8s APIを拡張するものです。
k8s標準のPodやbJobなどとは別の、新しい種類の独自のリソースとその振る舞いを定義できます(オペレーターパターン)。
実現したいこと
今回、CRDで実現したいことはこれらです。
- 複数のバッチ処理に共通するマニフェストの部分を柔軟に定義できる
- 共通部分のマニフェストに柔軟にパッチをあてることができる
- 上の二つを利用して、bJob, bCronJobを生成できる
定義するカスタムリソース
次のカスタムリソースを定義することにしました。
PodProfile
: マニフェストの共通部分Job
: 共通部分にパッチをあててbJobを作るCronJob
: 共通部分にパッチをあててbCronJobを作る
APIスキーマを定義する
カスタムリソースのマニフェストに何を書けるかを決めます。
例えばPodのマニフェストではspecの下にcontainersやvolumesを記述できますが、独自のリソースでは何を書けるようにしたいか、ということです。
PodProfile
どのbJob, bCronJobにも共通して存在するPodTemplateSpecを保持させます。PodTemplateSpecとは次のbJobのspec.template.spec以下のことです。
apiVersion: batch/v1 kind: Job metadata: name: pi spec: template: spec: containers: - name: pi image: perl:5.34.0 command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] restartPolicy: Never backoffLimit: 4
Job
次の三つを保持させます。
- テンプレートとして参照するPodProfileの名前
- PodTemplateSpecにあてるパッチ
- bJobパラメータ
CronJob
次の三つを保持させます。
- テンプレートとして参照するPodProfileの名前
- PodTemplateSpecにあてるパッチ
- bCronJobパラメータ
CRDプロジェクトの雛形を作る
kubebuilderを使いました。kubebuilderはSIG API Machineryのプロジェクトで、CRDを作るためのフレームワーク、ライブラリやコード生成ツールの集まりです。ドキュメントが充実しており、入門しやすいです。
CRDとは
- APIスキーマ
- controller: マニフェストの通りのワークロードをk8sクラスターに反映し続けるプロセス
- controllerをk8sクラスターにデプロイするためのマニフェスト
などからなるものです。ゼロベースで書くと記述量が多いです。
パッチのあてかた
JobやCronJobが保持するパッチを、PodProfileが保持するPodTemplateSpecに適用し、bJobやbCronJobのPodTemplateSpecを生成する必要があります。
パッチの適用方法としては2通り考えましたが、json patchを採用しました。
どう違うのかを説明します。説明のためにオリジナル(パッチをあてる対象)のマニフェストが以下だとします。適用したいパッチの内容はspec.template.spec.containers[0].nameをmainに変更することだとします。
apiVersion: batch/v1 kind: Job metadata: name: pi spec: template: spec: containers: - name: pi # pi を main に変えたい image: perl:5.34.0 command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] restartPolicy: Never backoffLimit: 4
patches strategic mergeでは以下のように書きます。
apiVersion: batch/v1 kind: Job metadata: name: pi spec: template: spec: containers: - name: main
json patchでは以下のように書きます。
- path: /spec/template/spec/containers/0/name op: replace value: main
patches strategic mergeは元のマニフェストと構造が似ていてわかりやすいですが、変更したい箇所までの構造を書く必要があります。パッチ対象を指定する情報(apiVersion, kind, metadata.name)がパッチの内容に含まれているため、使い回しが難しいです。
json patchでは構造ではなく変更箇所までのパスを指定します。こちらのほうが記述量が少なく、使い回しがききます。
パッチの実装
2通り考えました。
- k8sライブラリを使ってマニフェストのstructにパッチをあてる
- kubectl kustomizeコマンドをプロセス呼び出しする
後者を採用しました。
k8sライブラリを使ってマニフェストのstructにパッチをあてる
k8sのライブラリは巨大で複雑です。今回のカスタムリソースでは
を読んで適切なライブラリを見つける必要がありました。私にはjson patchの変換処理や適切な型変換がどれなのかを簡単に見破ることはできませんでした。
kubectl kustomizeコマンドをプロセス呼び出しする
kustomizationファイルに従ってマニフェストを生成するコマンドです。
kustomizationファイルやパッチ対象ファイル、パッチファイルを適切に配置、生成してコマンドを呼び出せば、パッチが適用されたマニフェストを作ってくれます。
型を気にせず、yamlへシリアライズしてコマンドを呼び出し、その結果をデシリアライズすれば目的を果たせます。
controllerの実装
与えられたJobやCronJobのマニフェストが指定する望ましい状態(spec)を、k8sクラスターに反映し続けるプロセスを実装します。
反映し続けるというところがミソで
- マニフェストが変更された
- マニフェストが依存しているリソースが変更された(ここではPodProfile)
- 反映したリソース(bJobやbCronJob)の状態が望ましい状態からはずれた
といったとき、望ましい状態に戻るような操作(Reconcile)を実行しなければなりません。
kubebuilderではそのようなイベントが発生した時にReconcileを呼び出すように簡単に設定できます。
JobのReconcile
次のような処理をします。
- Jobと依存するPodProfileのマニフェストからPodTemplateSpecを作る
- 1とJobからbJobのマニフェストを作る
- bJobを生成する
順番に説明します。
1. Jobと依存するPodProfileのマニフェストからPodTemplateSpecを作る
kubectl kustomize
を使ってパッチを適用します。
2. 1とJobからbJobのマニフェストを作る
Jobが保持しているactiveDeadlineSecondsなどのパラメータを新しいbJobマニフェストに書き込みます。
3. bJobを生成する
無条件に生成するわけではありません。Reconcileは様々なイベントから呼び出されるので、無条件だとbJobが無数に発生してしまいます。場合分けが必要です。
- 既存のbJobがない場合
- 既存のbJobが完了していて、specが2のマニフェストと異なる場合
前者はJobが所有するbJobがない場合です。 望ましい状態のbJobが存在していないので生成します。
後者はJobが所有するbJobがある場合です。 既存のbJobが完了していない場合は何もしません。Jobは一つのバッチ処理に対応するため、古いバージョンと新しいバージョンのbJobが同時に起動していると問題がある場合が多いからです。
specが異なるということは望ましいbJobの設定が異なるということです。specが異なるかどうかをすばやく判定するため、あらかじめbJobの jobs.pixiv.net/job-spec-hash
ラベルにspecのハッシュ値を記録しています。
Jobの履歴
JobはbCronJobのようにbJobの実行履歴を持ちます。実行が完了したbJobについては、成功にせよ失敗にせよログを閲覧できる猶予がほしいからです。
JobのTTL
bJobパラメータにはttlSecondsAfterFinishedがあります。完了してから指定した時間経過したら自身を消すという設定です。
素直にbJobに設定してしまうとJobとしては意図しない挙動になります。
ReconcileはbJobの削除時にも呼ばれるため、既存のbJobがTTLで削除されなくなった状態でReconcileします。その場合は新しいbJobが生成されてしまいますが、ユーザーはそのためにttlSecondsAfterFinishedを設定するわけではありません。
しかし、もともと既存bJobがなかった(Job新規作成時)のか、TTLで既存bJobがなくなって、結果として既存bJobがないのかをReconcileが見分けることは難しいです。仮にbJobの削除イベントを無視したとしても、別のイベント(PodProfileの変化など)が発生すると既存bJobがない状態でReconcileが呼ばれます。
そのため、TTLでのbJob消去処理は独自に実装しました。TTLで既存bJobがすべてなくなってしまう場合はTTLを無視します。
CronJobのReconcile
次のような処理をします。
- CronJobJobと依存するPodProfileのマニフェストからPodTemplateSpecを作る
- 1とCronJobからbCronJobのマニフェストを作る
- bCronJobマニフェストを生成もしくはapplyする
Jobと比べると簡単です。bJobは再生成が必要ですが、bCronJobの修正処理はServer-Side Applyで済むからです。
controllerのマニフェスト
controllerをk8sクラスターにデプロイするためのマニフェストです。
kubebuilderがもともとkustomize形式のマニフェストを作ってくれていましたが、利用しやすさの観点からHelmチャートにしました。kubebuilderにはHelmチャートを生成してくれるプラグインがあります。
Helmチャートではcontrollerのresourcesなど、変更可能な箇所が一覧(values.yaml)になっており把握しやすく、それと同じ構造のファイルを書けばパッチを適用できます。kustomizeでは変更したい箇所の構造を調べてパッチを書く必要があります。