ActiveJobでKubernetesのJobを作れるgemを作った

https://github.com/yuemori/kube_queuegithub.com

概要

きっかけ

ruby-jp slackの #container チャンネルで、kubernetesでのsidekiqが話題になりました。

そういえば、個人的にsidekiqのdockerでのデプロイをいい感じにできないものかと悩んでいます。
デプロイする前にsidekiqのquietして、workerプロセスが全部死んでから、
デプロイする必要があって、長いジョブがいるとなかなかデプロイされずに困るんですよね。
コンテナじゃなければ、quietした後、同じサーバーの中でまたsidekiqを立ち上げるので、
長いジョブも生き続けてくれたんですけど。。

今の所ジョブは短く作るみたいなソリューションなんですけど、
どうしたものかなぁと。ジョブ自体もcloudrun的な一つのコンテナプロセスに閉じるとか、妄想はしてるんですが。。。

自分はあまりジョブキューの管理をやりたいと思ってなくて、そういえばそもそもKubernetesにはJobっていう仕組みがあるよねって思ったのがきっかけです。

また今のプロジェクトでKubernetesAPIを叩いてゴニョゴニョするみたいな実装をしていてそういうことが出来ると知っていたのも大きいですね。

ActiveJobで perform_later が呼ばれたらKubernetesのJobを作るのって意外と簡単なのでは?と思って試しに軽く実装してみたら意外と上手く行けそうだったのでお盆休み中作業していました。

ActiveJob周り

実はActiveJobをほぼ使ったことがなく、defaultのThreadでの使い方しかまだやってなかったのでインターフェースの仕様などを知るのが一番苦労していました。

参考として実装を読んでいたのは主にこちらのgemたち。

最初はsidekiqやshoryukenを参考にしていたが、今回作るgemはジョブやキューを管理する必要がないので参考にしづらいことに割と最初の方で気づいたので、最終的にはrailsのActiveJobのコードを読んでました。

(それでもcliのインターフェース周りの実装は参考になった)

ActiveJob自体のコードは機能もそんなにないため非常に薄く、比較的最近書かれたコードなので歴史的経緯もなくて読みやすかったので、ActiveJobのadapterを書こうという稀有な人がいたらActiveJob自体を読んだほうが結果的に早いと思います。

Kubernetes周り

Kubernetesはリソース指向で、ほぼすべての機能をREST API経由で管理することが出来ます。

kubectl コマンドも内部的にはvalidationなどはコマンド側ですが、実行自体はAPIを叩いているだけなので、kubectl で実行できる大抵のことはAPI経由で出来るので非常に便利。

例えば今RailsのDeploymentのreplica数が何台かを知りたい、みたいなことは簡単にできます。

kubernetes client gem

rubykubernetes clientの実装としては、現状だと3つが候補に上がります。

kubernetes-client/ruby は公式のもので、kubernetesが持つswaggerから自動生成されたもののため一番strictです。

ただ、このclientはgenerateされただけでrubygemsには公開されておらず、gemのdependencyとして追加するにはあまりにも辛そうだったので採用を見送りました。

残る2つの選択肢としてはkubeclientの方がStarが多く業務でも使っているので使おうとしたのですが batch/v1APIを叩くのが難しそうだったため、k8s-clientを採用しました。

ハマりどころとしては、kubectlだとmanifestのtypoなどがある場合はエラーを返してくれるのですが、あのvalidationはkubectlがやっているのでAPIに投げた場合は無視されてエラーが返ってきません。

kubernetesの認証周り

k8s-clientとkubeclientはどちらも使い方としては非常にシンプルなので説明はいらなさそうですが、Kubernetesの認証周りでハマりやすいと思うのでそこだけ注意したほうが良いです。

ServiceAccount を作成して Pod から kubectl を使って Pod の情報を取得する - Qiita

この辺の記事がわかりやすいかも。ポイントとしては以下。

  • ServiceAccountを作る
  • 作ったServiceAccountにRBACで権限を振る
  • PodSpecの serviceAccountName に作ったServiceAccountを指定すると、 /var/run/secrets 以下にca.crtとtokenが自動マウントされるのでそれを使って認証

わかんなかったらKubernetes完全ガイドを読みましょう。

作った感想

Kubernetesの可能性

cookpadさんの事例としてRubyKaigi2016でk0kubunさんが発表されていたbarbequeのことは知っていて、ジョブをコンテナで管理するのって良さそうだよなあ、と思ってました。

Scalable Job Queue System Built with Docker - Speaker Deck

Kubernetesというplatformに乗ることで圧倒的に簡単にジョブをコンテナ化することが出来たのでちょっとびっくりしてます。 gemのコードを読んでもらうとわかるけれど、gem側でほとんど何もやってなくてDSL提供してmanifest作ってAPI投げてるだけ。

それでいてKubernetes Jobにはリトライ・タイムタウト・スケジューリングとたくさんの機能があるので自分で実装しなくて良いのも楽ポイント。

現状の問題点としては作られたJobやCronJobを消すのに手動対応が必要なことぐらいですが、それも現在alphaになっているttlAfterFinishedが入れば気にならなくなるんじゃないかと思います。

https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#ttl-mechanism-for-finished-jobs

Jobがより疎結合

あと当初想定していなかったこととして、コンテナを呼ぶという仕様になったおかげでJob側がRubyである必要すらなくなってしまったのはちょっと面白い。

これは公式のpiをperlで計算するJobを作る場合の例。

class ComputePiJob < ApplicationJob
  include KubeQueue::Worker

  worker_name 'pi'
  image 'perl'
  container_name 'pi'
  command "perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"
end

例えばActiveJob経由でembulkを呼び出したり、時間のかかる処理をgolangで書いて呼び出したりしながら、疎結合に出来るので結構可能性があるんじゃないかと思います。

リソース管理の変化

もう一点面白い点として、これを採用することでbackground jobに対するリソースの考え方を変えることができそうな点。

sidekiqやresqueをスケールするには現在キューイングされているジョブやジョブの処理時間などに応じてスケールする必要があり、オートスケールにはちょっと一工夫必要です。

KubernetesのJobを利用すると管理するリソースはジョブ単位で与えるresource requests/limitsとノードプールのリソースになるので、より抽象的な管理が出来るようになります。

これ自体は考え方や管理方法の変化なので必ずしもメリットではない場合がありそうが、platformとしてKubernetesを採用する場合は選択肢として入ってきそうなポイントですね。

妄想としてはこれを使ったCIシステムを作ると、containerizeされていてオートスケールしやすいruby製のCIツールという珍しいものができそうだなと思っていたり。

まとめ

作ったばっかりでまだ運用などもしてないので、興味のある人は試しに使ってみてください。そしてcontributeお待ちしてます。

あとこのgemに全然関係ない話として、

f:id:wakaba260yen:20190815223901p:plain

自分は熱しやすく冷めやすいタイプなので一過性のものかもしれないけれどruby-jpというSlackはすごいなと思ってますしコミュニティの皆さんにはめっちゃ感謝してます。