つくって学ぶkubebuilder を読んだ。
簡潔によく纏まっている資料で勉強になった。 読んだ感想としてCustom Resource(CR)は自由度が高いので作りたくなるけど、 迂闊にその数を増やすと管理しきれなくなると思う。 おそらく本質的にCRでないと実現できない課題に対してのみ採用して、 かつミニマムに作ったり使ったりするのが良いんだろうと感じた。 例えばここで題材としたMarkdownViewに関しては、 現実にはCRじゃなく標準リソースの組み合わせで実現した方が良いんだろう。 もちろん題材としてはとっつきやすく良かった。
性能面はどうだろうか。 Reconcileループは1ループを軽量にしないと収束時間の予測が難しく運用しづらそう。 リトライはその状態を次のループにわたすなど、おそらく実装上のテクニックがいくつかあるんだろう。 標準のコントローラ、etcd、kube-apiserverへ与える性能影響も気になる。
Kubebuilerに相当するフレームワークは他にあるんだろうか? Kubebuilderが標準でメジャーなのかな。 ざっくり調べたところKubernetes Way(client-goとcode-generator)とOperator SDKがありそうだ。
ここからメモ。私は Kubebuilder を今回初めて触るので誤解も含まれているはず。
序章
KubebuilderはCustom ControllerやOperatorを開発するためのフレームワークで、 controller-toolsとcontroller-runtimeが含まれる。 Custom Resource(CR)のコントローラをCustom Controllerと呼ぶ。 CRの仕様はCRD(Custom Resource Definition)で定義され、これはGoの構造体から自動生成される。 Kubernetesのリソースは宣言的、冪等、レベルドリブントリガーといった特徴があるので、 それに則った形で実装する。 MarkdownViewというCustom Resource/Controllerを題材にして手を動かしながら学べる。 ここ にコードが公開されているのであわせて読むと良い。
MarkdownView Custom Resourceを使うと以下を実現できる。
- ConfigMapにMarkdownを1ファイルずつ保存する
- MarkdownのレンダラーとしてmdBookイメージを使ったDeploymentを作成する
- コンテナイメージとレプリカ数をCustom Resourceで指定する
- mdBookのDeploymentに外部からHTTPでアクセスできるようSerivceを作成する
kubebuilder
init/edit サブコマンド
まずは空のディレクトリ上でinitサブコマンドを発行してプロジェクトの雛形を作る。 途中で変更する場合にはeditサブコマンドを使う。 重要なオプションは以下の2つ。
--domain
でCRDのグループ名を指定する--repo
にGoモジュール名を指定
makeコマンドを頻繁に使うのでmake help
でmakeターゲット一覧を確認しておくと良い。
ファイル中の//+kubebuilder
は重要なマーカーなので削除しないよう注意する。
cmd/main.go
がCustom Controllerのエントリポイントとなる。
config/
配下にマニフェストが集約されている。
これらマニフェストは kustomization.yaml
でまとめて管理されている。
create サブコマンド
createサブコマンドで新たなAPIやWebhookを追加することができる。
例えばAPIの場合には、
create api
サブコマンドでCustom ResourceやCustom Controllerの雛形を生成できる。
api/v1/markdown_view_types.go
:CRDの生成元api/v1/generated.deepcopy.go
:自動生成されたものなので触らないよう注意internal/controllers
:主要なcuostom controllerの実装本体
create webhook
サブコマンドでAdmission Webhookを追加できる。
リソースの作成・更新時にデフォルト値を埋め込んだりバリデーションしたりできる。
api/v1/markdownview_webhook.go
が実装本体となる。
Webhookを有効にするためにconfig/default/kustomization.yaml
のコメントを外す。
動かす
多くの操作はmake docker-build/manifests/generate/install/uninstall/deploy
コマンドで行う。
その他に kind load docker-image controller:latest
でCustom Controller
のイメージを Kubernetes に読み込んだり、kubectl rollout restart
でCustom Controller Podを
リスタートしたり、といった操作が必要になる。
今回は試さなかったが、Tiltを使うと効率よく開発ができるようだ。
今回 https://github.com/zoetrope/kubebuilder-training/tree/main を参考にしながら、 https://github.com/bobuhiro11/markdown-view が私の作業リポジトリを作った。
手元にあるツールのバージョンは以下のとおり。
Tool | Version |
---|---|
Go | 1.20.3 |
Docker | 24.0.5 |
Kind | 0.20.0 |
Kubebuilder | 3.11.1 |
controller-gen | 0.12.0 |
Helm | 3.13.0 |
ざっくりと使い方をメモするとこんな感じ。 まずプロジェクトと初期化し、依存ツールをインストール。
mkdir markdown-view
cd markdown-view
# Init project.
kubebuilder init --domain bobuhiro11.net \
--repo github.com/bobuhiro11/markdown-view
kubebuilder create api --group view --version v1 --kind MarkdownView
kubebuilder create webhook --group view --version v1 --kind MarkdownView
--programmatic-validation --defaulting
vim config/default/kustomization.yaml
# Start kubernetes cluster.
kind create cluster
kubectl cluster-info --context kind-kind
# Deploy cert-manager.
kubectl apply --validate=false \
-f https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml
続いてCustom Controllerのイメージを生成してkubernetesクラスタにロード。
make docker-build
kind load docker-image controller:latest
kubectl rollout restart -n markdown-view-system \
deployment markdown-view-controller-manager
CRDなどのマニフェストを適用。
make install
make deploy
kubectl get crd markdownviews.view.bobuhiro11.net
# NAME CREATED AT
# markdownviews.view.bobuhiro11.net 2023-09-28T04:57:48Z
最後にCustom Resource のマニフェストを適用。
kubectl apply -f config/samples/view_v1_markdownview.yaml
kubectl port-forward svc/viewer-markdownview-sample \
3000:80 --address 0.0.0.0
controller-tools
controller-toolsは補助ツール群で、重要なツールとしてcontroller-genが含まれている。
Goコードからマニフェストなどを自動生成するもので、
Go構造体としてSpecとStatusを定義すると自動的にCRDが生成される。
自動生成にあたっていくつかマーカーを使うことができ、
一例として+kubebuilder:printcolumn
マーカーを使うとkubectlなどの出力形式を改造できる。
Specはユーザが記載するもの、Statusはコントローラが記載するものと役割分担できる。
そこで、Statusに+kubebuilder:subresource:status
マーカーをつけてサブリソースとすると良い。
サブリソースは単体で独立したAPIエンドポイントを持ち、単独で更新できるようだ。
RBACやAdmission Webhookのマニフェストもこれで生成される。
ちなみに+kubebuilder
マーカーの一覧とそれらの説明を確認できる。
./bin/controller-gen crd -w # or -www
./bin/controller-gen webhook -w # or -www
type-scaffoldとhelpgenといったツールも含まれているようだが、よく分からなかったので後で調べる。
controller-runtime
client.Clientの埋め込み
MarkdownViewReconcilerがCustom Controllerの中心的な実装に相当する。
MarkdownViewReconcilerは client.Client
を埋め込んでおり、
r.Get()
や r.Create()
のような操作をいくつかのリソースに対して発行しながら
Reconile処理を実現する。
リソースの更新
リソースの更新については競合が発生する可能性があるので注意して実装する。
まずUpdate()
やCreateOrUpdate()
はリソースの取得と書き換えがアトミックでないので、
TOCTTOU(Time of check to time of use)な操作となる。
対策としてPatch()
を用いる。Patch()
にはいくつかバリエーションがあるようだ。
- client.MergeFrom:リストの更新は上書きに相当
- client.StrategicMergeFrom:
patchStrategy
によってリストの更新が上書きになったり追加になったりする - Servcer-Side Apply:kubernetes v1.14+
注意点としてStatusはサブリソースとしたので、
client.Status().Update()
やclient.Status().Patch()
で更新する。
また、リソースの取得と削除の間で、同名のリソースが作られる可能性があるので、
PreconditionsオプションでUIDとResourceVersionの一致をチェックすると安全に削除できる。
Reconciler
ReconcilerインターフェイスでReconile処理の肝となる処理を実装する。
Reconcile()
メソッドはCRのNemespaceとNameを受け取り、
Requeueするかどうかを返す。
時間がかかる処理の場合にはRequeueすることでRecondile()
メソッドからは即座に抜けるよう設計する。
Reconile()
は冪等な実装でなければならない。
Custom Controllerは後述するManagerに登録されるが、
登録時にNewControllerManagedBy()
において
For
でReconcile対象のCR、Owns
で関連するリソースを登録する。
Owns
対象にownerReferences
を設定しガベージコレクションが動作するように設定する。
DeletionTimestamp
がnon-zeroなら削除が開始されたことを意味するので抜ける。
Admission Webhook
下記の2種がある。
- MutatingWebhook:リソースの作成・更新・削除の直前にその内容を書き換える。
Defaulter
インターフェイス。 - ValidatingWebhook:値を検証をする。
Validator
インターフェイス。
CRのマニフェストを書き換えて適用しようとすると、バリデーションが動くことを確認できる。
kubectl apply -f config/samples/view_v1_markdownview_noimage.yaml
# The MarkdownView "markdownview-sample-noimage" is invalid:
# * spec.replicas: Invalid value: 1000: replicas must be in the
# range of 1 to 5.
# * spec.markdowns: Required value: markdowns must have SUMMARY.md.
Envtest
Envtestでコントローラをテストできる。
make test
コマンドで実行でき、Custom ControllerやAdmission Webhookをテストできる。
内部でetcdとkube-apiserverが稼働する。
リソースの削除
リソースの削除はCreateやUpdateと比べ難易度が高い。
同一のNamespaceのリソースに対しては.metadata.ownerReferences
によって親リソースの削除によって子リソースが削除されるようガベージコレクションを設定すれば良い。
controllerutil.SetControllerReference()
- あるいはServer-Side Applyの時には
WithOwnerReferences(owner)
異なるNamespaceやKubernetesの外部にあるリソースの削除に対してはFinalizerを使う。
.metadata.finalizers
が指定されたリソースは削除される代わりにdeletionTimestampが付与される- Custom ControllerはdeletionTimestampが付与されていれば関連リソースを削除し
.metadata.finalizers
を削除する - 削除イベントを取りこぼしても何度でもReconcileが呼ばれる
controllerutil.Contains/Add/RemoveFinalizer()
を使う
Manager
ManagerはCustom Controllerの上位の概念で、 コントローラの管理・リーダ選出・メトリクス集計・ヘルスチェックを担当する。
可用性向上のためにCustom Controller Podを複数立ち上げた時、
同リソースに対してReconcile処理が競合するのを防げる。
これはNewManager()
でLeaderElection=true
としてLeaderElectionIDを設定すれば良い。
同じIDをもつ複数のPodから唯一つがリーダーとして選出される。
以下のようにログからもリーダー選出の状況を確認できる。
kubectl -n markdown-view-system logs markdown-view-controller-manager...
# ... leaderelection.go:255] successfully acquired lease ...
Reconcile Loop以外にgoroutineを立ち上げて何かを実行するときはRunnableを使えば良い。
CRのStatusは現在の状態だけだか、過去のStatus変更の履歴を残すにはEventRecorderを使う。
controller.Recorder.Event()
でログを記録でき、kubectl get events
で取得できる。
HealthProbeBindAddress
でヘルスチェック用のエンドポイントを設定できる。
mgr.AddHealthzCheck
とmgr.AddReadyzCheck
のハンドラがlivenessProbeとreadinessProbeに相当する。
mgr.GetFieldIndexer()
と使うとリソースの取得にインデックスを使うことができる
- インデックスはリソースの任意の複数のフィールドの組み合わせて実現できる
- インデックスはGVK(Group Version Kind)ごとなのでリソース間で重複しても良い
- インデックスにnamespaceは暗黙的?に含まれるので明示する必要はない
IndexFiled()
の第3引数はフィールド名と一致させておくと便利
メトリクス
Kubebuilderの生成コードによって、
自動的にCPU, Memory, Reconcile時間などのメトリクスが取得される。
ManagerのMetricsBindAddress
で設定できる。
- Prometheus互換のフォーマット
- controller-runtimeの
metrics.Registry
で任意のメトリクスを追加できる - kube-rbac-proxyを利用するとメトリクスエンドポイントAPIにRBACを設定できる。今回は自動で有効化されていた
手元でメトリクスが取得されていることを確認できる。
kubectl -n markdown-view-system port-forward \
deploy/markdown-view-controller-manager 8080:8080 --address 0.0.0.0
curl -s localhost:8080/metrics | grep ^markdown
# markdownview_available{name="markdownview-sample",namespace="default"} 0
# markdownview_healthy{name="markdownview-sample",namespace="default"} 1
# markdownview_notready{name="markdownview-sample",namespace="default"} 0
もちろんメトリクスをPrometheusに送り、Grafanaから読むことができる。
helm repo add prometheus-community \
https://prometheus-community.github.io/helm-charts
helm repo update
kubectl create ns prometheus
helm install prometheus prometheus-community/kube-prometheus-stack \
--namespace=prometheus \
--set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false
kubectl apply -f ./config/rbac/prometheus_role_binding.yaml
# Forward port for Grafana Web UI
kubectl -n markdown-view-system port-forward \
deploy/markdown-view-controller-manager \
8080:8080 --address 0.0.0.0