(最後に日本語版があります)
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
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;
- Convert the request body to admission.k8s.io/v1.AdmissionReview.
- If the requested resource(Job) is owned by any CronJob, create a JSON Patch to add the label “uzimihsr.github.io/cronjob-name={Job.metadata.ownerReferences[0].name}”.
- You must escape
/
in a JSON key.
- You must escape
- Convert the JSON Patch to admission.k8s.io/v1.AdmissionReview and return it.
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.)
つくったもの
Kubernetes
のJob
で起動した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
が作成できたら、そのPod
をClusterIP 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
ラベルをつけたおかげでCronJob
のJob
がそこそこ速く絞り込めるようになった。
やったぜ。
おわり
admission webhookを自分で作って動かしてみた。
いい勉強になったし、自分で使う上でもそこそこ便利なものができたと思っている。
カッコつけてそれっぽくリポジトリをつくったものの、まだテストが書けてなかったりするので暇なときにちょくちょく修正していきたい。