Upgrade to Pro — share decks privately, control downloads, hide ads and more …

controller-runtime Deep Dive

bells17
October 06, 2022

controller-runtime Deep Dive

Kubernetes Meetup Tokyo #53 ( https://k8sjp.connpass.com/event/259350/ ) のセッション資料です。
controller-runtimeのアーキテクチャや内部実装について解説しています。

セッション動画はこちらです。
https://youtu.be/jCyt993dzaU

以下スライドで紹介しているリンク:

controller-runtime clientについて: https://zenn.dev/bells17/articles/controller-runtime-client
controller-runtime: https://github.com/kubernetes-sigs/controller-runtime/tree/v0.12.3
aws-load-balancer-controller: https://github.com/kubernetes-sigs/aws-load-balancer-controller/tree/v2.4.4
kueue: https://github.com/kubernetes-sigs/kueue/tree/v0.2.1
Kubebuilder Book: https://book.kubebuilder.io/architecture.html
つくって学ぶKubebuilder: https://zoetrope.github.io/kubebuilder-training/
Ginkgo/GomegaによるKubernetes Operatorのテスト手法: https://zenn.dev/zoetro/books/testing-kubernetes-operator
Caching Unstructured Objects using controller-runtime: https://ymmt2005.hatenablog.com/entry/2021/07/25/Caching_Unstructured_Objects_using_controller-runtime
kubebuilder-declarative-pattern: https://github.com/kubernetes-sigs/kubebuilder-declarative-pattern
kubebuilder: https://github.com/kubernetes-sigs/kubebuilder
controller-tools: https://github.com/kubernetes-sigs/controller-tools

aws-load-balancer-controller(Ingress Controller for AWS): https://github.com/kubernetes-sigs/aws-load-balancer-controller
kueue(Job Queueing): https://github.com/kubernetes-sigs/kueue
topolvm(CSI Driver for LVM): https://github.com/topolvm/topolvm
moco(MySQL Operator): https://github.com/cybozu-go/moco
logging-operator: https://github.com/banzaicloud/logging-operator
istio(Service Mesh): https://github.com/istio/istio

bells17

October 06, 2022
Tweet

More Decks by bells17

Other Decks in Programming

Transcript

  1. ▶ @bells17 ▶ Software Engineer ▶ 普段やってること: + Kubernetes 関連コンポーネントの開発

    + Kubernetes as a Service開発 + 社内におけるKubernetes普及活動 ▶ Kubernetes SIG-Docs Japanese localization reviewer ▶ Kubernetes Internal Organizer ▶ #kubenews ▶ @bells17_
  2. このセッションについて ▶ セッションを聴いてわかること + controller-runtimeのアーキテクチャ + controller-runtimeの内部実装 + controller-runtimeのtips ▶

    このセッションの⽬的 + 既存のドキュメントではあまり解説されていないcontroller-runtimeのアーキテクチャや内部実装、tipsなどを まとめること + controller-runtimeの内部実装を把握することで開発者がcontroller-runtimeを拡張するための基礎知識を 
 まとめること ▶ 対象とする視聴者 + controller-runtimeを使ってアプリケーションを開発する⼈ + Kubernetes関連アプリケーションのアーキテクチャや内部実装に興味のある⼈ ▶ 視聴者に要求する前提知識 + controller-runtimeの概要や基礎知識
  3. 注意点 ▶ 対象バージョン: controller-runtime v0.12.3 + https://github.com/kubernetes-sigs/controller-runtime/tree/v0.12.3 ▶ あくまでcontroller-runtimeの実装を追った結果での理解の説明になるので、 


    間違いが含まれている可能性があります ▶ 引⽤しているコードはスライドにうまく収めるため省略、⼀部書き換えを 
 ⾏っている箇所があります
  4. controller-runtime ▶ Kubernetesの以下のようなアプリケーションを構築するためのフレームワーク + Kubernetes Operator(CRD+Controller) + Webhook Server(Validating/Mutating/Conversion) +

    その他k8s clientなどを使⽤したKubernetes向けアプリケーション ▶ client-go単体で使うよりも便利なので、Kubernetesに関するアプリケーションを構築するときには 
 とりあえずcontroller-runtimeを使っておけばOK(拡張API Serverなど⼀部ケースでは向いてない) ▶ kubebuilder/operator-sdkなどのフレームワークの内部で利⽤されている ▶ 上記のフレームワークを使わずに、controller-runtime単体での使⽤も可能 ▶ 以下のようなKubernetesを使ったアプリケーション構築に必要なものが⼀通り 
 含まれている + Controller実⾏を含めたrunnerの管理機能 + Webhook Server + Prometheus Server + cache機能付きk8s clientの提供 + e2eテスト環境構築ツール(envtest)
  5. controller-runtimeの利⽤例 ▶ aws-load-balancer-controller(Ingress Controller for AWS): https:// github.com/kubernetes-sigs/aws-load-balancer-controller ▶ kueue(Job

    Queueing): https://github.com/kubernetes-sigs/kueue ▶ topolvm(CSI Driver for LVM): https://github.com/topolvm/topolvm ▶ moco(MySQL Operator): https://github.com/cybozu-go/moco ▶ logging-operator: https://github.com/banzaicloud/logging-operator ▶ istio(Service Mesh): https://github.com/istio/istio ▶ etc …
  6. controller-runtimeの使い⽅を学ぶ ▶ controller-runtime(kubebuilder)の使い⽅については以下のような充実した 
 ドキュメントが公開されている + Kubebuilder Book + つくって学ぶKubebuilder

    + Ginkgo/GomegaによるKubernetes Operatorのテスト⼿法 ▶ controller-runtimeについて詳しく知りたい⽅は上記ドキュメントと合わせて このセッション資料を⾒てもらえると
  7. controller-runtime architecture overview 画像はKubebuilder Book( https://book.kubebuilder.io/architecture.html ) より引⽤ ▶ Manager:

    
 実⾏する各種runnerやclientなどを管理 ▶ Client & Cache: 
 client-goをベースにしたcache機能付きの 
 独⾃k8s clientを提供 ▶ Controller: 
 ユーザーが実装したReconcilerを実⾏ ▶ Webhook: 
 ユーザー定義に基づくWebhook Serverを 
 実⾏ ※ runner: 「各種Controller/Serverなどを含めたManagerが管理 する処理の実⾏単位」くらいに考えてもらえればOK ユーザー独⾃のrunnerを実⾏することも可能
  8. CRDが⽣成され // SampleSpec defines the desired state of Sample type

    SampleSpec struct { Message string `json:"message,omitempty"` } // SampleStatus defines the observed state of Sample type SampleStatus struct { Message string `json:"message,omitempty"` } // Sample is the Schema for the samples API type Sample struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec SampleSpec `json:"spec,omitempty"` Status SampleStatus `json:"status,omitempty"` }
  9. Reconcilerが⽣成されて // SampleReconciler reconciles a Sample object type SampleReconciler struct

    { client.Client Scheme *runtime.Scheme } // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. func (r *SampleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) // TODO(user): your logic here return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *SampleReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&sampleoperatorv1beta1.Sample{}). Complete(r) }
  10. main.goのコードが⾃動でアップデートされる func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(sampleoperatorv1beta1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } func main()

    { … if err = (&controllers.SampleReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Sample") os.Exit(1) } //+kubebuilder:scaffold:builder }
  11. controller実⾏の流れ ▶ SharedInformerがKube API Serverから対象リソースをList & Watchで監視 ▶ 対象リソースの変更時に登録したEventHandlerでイベントを受信 ▶

    受信したイベントをPredicatesでフィルタリング ▶ フィルタリングしたイベントをEventHandlerでWorkQueueのイベントに変換 ▶ ControllerがWorkQueueに溜まっているイベントを元にReconcilerを実⾏
  12. Reconciler & Controllerまとめ ▶ Reconciler: controller-runtimeを使って開発者が実装する調整ループの中⾝ ▶ Controller: 作成したReconcilerやPredicateなどの設定を元にcontroller-runtimeが ⽣成~実⾏制御を⾏うもの

    ▶ Controllerの起動はmgr.Add()によってManagerに管理される controller-runtimeにおいては開発者が実装するのはReconcilerとControllerの設定のみ Controller本体はcontroller-runtimeが⽣成や管理を⾏ってくれる
  13. Manager(controller-runtime)の全体像 ▶ Managerを通してアプリケーション全体を管理するのでManager=controller-runtimeと思ってもらってOK ▶ ManagerはRunnerという仕組みを中⼼成り⽴っている ▶ Runnerの登録はmgr.Add()というメソッドで⾏うことができる ▶ Runner: Runnableというinterfaceを満たす実装を予め登録しておき、Manager起動時に登録した実装を実

    ⾏する機能 ▶ Runnerの種類: Runnerはどのようなinterfaceを満たすかに応じて4種類に分類される + Webhooks: Webhookサーバーへのhandlerの登録処理を⾏う + Caches: Informerの起動を⾏う + LeaderElections: LeaderElectionでリーダー選出された際のみ実⾏されるRunner + Others: LeaderElectionによるリーダー選出に関わらず実⾏されるRunner ▶ 以下のServerはRunnerとは独⽴して実⾏される + Metric Server: controller-runtimeのメトリクスを取得するためのprometheus server + Health Prove Server: 任意のcheckerを設定できるヘルスチェック⽤サーバー
  14. Webhook Runners ▶ Webhook Runnerは基本的にmgr.GetWebhookServer()によってWebhook Serverを⽣成 したときのみに設定されるRunner種別 ▶ Webhook ServerはRunnerの仕組みを通して起動される

    ▶ Webhook ServerにはCertWatcherという機能があり、SSL証明書ファイルが更新された 際に⾃動でリロードする仕組みが提供されている
  15. Health Probe Server ▶ Health Probe Serverはヘルスチェック⽤のHTTPサーバー ▶ `/healthz`と`/readyz`の2つのヘルスチェック⽤のエンドポイントを提供している ▶

    ヘルスチェック処理は予め登録したcheckerによるチェックをパスしたかどうかで判定 される仕組みになっている ▶ このcheckerはそれぞれ、AddHealthzCheckとAddReadyzCheckという2つメソッドに よって登録することができるようになっている
  16. k8s client ▶ controller-runtimeではclient-goをベースとした3種類のk8s clientが提供されている ▶ k8s clientの種類は以下の通り + Delegating

    Client: 読み取り系の処理時にCache clientを利⽤するクライアント + Cache Client: Informerを通したローカルキャッシュからデータ取得を⾏う 
 reader + API Reader: キャッシュを使⽤せず都度API Serverにリクエストを送りデータ取得を⾏ うreader ▶ そのため基本的には以下のようにclientを使い分けておけばOK + Delegating Client: 特に理由がなければこのクライアントを利⽤する + API Reader: 最新のデータ取得を確実に⾏いたい際に利⽤ + Cache Client: Delegating Client内部で使⽤されているので基本的に利⽤する必要無し
  17. Delegating ClientとCache Clientはオプションから独⾃クライアントを 使⽤可能 func main() { mgr, err :=

    ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ NewCache: cache.New, NewClient: cluster.DefaultNewClient, … }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } } ※ API Readerについては独⾃クライアントへの上書きはできない
  18. 各clientは以下のように取得することが可能 mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{}) if err != nil

    { setupLog.Error(err, "unable to start manager") os.Exit(1) } delegatedClient := mgr.GetClient() cache := mgr.GetCache() apireader := mgr.GetAPIReader()
  19. client-go overview 画像引⽤元: https://github.com/kubernetes/sample-controller/blob/v0.25.0/docs/images/client-go-controller-interaction.jpeg ▶ Re fl ector: API Serverから指定リソースをList

    & Watch で取得 & 以降の変更を監視 ▶ Delta Fifo queue: Re fl actorが取得したデータをqueue として格納 ▶ Informer: Delta Fifo queueからデータを取り出して 1. データをIndexerへと渡す 2. Informerに設定された各EventHandlerを呼び出す ▶ Indexer: 渡されたデータをインメモリにキャッシュ 
 (structのmapにデータを設定するだけ) Kubernetes ControllerはInformerのEventHandlerに 
 それぞれのhandlerを設定することでイベント受信して 
 動作するよう振る舞うが、ここではclientがメインなので 以降の流れは省略
  20. controller-runtime client overview ▶ Delegating Client: 
 Cache ClientをReaderとして 


    Default ClientをWriterとしてそれぞれ使⽤ ▶ Cache Client: リソースごとの各SharedInformerを 
 データソースとしてデータを取得するクライアント ▶ Base Client: 
 Delegating ClientのWriterとして使⽤される 
 内部クライアント 
 API ReaderはこのBase ClientをReaderとして使⽤ したもの(structとしては同じもの) ▶ Rest Client: 
 実際にAPI Serverにhttp(s) requestを送るclient
  21. clientに渡すObjectに応じて内部clientは⾃動で切り替わる client := mgr.GetClient() // use structured client podList :=

    corev1.PodList{} err := client.List(ctx, podList) // use unstructured client uList := unstructured.UnstructuredList{} uList.SetGroupVersionKind(corev1.GroupVersion.WithKind("PodList")) err := client.List(ctx, podList) // use metadata client pList := metav1.PartialObjectMetadataList{} pList.SetGroupVersionKind(corev1.GroupVersion.WithKind("PodList")) err := client.List(ctx, podList)
  22. k8s client object ▶ Structured: PodやPodListなどの各リソースごとのstruct ▶ Unstructured: "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" にある以下のstruct

    + Unstructured: Podなどのリソース単体向けに使⽤ + UnstructuredList: PodListなどのListリソース向けに使⽤ + Unstructured ObjectはStructured Objectのようにリソースの取得・更新など諸々の操作を ⾏うことが可能なObject ▶ Metadata: "k8s.io/apimachinery/pkg/apis/meta/v1" にある以下のstruct + PartialObjectMetadata: Podなどのリソース単体向けに使⽤ + PartialObjectMetadataList: PodListなどのListリソース向けに使⽤ + Metadata Objectはリソースのmetadata取得・更新にのみ利⽤できる
  23. scheme.Convert()を利⽤することで異なるObjectのデータを 
 変換することができる client := mgr.GetClient() uList := unstructured.UnstructuredList{} uList.SetGroupVersionKind(corev1.GroupVersion.WithKind("PodList"))

    err := client.List(ctx, uList) if err != nil { return err } podList := corev1.PodList{} err = mgr.GetScheme().Convert(uList, podList, nil) if err != nil { return err }
  24. Unstructured(List)とMetadata(List)はGVKによってリソース種別を 
 判定している u := unstructured.Unstructured{} u.SetGroupVersionKind(schema.GroupVersionKind{ Group: "networking", Version:

    "v1", Kind: "Ingress", }) p := metav1.PartialObjectMetadata{} u.SetGroupVersionKind(schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "Pod", }) ※ Structured Objectはre fl ect.ValueOf(obj).Elem().Type() で取得できるObjectの re fl ect.Type をキーにリソースを判定している
  25. つまり、scheme.Convert()・GVK・Unstructuredを利⽤して異なる Structured Objectを変換可能 client := mgr.GetClient() ingress := networkingv1.Ingress{} err

    := client.Get(ctx, types.NamespacedName{Name: "ingress", Namespace: "default"} ingress) if err != nil { return err } u := unstructured.Unstructured{} err = mgr.GetScheme().Convert(ingress, u, nil) if err != nil { return err } ingressbeta := networkingv1beta1.Ingress{} u.SetGroupVersionKind(networkingv1beta1.GroupVersion.WithKind("Ingress")) err = mgr.GetScheme().Convert(u, ingressbeta, nil) if err != nil { return err } ※ CRDのgroup名をrenameしたときになどはこの⽅法を使うと便利だった
  26. Unstructured Objectのキャッシュを有効化するには CacheUnstructuredオプションを有効化する必要がある func NewCustomDelegatingClient( cache cache.Cache, config *rest.Config, options

    client.Options, uncachedObjects ...client.Object ) (client.Client, error) { c, err := client.New(config, options) if err != nil { return nil, err } return client.NewDelegatingClient(client.NewDelegatingClientInput{ CacheReader: cache, Client: c, UncachedObjects: uncachedObjects, CacheUnstructured: true // enable caching for unstructured objects }) } func main() { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ NewClient: NewCustomDelegatingClient, … }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } }
  27. k8s clientまとめ ▶ controller-runtimeでは以下の3種類のclientが提供されている + Delegating Client: Readerのみキャッシュが利⽤されるクライアント + Cache

    Client: SharedInformerを利⽤したキャッシュクライアント + API Reader: 都度同期的にAPI Serverにリクエストするクライアント ▶ Delegating ClientとCache Clientはオプションで独⾃クライアントに上書き可能 ▶ Client objectには以下の3種類が提供されている + Structured + Unstructured + Metadata ▶ Structured ObjectやUnstructured Objectはscheme.Convert()で相互変換が可能 ▶ Unstructured ObjectとMetadata Clientは内部のGVK情報によってGVKを管理 ▶ Structured Objectはstructのre fl ect.TypeをキーにGVKを管理 ▶ `CacheUnstructured: true` に設定することでUnstructured Objectのキャッシュが可能になる
  28. envtest overview ▶ setup-envtest 
 指定Kubernetesバージョンの 
 etcd/API Serverなどのダウンロードを⾏う ▶

    envtest.Environment.Start 
 以下のようなAPI Serverを使ったKubernetes Operatorのe2eテスト環境のセットアップを⼀通り ⾏ってくれる + KUBEBUILDER_ASSETSを通して渡された 
 パスにあるetcd/API Serverの起動 + CRDのインストール + デフォルトユーザーの追加 + Webhook設定 ▶ envtest.Environment.Stopで起動した 
 etcd/API Serverの停⽌が可能
  29. Tips: 
 対象バージョンのkubectlの使⽤や独⾃ユーザーの設定などが可能 testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd",

    "bases")}, ErrorIfCRDPathMissing: true, } var err error cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) // use kubectl cmd testEnv.ControlPlane.KubeCtl().Run("get", "nodes") // add user & use kubectl cmd user, err := env.ControlPlane.AddUser(envtest.User{ Name: "envtest-admin", Groups: []string{"system:masters"}, }, nil) Expect(err).NotTo(HaveOccurred()) kubectl, err := user.Kubectl() Expect(err).NotTo(HaveOccurred()) kubectl.Run("get", "nodes")
  30. 設定ファイルの拡張 type customConfig struct { metav1.TypeMeta `json:",inline"` configv1alpha1.ControllerManagerConfigurationSpec `json:",inline"` CustomValue

    string `json:"customValue"` } func main() { options := ctrl.Options{ Scheme: scheme, HealthProbeBindAddress: ":8081", MetricsBindAddress: ":8080", Port: 9443, } var err error config := customConfig{} options, err = options.AndFrom(ctrl.ConfigFile().AtPath("./config.yaml").OfKind(&config)) if err != nil { setupLog.Error(err, "unable to load the config file") os.Exit(1) } fmt.Println(config.CustomValue) } # ./config.yaml apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 kind: CustomControllerManagerConfiguration customValue: foo port: 9442 controller-runtimeの設定ファイルを拡張して独⾃設定を渡せるので、設定の数が多いときに便利
  31. Controller: Reconcile対象以外のリソースからのイベントを元に調整ループを実⾏ func (r *SampleReconciler) SetupWithManager(mgr ctrl.Manager) error { handler

    := &eventHandler{} return ctrl.NewControllerManagedBy(mgr). For(&sampleoperatorv1beta1.Sample{}). Watches(&source.Kind{Type: &corev1.Service{}}, handler). Complete(r) } type eventHandler struct {} func (h *eventHandler) Create(e event.CreateEvent, queue workqueue.RateLimitingInterface) { svc := e.Object.(*corev1.Service) queue.Add(reconcile.Request{NamespacedName: types.NamespacedName{ Namespace: svc.Namespace, Name: svc.Name, }}) } func (h *eventHandler) Update(e event.UpdateEvent, queue workqueue.RateLimitingInterface) {} func (h *eventHandler) Delete(e event.DeleteEvent, queue workqueue.RateLimitingInterface) {} func (h *eventHandler) Generic(e event.GenericEvent, queue workqueue.RateLimitingInterface) {} 上記の例の場合、Serviceリソースが作成されたら、ServiceリソースのName/NamespaceをキーにSample CRDのReconcileを実⾏することになる
  32. Controller: Goチャネルからのイベントを元に調整ループを実⾏ func (r *SampleReconciler) SetupWithManager(mgr ctrl.Manager) error { handler

    := &eventHandler{} evt := make(chan event.GenericEvent) return ctrl.NewControllerManagedBy(mgr). For(&sampleoperatorv1beta1.Sample{}). Watches(&source.Channel{Source: evt}, handler). Complete(r) } type eventHandler struct {} func (h *eventHandler) Create(e event.CreateEvent, queue workqueue.RateLimitingInterface) {} func (h *eventHandler) Update(e event.UpdateEvent, queue workqueue.RateLimitingInterface) {} func (h *eventHandler) Delete(e event.DeleteEvent, queue workqueue.RateLimitingInterface) {} func (h *eventHandler) Generic(e event.GenericEvent, queue workqueue.RateLimitingInterface) { queue.Add(reconcile.Request{NamespacedName: types.NamespacedName{ Namespace: e.Object.GetNamespace(), Name: e.Object.GetName(), }}) } 上記の例の場合、Serviceリソースが作成されたら、ServiceリソースのName/NamespaceをキーにSample CRDのReconcileを実⾏することになる
  33. 参考資料 ▶ controller-runtime clientについて: https://zenn.dev/bells17/articles/controller-runtime-client ▶ controller-runtime: https://github.com/kubernetes-sigs/controller-runtime/tree/v0.12.3 ▶ aws-load-balancer-controller:

    https://github.com/kubernetes-sigs/aws-load-balancer-controller/tree/v2.4.4 ▶ kueue: https://github.com/kubernetes-sigs/kueue/tree/v0.2.1 ▶ Kubebuilder Book: https://book.kubebuilder.io/architecture.html ▶ つくって学ぶKubebuilder: https://zoetrope.github.io/kubebuilder-training/ ▶ Ginkgo/GomegaによるKubernetes Operatorのテスト⼿法: https://zenn.dev/zoetro/books/testing-kubernetes-operator ▶ Caching Unstructured Objects using controller-runtime: https://ymmt2005.hatenablog.com/entry/2021/07/25/ Caching_Unstructured_Objects_using_controller-runtime ▶ kubebuilder-declarative-pattern: https://github.com/kubernetes-sigs/kubebuilder-declarative-pattern ▶ kubebuilder: https://github.com/kubernetes-sigs/kubebuilder ▶ controller-tools: https://github.com/kubernetes-sigs/controller-tools