Featured image of post Add a cronjob-name label to Kubernetes Jobs with Mutating Admission Webhook

Add a cronjob-name label to Kubernetes Jobs with Mutating Admission Webhook

Kubernetes JobにCronJob名のlabelをつけるMutatingAdmissionWebhookをつくった

(最後に日本語版があります)

cronjob-name labels admission webhook

Kubernetes Pods owned by Job have a job-name label, so you can easily filter Pods by the name of the owner Job with label selector.
Meanwhile, Jobs owned by CronJob don’t have such one.

# filter Pods by Job with labelSelector
$ kubectl get pod -l job-name=my-job
NAME           READY   STATUS      RESTARTS   AGE
my-job-zm4p4   0/1     Completed   0          25h

# Jobs do not have any labels related to the owner CronJob
$ kubectl get job --show-labels
NAME                  COMPLETIONS   DURATION   AGE     LABELS
my-cronjob-27322002   1/1           2s         2m26s   controller-uid=43e62299-838b-4f9a-96b7-e35cfc82a2ec,job-name=my-cronjob-27322002
my-cronjob-27322003   1/1           2s         86s     controller-uid=56130637-ae62-42f4-b8f5-f5248c929ec0,job-name=my-cronjob-27322003
my-cronjob-27322004   1/1           3s         26s     controller-uid=308090e7-2bdb-4960-bd38-ff88b3c0aaf0,job-name=my-cronjob-27322004
my-job                1/1           3s         25h     controller-uid=3369540c-552b-438c-8767-8808d9c42fe6,job-name=my-job

Of course, Jobs also have a field called OwnerReferences which has the name of the owner CronJob, but it can’t be used for filtering Jobs.

# metadata.ownerReferences have the name of the owner CronJob
$ kubectl get job my-cronjob-27322004 -o jsonpath="{.metadata.ownerReferences[0].name}"
my-cronjob

# ownerReferences cannot be used to filter Job
$ kubectl get job --field-selector ".metadata.ownerReferences[0].name=my-cronjob"
Error from server (BadRequest): Unable to find "batch/v1, Resource=jobs" that match label selector "", field selector ".metadata.ownerReferences[0].name=my-cronjob": field label ".metadata.ownerReferences[0].name" not supported for Job

Therefore, when there are many Jobs in the Cluster, it is difficult to find the Jobs by the name of the owner CronJob.
Of course you can use grep or jq with kubectl, but it is sometimes not fast :(

To solve this problem, I created my own Mutating Admission Webhook for the first time.
https://github.com/uzimihsr/cronjob-name-labels-admission-webhook

# Jobs have a cronjob-name label and it can be used for filtering
$ kubectl get job -l uzimihsr.github.io/cronjob-name=my-cronjob --show-labels
NAME                  COMPLETIONS   DURATION   AGE    LABELS
my-cronjob-27322012   1/1           2s         2m26s  uzimihsr.github.io/cronjob-name=my-cronjob
my-cronjob-27322013   1/1           2s         86s    uzimihsr.github.io/cronjob-name=my-cronjob
my-cronjob-27322014   1/1           2s         26s    uzimihsr.github.io/cronjob-name=my-cronjob

Prerequisites

  • Kubernetes v1.21.1
  • kind v0.11.1
  • Go 1.17.3
  • OpenSSL 3.0.0

How it works

composition

Mutating Admission Webhook works as an HTTP server that returns responses to admission requests and we can make it with any language.
In this case, I used the webhook example written in Go.

This HTTP server works as follows;

The HTTP server can be located anywhere as long as it is reachable from the Kubernetes API, but I preferred to run it as Deployment in the cluster.

The TLS Secret is needed because the admission webhook requires a TLS connection between the Kubernetes API and the HTTP server.
If the HTTP server runs in the cluster, self-signed certificate is sufficient.

The ClusterIP Service exposes the Pod to the cluster.
It is referenced as "(service-name).(service-namespace).svc” from within the cluster, so the certificate of the TLS Secret must have that name as SANs.

MutatingWebhookConfiguration allows the Kubernetes API to send admission requests to the HTTP server.
It contains the conditions to send webhooks and base64-encoded CA certificate of the self-signed certificate.

How to install

The container image and manifest files are available on GitHub.

https://github.com/uzimihsr/cronjob-name-labels-admission-webhook#install

# generate a private key
openssl genrsa -out tls.key

# create a self-signed certificate
# Common Name (e.g. server FQDN or YOUR name) []:cronjob-name-labels-admission-webhook.default.svc (if you run Pod in the default namespace)
openssl req -x509 -key tls.key -out tls.crt -days 3650 -addext 'subjectAltName = DNS:cronjob-name-labels-admission-webhook.default.svc'

# create a TLS Secret
kubectl create secret tls cronjob-name-labels-admission-webhook-tls-secret --cert=tls.crt --key=tls.key

# create Deployment, Service, and MutatingWebhookConfiguration
# replace "CA_BUNDLE" in MutatingWebhookConfiguration by base64-encoded tls.crt
curl https://raw.githubusercontent.com/uzimihsr/cronjob-name-labels-admission-webhook/main/manifests/manifest.yaml \
  | sed "s/CA_BUNDLE/$(kubectl get secret cronjob-name-labels-admission-webhook-tls-secret -o jsonpath='{.data.tls\.crt}')/g" \
  | kubectl apply -f -

Once all the resources have been successfully created and the Pod is running, create some CronJobs to verify the operation.

# create some CronJobs
kubectl create cronjob cronjob-a --image=busybox --schedule="*/1 * * * *" -- date
kubectl create cronjob cronjob-b --image=busybox --schedule="*/1 * * * *" -- date
kubectl create cronjob cronjob-c --image=busybox --schedule="*/1 * * * *" -- date

# create a "normal" Job (not owned by CronJob)
kubectl create job my-job --image=busybox -- date

Wait for while and check the Jobs.

# Jobs owned by CronJob have a cronjob-name label
# The "normal" Job does not have the label
$ kubectl get job --show-labels
NAME                 COMPLETIONS   DURATION   AGE     LABELS
cronjob-a-27322073   1/1           6s         2m23s   uzimihsr.github.io/cronjob-name=cronjob-a
cronjob-a-27322074   1/1           5s         83s     uzimihsr.github.io/cronjob-name=cronjob-a
cronjob-a-27322075   1/1           5s         23s     uzimihsr.github.io/cronjob-name=cronjob-a
cronjob-b-27322073   1/1           2s         2m23s   uzimihsr.github.io/cronjob-name=cronjob-b
cronjob-b-27322074   1/1           4s         83s     uzimihsr.github.io/cronjob-name=cronjob-b
cronjob-b-27322075   1/1           3s         23s     uzimihsr.github.io/cronjob-name=cronjob-b
cronjob-c-27322073   1/1           4s         2m23s   uzimihsr.github.io/cronjob-name=cronjob-c
cronjob-c-27322074   1/1           2s         83s     uzimihsr.github.io/cronjob-name=cronjob-c
cronjob-c-27322075   1/1           6s         23s     uzimihsr.github.io/cronjob-name=cronjob-c
my-job               1/1           8s         23s     controller-uid=27160b7f-65e0-4e48-aa6b-9319e162f422,job-name=my-job

# Wow you can filter Jobs by CronJob :)
$ kubectl get job -l uzimihsr.github.io/cronjob-name=cronjob-b
NAME                 COMPLETIONS   DURATION   AGE
cronjob-b-27322073   1/1           2s         2m39s
cronjob-b-27322074   1/1           4s         99s
cronjob-b-27322075   1/1           3s         39s

I did it. It’s working!

Finally, let’s compare the speed of filtering Jobs by the name of the CronJob with other methods.

# create 100 CronJobs
for i in $(seq -w 100); do 
    kubectl create cronjob cronjob-"${i}" --image=busybox --schedule="*/1 * * * *" -- date
done

## look for Jobs owned by the CronJob "cronjob-099" in different ways

# kubectl + grep: 0.277(kubectl) + 0.276(grep) ≃ 0.5 sec.
$ time kubectl get job | grep cronjob-099
cronjob-099-27322103   1/1           2m29s      6m33s
cronjob-099-27322104   1/1           5m35s      5m41s
cronjob-099-27322105   0/1           4m39s      4m39s
cronjob-099-27322106   0/1           3m55s      3m55s
cronjob-099-27322107   0/1           2m39s      2m39s
cronjob-099-27322108   0/1           109s       109s
cronjob-099-27322109   0/1           41s        42s
kubectl get job  0.25s user 0.04s system 105% cpu 0.277 total
grep --color=auto cronjob-099  0.00s user 0.00s system 2% cpu 0.276 total

# kubectl + jq: 0.642(kubectl) + 0.648(jq) ≃ 1.2 sec.
$ time kubectl get job -o json | jq '.items[] | select(.metadata.ownerReferences[]?.name=="cronjob-099") | .metadata.name'
"cronjob-099-27322103"
"cronjob-099-27322104"
"cronjob-099-27322105"
"cronjob-099-27322106"
"cronjob-099-27322107"
"cronjob-099-27322108"
"cronjob-099-27322109"
kubectl get job -o json  0.70s user 0.06s system 117% cpu 0.642 total
jq   0.08s user 0.01s system 13% cpu 0.648 total

# Label Selector "uzimihsr.github.io/cronjob-name" : ≃ 0.1 sec.
$ time kubectl get job -l uzimihsr.github.io/cronjob-name=cronjob-099
NAME                   COMPLETIONS   DURATION   AGE
cronjob-099-27322103   1/1           2m29s      6m40s
cronjob-099-27322104   1/1           5m35s      5m48s
cronjob-099-27322105   0/1           4m46s      4m46s
cronjob-099-27322106   0/1           4m2s       4m2s
cronjob-099-27322107   0/1           2m46s      2m46s
cronjob-099-27322108   0/1           116s       116s
cronjob-099-27322109   0/1           48s        49s
cronjob-099-27322110   0/1           1s         1s
kubectl get job -l uzimihsr.github.io/cronjob-name=cronjob-099  0.08s user 0.03s system 113% cpu 0.094 total

Wow the label added by Mutating Admission Webhook makes it so fast to filter Jobs by the name of owner CronJob!

Thank you for reading :)


(the following is the Japanese version.)

つくったもの

KubernetesJobで起動したPodにはjob-nameというラベルがついている。
これにより、“hogehogeというJobで起動されているPodの一覧が欲しい"といったときはKubernetes APIへのリクエスト時にlabelSelectorを指定することで絞り込みができる。

# job-nameラベルを用いたPodの絞り込み
$ kubectl get pod -l job-name=my-job
NAME           READY   STATUS      RESTARTS   AGE
my-job-zm4p4   0/1     Completed   0          25h

しかし、CronJobによって起動されたJobにはそのようなラベルがなく、
“fugafugaというCronJobで起動されているJobの一覧が欲しい"というときに不便。

# JobにはCronJob名のラベルがない
$ kubectl get job --show-labels
NAME                  COMPLETIONS   DURATION   AGE     LABELS
my-cronjob-27322002   1/1           2s         2m26s   controller-uid=43e62299-838b-4f9a-96b7-e35cfc82a2ec,job-name=my-cronjob-27322002
my-cronjob-27322003   1/1           2s         86s     controller-uid=56130637-ae62-42f4-b8f5-f5248c929ec0,job-name=my-cronjob-27322003
my-cronjob-27322004   1/1           3s         26s     controller-uid=308090e7-2bdb-4960-bd38-ff88b3c0aaf0,job-name=my-cronjob-27322004
my-job                1/1           3s         25h     controller-uid=3369540c-552b-438c-8767-8808d9c42fe6,job-name=my-job

一応Jobはmetadata.ownerReferencesというフィールドにどのCronJobによって作成されたかの情報を持っているのだが、
このフィールドはfieldSelectorに対応していないので少し使いづらい。

# metadata.ownerReferencesには一応CronJobの情報がある
$ kubectl get job my-cronjob-27322004 -o jsonpath="{.metadata.ownerReferences[0].name}"
my-cronjob

# fieldSelectorで指定しようとするとエラーになる
$ kubectl get job --field-selector ".metadata.ownerReferences[0].name=my-cronjob"
Error from server (BadRequest): Unable to find "batch/v1, Resource=jobs" that match label selector "", field selector ".metadata.ownerReferences[0].name=my-cronjob": field label ".metadata.ownerReferences[0].name" not supported for Job

このため、CronJobを大量に使っているときに調べたい対象のJobを絞るのが面倒で困っていた。
…だったら自分でcronjob-name的なラベルをつけちゃえば良いのでは? と思い、
最近勉強しているadmission webhookを使って実現してみた。

https://github.com/uzimihsr/cronjob-name-labels-admission-webhook

# JobにCronJob名のラベルがついたのでlabelSelectorで絞り込める
$ kubectl get job -l uzimihsr.github.io/cronjob-name=my-cronjob --show-labels
NAME                  COMPLETIONS   DURATION   AGE    LABELS
my-cronjob-27322012   1/1           2s         2m26s  uzimihsr.github.io/cronjob-name=my-cronjob
my-cronjob-27322013   1/1           2s         86s    uzimihsr.github.io/cronjob-name=my-cronjob  
my-cronjob-27322014   1/1           2s         26s    uzimihsr.github.io/cronjob-name=my-cronjob

環境

  • Kubernetes v1.21.1
  • kind v0.11.1
  • Go 1.17.3
  • OpenSSL 3.0.0

しくみ

今回は下記のような構成でつくってみた。

こんなかんじ

まずは決められた形式のリクエストに対してレスポンスを返すHTTPサーバーをDeploymentとして立てる。
Mutating Admission Webhookでラベルを付与する処理はGoで書かれた公式の例を参考にした。

このサーバーの処理の流れとしては

  • リクエストボディをadmission.k8s.io/v1.AdmissionReviewにパース
  • リクエストされているリソース(Job)のObjectをチェックし、対象のリソースがCronJobによって作成されている場合のみ “uzimihsr.github.io/cronjob-name=(CronJob名)” のラベルを付与するJSON Patchを作成
    • 今回はラベルのキーに/が含まれるので、keyの指定方法に気をつけた
  • 作成したJSONパッチをadmission.k8s.io/v1.AdmissionReviewに詰めて返す

といった感じ。

また、admission webhookでは対象のエンドポイントとの通信がTLS化されている必要があるので、
オレオレ証明書のTLS Secretを用意してPodにマウントして使用する。

Deploymentが作成できたら、そのPodClusterIP Serviceでクラスタ内に公開する。
このときクラスタ内から参照するホスト名が "(service-name).(service-namespace).svc” となるので、前述のTLS SecretのTLS証明書のSANsがこの名前を含む必要がある。

最後にMutatingWebhookConfigurationを作成すると、
Kubernetes APIが指定されたAPIオブジェクトを操作するときに指定されたService宛にwebhookするようになる。
今回はオレオレ証明書を使用しているので、信頼するCA証明書(すなわちオレオレ証明書そのもの)をbase64エンコードしたものをwebhooks.clientConfig.caBundleに指定する。

つかいかた

Deployment用のimageと各種yamlファイルはGitHubに用意しているので、
あとはTLS証明書用のSecretさえ用意すれば動くはず。

https://github.com/uzimihsr/cronjob-name-labels-admission-webhook#install

# 秘密鍵の作成
openssl genrsa -out tls.key

# オレオレ証明書の作成
# Common Name (e.g. server FQDN or YOUR name) []:cronjob-name-labels-admission-webhook.default.svc とする
openssl req -x509 -key tls.key -out tls.crt -days 3650 -addext 'subjectAltName = DNS:cronjob-name-labels-admission-webhook.default.svc'

# TLS Secretの作成
kubectl create secret tls cronjob-name-labels-admission-webhook-tls-secret --cert=tls.crt --key=tls.key

# Deployment, Service, MutatingWebhookConfigurationの作成
# MutatingWebhookConfigurationのcaBundlerはbase64化したtls.crtを指定する
curl https://raw.githubusercontent.com/uzimihsr/cronjob-name-labels-admission-webhook/main/manifests/manifest.yaml \
  | sed "s/CA_BUNDLE/$(kubectl get secret cronjob-name-labels-admission-webhook-tls-secret -o jsonpath='{.data.tls\.crt}')/g" \
  | kubectl apply -f -

すべてのリソースが作成できたら、動作確認用のCronJobを作成する。

# CronJobの作成
kubectl create cronjob cronjob-a --image=busybox --schedule="*/1 * * * *" -- date
kubectl create cronjob cronjob-b --image=busybox --schedule="*/1 * * * *" -- date
kubectl create cronjob cronjob-c --image=busybox --schedule="*/1 * * * *" -- date

# CronじゃないJobの作成
kubectl create job my-job --image=busybox -- date

すこし放置して、Jobが起動したら結果を確認する。

# JobにCronJob名のラベルがついている
# 手動で作ったJobのラベルはデフォルトのまま
$ kubectl get job --show-labels
NAME                 COMPLETIONS   DURATION   AGE     LABELS
cronjob-a-27322073   1/1           6s         2m23s   uzimihsr.github.io/cronjob-name=cronjob-a
cronjob-a-27322074   1/1           5s         83s     uzimihsr.github.io/cronjob-name=cronjob-a
cronjob-a-27322075   1/1           5s         23s     uzimihsr.github.io/cronjob-name=cronjob-a
cronjob-b-27322073   1/1           2s         2m23s   uzimihsr.github.io/cronjob-name=cronjob-b
cronjob-b-27322074   1/1           4s         83s     uzimihsr.github.io/cronjob-name=cronjob-b
cronjob-b-27322075   1/1           3s         23s     uzimihsr.github.io/cronjob-name=cronjob-b
cronjob-c-27322073   1/1           4s         2m23s   uzimihsr.github.io/cronjob-name=cronjob-c
cronjob-c-27322074   1/1           2s         83s     uzimihsr.github.io/cronjob-name=cronjob-c
cronjob-c-27322075   1/1           6s         23s     uzimihsr.github.io/cronjob-name=cronjob-c
my-job               1/1           8s         23s     controller-uid=27160b7f-65e0-4e48-aa6b-9319e162f422,job-name=my-job

# labelSelectorを使った絞り込みができる
$ kubectl get job -l uzimihsr.github.io/cronjob-name=cronjob-b
NAME                 COMPLETIONS   DURATION   AGE
cronjob-b-27322073   1/1           2s         2m39s
cronjob-b-27322074   1/1           4s         99s
cronjob-b-27322075   1/1           3s         39s

CronJobから作成されたJobに所望のラベルが追加されていて、ラベルを用いた絞り込みができることを確認できた。

最後に、CronJobから作られたJobを絞り込む際の速度を他の方法と比較してみる。

# CronJobが100個稼働している状態
for i in $(seq -w 100); do 
    kubectl create cronjob cronjob-"${i}" --image=busybox --schedule="*/1 * * * *" -- date
done

## 色んな方法でcronjob-099のJobを探してみる

# 愚直にgrepするとkubectlで0.277+grepで0.276==約0.5秒かかる
$ time kubectl get job | grep cronjob-099
cronjob-099-27322103   1/1           2m29s      6m33s
cronjob-099-27322104   1/1           5m35s      5m41s
cronjob-099-27322105   0/1           4m39s      4m39s
cronjob-099-27322106   0/1           3m55s      3m55s
cronjob-099-27322107   0/1           2m39s      2m39s
cronjob-099-27322108   0/1           109s       109s
cronjob-099-27322109   0/1           41s        42s
kubectl get job  0.25s user 0.04s system 105% cpu 0.277 total
grep --color=auto cronjob-099  0.00s user 0.00s system 2% cpu 0.276 total

# jqとの組み合わせ技だとkubectlで0.642+jqで0.648=約1.2秒...
$ time kubectl get job -o json | jq '.items[] | select(.metadata.ownerReferences[]?.name=="cronjob-099") | .metadata.name'
"cronjob-099-27322103"
"cronjob-099-27322104"
"cronjob-099-27322105"
"cronjob-099-27322106"
"cronjob-099-27322107"
"cronjob-099-27322108"
"cronjob-099-27322109"
kubectl get job -o json  0.70s user 0.06s system 117% cpu 0.642 total
jq   0.08s user 0.01s system 13% cpu 0.648 total

# webhookで付与したラベルを使うと約0.1秒で済む
$ time kubectl get job -l uzimihsr.github.io/cronjob-name=cronjob-099
NAME                   COMPLETIONS   DURATION   AGE
cronjob-099-27322103   1/1           2m29s      6m40s
cronjob-099-27322104   1/1           5m35s      5m48s
cronjob-099-27322105   0/1           4m46s      4m46s
cronjob-099-27322106   0/1           4m2s       4m2s
cronjob-099-27322107   0/1           2m46s      2m46s
cronjob-099-27322108   0/1           116s       116s
cronjob-099-27322109   0/1           48s        49s
cronjob-099-27322110   0/1           1s         1s
kubectl get job -l uzimihsr.github.io/cronjob-name=cronjob-099  0.08s user 0.03s system 113% cpu 0.094 total

ラベルをつけたおかげでCronJobJobがそこそこ速く絞り込めるようになった。
やったぜ。

おわり

admission webhookを自分で作って動かしてみた。
いい勉強になったし、自分で使う上でもそこそこ便利なものができたと思っている。

カッコつけてそれっぽくリポジトリをつくったものの、まだテストが書けてなかったりするので暇なときにちょくちょく修正していきたい。

おまけ

キャリーバッグに入るそとちゃん