kubernetes上で単発のジョブを実行するツールを作った

kubernetesにはJobというリソースがある(CronJobもあるよ). このJobを使いたい用途が2つあった.

  1. デプロイ時のmigration等
  2. Rundeckからジョブを実行したい

というわけで,この2つを両方共満たせるものを作った.

github.com

やりたいこと

Jobの定義

Jobの原型はyamlで定義しておきたい.これはkubernetesを使う上で割と一般的な要求だと思う.

ただ,migrationはともかく,Rundeckで実行したいジョブを,その数ぶんだけ定義するというのはなかなか大変だ. もちろんジョブによってimageやresource等を変更したい場合は,面倒でもそうするしかない. しかし,imageは全て同じで,resource等の変更もなく,単に args を書き換えられれば満足である場合,これを全て定義するのはだるい.

というわけで,

  • 大本となるyamlを読み込みつつ
  • argsはCLIから任意のコマンドを差し込め
  • ジョブの完了まで同期的に待つ

ようにしたいと思った.

ログについて

また,ログについても注文がある.

migrationにしろ,Rundeckにしろ,ジョブのログは全て標準出力に流れてほしい.これは特にRundeckで顕著になる要求だが,RundeckはログをRundeck内で保存してくれる. ログはジョブの履歴とともに保存され,過去に実行されたジョブはそのログを辿ることができるようになっている. この仕組みは非常に大事で,失いたくはなかったので,ログの標準出力は絶対に外せなかった.

近いOSSたち

github.com

これあたりは割と近い. が,

  • 俺がやりたいことに対して,結構多機能である
  • WARNING: This is alpha-quality software, and may have bugs, and should not be used for production or mission-critical workloads

なので,ちょっと遠慮した.

こういう系統のOSSだと,

blog.manabusakai.com

これのように,kubectlコマンドをいくつかラップするシェルスクリプトだったりRubyスクリプトだったりすものが非常に多い. ただ個人的には,ラップしただけのコマンドはあまり保守したくない気持ちが強い.テストも書けないしね…….

どうせkubernetesを触るのであれば,

github.com

これを使えば楽そうだったので,自作したほうがよいかと思い,作った.

さすがkubernetesだけあって,小さいOSSは他にもいっぱいあるのだが,どれを使っていいとも言えない状況なので,探して試すくらいなら自作するほうが速い.

使い方

kubeconfigを作る

apiVersion: v1
clusters:
- cluster:
    server: https://example.com
    certificate-authority-data: certificate
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: hoge
  name: hoge
current-context: hoge
kind: Config
preferences: {}
users:
# ...

Jobのtemplateを作る

apiVersion: batch/v1
kind: Job
metadata:
  name: test-job
  namespace: public
  labels:
    app: test-job
spec:
  template:
    metadata:
      labels:
        app: test-job
    spec:
      containers:
      - name: echo
        image: alpine:latest
        imagePullPolicy: Always
        args: ["echo", "hoge"]
      restartPolicy: Never
  backoffLimit: 2

そして実行する

以上の2つを指定して,

$ ./kube-job run --config=$HOME/.kube/config --template-file=./job.yaml --args="echo fuga" --container="echo"
fuga

こんな感じに使える.

ジョブ失敗時の挙動

kubernetesのJobは,restartPolicy: Never を指定した場合,失敗時にPodを使いまわさず,新しいPodを生成する.この場合,ちゃんと新たに生成されたPodのログも表示するようになってる.

restartPolicy: Never ではない場合,同じPodを使いまわしてジョブを再実行するので,もちろんこの場合でもログは正常に吐き出される.

最終的に,restartされたPodを含めて,全てのPodが終了し,Jobが終了した時点での成功/失敗を見て,kube-jobコマンドも終了する.即ち,ジョブが最後まで失敗し続けた場合,kube-jobもexit 1で終了する.

内部でやっていること

  1. kubeconfigからクライアントを生み出す
  2. template-fileを確認する
  3. template-fileがURLだった場合にはダウンロードしておく
  4. containerで指定されたコンテナをtemplate-fileから探し出し,Argsをargsで上書きする
  5. Jobをcreateする
  6. Jobに紐づくPodを見つけ出す
  7. 見つかったPodの全ログを標準出力に出す
  8. Jobに紐づくPodが増えていないかを確認し,増えていた場合には7.に追加する.以降はこれを繰り返す
  9. Jobが終了したら成功/失敗に関わらずログのtailを終了しコマンド自体を終了させる
  10. 終了時に完了したジョブを削除しておく

最後に完了したジョブをクリーンする.これ専用のOSSもいくつかあるが,kube-jobを使うと実行後に勝手に削除してくれる.ログは標準出力に出るしね.

template fileとしてgithub等のファイルを使いたい

もちろんできる. template-fileはURLを指定した場合,そのファイルをダウンロードしてtemplate-fileとして利用する.

また,このtemplate-fileはgithubのprivate repositoryを指定することもできる.

$ export GITHUB_TOKEN=hogehogefugafuga
$ ./kube-job run --template-file=https://api.github.com/repos/h3poteto/k8s-services/contents/external-prd/fascia/job.yml --args="echo fuga" --container="go"

みたいなことをすると,private repositoryであっても問題なく使える. なお,ssh鍵じゃなくてpersonal access tokenを利用したのは CircleCI等にssh鍵を設置するのが面倒だから .もしprivate repositoryへのもっと良いアクセス方法があったら教えてください.

今後

作ってみたら意外に簡単だった.client-goすごいな.

せっかくgoで作っているので,もう少しテストを追加して安心感を得たい.あと,kubectlではpatchというサブコマンドがあり,これでtemplate fileの任意の項目を上書きしながらデプロイすることができる. 今は,対象となるコンテナをcontainer パラメータで指定して,なおかつ args しか上書きすることができないのだが,patchレベルまで作り込むと,任意の項目まで上書きしながらジョブを実行することができるので,便利になりそうな予感がしている.

それkubectlでよくね?とも思うのだが,

  • 1コマンドでジョブの実行終了までを同期的に待ち
  • なおかつそのログを全て標準出力に出す

という要求は,やはりkubectlだけで実現できそうにないので,作る価値はありそうな気がしている. あと,手元でやった限りJobをpatchするのって無理じゃね?