📝

controller-runtime clientについて

2022/08/27に公開

KubernetesでOperatorやControllerを開発する際に利用するフレームワークであるcontroller-runtimeのclientについて調べたのでまとめます。

この記事の目的は以下のような感じになります:

  • controller-runtimeが提供するKubernetes clientの概要についてまとめること
  • controller-runtime client周りの追加の不明点などがあった場合には、この記事をベースにコードベースで調べたいことをすぐに調べられる程度にはコードレベルで詳しい内容をまとめること
  • 以下についてわかるようになること
    • 各種内部clientについて理解できるようになること
      • typedClient
      • unstructuredClient
      • metadataClient
    • Informerの仕組みについてコードレベルで理解できるようになること
    • TODO(今後追記するかも): DynamicRESTMapperについて理解すること

controller-runtimeはkubebuilderやOperator SDKの内部で利用されているので、これらを利用している方にも役立つかも知れません。
また、Kubernetes関連の実装に興味がある方にとっても面白いかも知れません。

controller-runtimeの既存の情報としては

などで主に使い方や基本的な機能についてまとめられているので、controller-runtimeについて知らない方はそちらを見てください。

上記はKubebuilderについてのページですが、大まかに言うとKubebuilderは

  • controller-runtimeによるアプリケーションフレームワークの提供
  • envtest(controller-runtime)による簡易的なe2eテスト環境の提供
  • controller-genによるコード生成機能
  • Kubebuilderによるコード生成機能

という構成になっているのでKubebuilderについて話しているページのコードについては基本controller-runtimeを使っています。

controller-runtime client

controller-runtimeのKubernetes clientは基本的に3種類あります。

  • delegatingClient
  • cache client(これをclientと呼んで良いかは人によって変わりそうだけどこの記事ではclientとして扱います)
  • client

controller-runtimeを使ってコードを書いていてclientを取得する際に

cfg, err := ctrl.GetConfig()
if err != nil {
	return err
}
mgr, err := ctrl.NewManager(cfg, ctrl.Options{})
if err != nil {
	return err
}

delegatedClient := mgr.GetClient()
cache := mgr.GetCache()
client := mgr.GetAPIReader()

のようにして各種clientを利用すると思いますが、これらのことです。
(ただしclient := mgr.GetAPIReader() については Reader インターフェイスを通しているため GetList のみが提供されています)

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/dryrun.go#L32-L37

delegatingClient

delegatingClientは mgr.GetClient() で取得することができるclientです。
delegatingClientは DefaultNewClient 関数を通して生成されます。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cluster/cluster.go#L258-L270

delegatingClientは

  • GetやListなどの読み込み系の処理を行う際には基本的にキャッシュからデータを返す(都度API Serverとの通信を行わない)
  • CreateやUpdateなどの更新系の処理の際は都度API Serverとやり取りをして処理を行う

という特徴を持っています。

そのためcontoroller-runtimeを使ったControllerの開発では、基本的にはこのdelegatingClientを使っていくことになります。


https://github.com/kubernetes/sample-controller/blob/v0.25.0/docs/images/client-go-controller-interaction.jpeg

また、GetやListの際に利用するキャッシュというのは上記の図で赤で囲った部分のような仕組みになっており

  • はじめにListで対象リソース一覧を取得
  • 取得したリソースをインメモリにキャッシュ
  • 以降Watchで変更があった際に変更内容を取得しキャッシュを更新

という方法でキャッシュの同期を行っています。

delegatingClientの仕組み

NewDelegatingClient の中身は以下のようになっています。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/split.go#L44-L67

見ればなんとなくイメージがつくかも知れませんが内部では

  • Reader
  • Writer

をembedしています。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/split.go#L69-L76

更にReaderの中には

  • CacheReader
  • ClientReader

を持っている構成になっています。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/split.go#L93-L100

embedしている Writer のinterfaceを見ると以下の様になっており、 CreateUpdate などの更新系の処理は NewDelegatingClient の引数で渡されたclientがそのまま使われていることがわかります。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/interfaces.go#L62-L79

一方で Reader のinterfaaceは以下の様になっており、GetList が定義されています。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/interfaces.go#L48-L59

この Reader には delegatingReader というものが使われています。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/split.go#L123-L141

delegatingReader の GetList は上記のようになっており、GetList で取得するリソースについて

  • controller-runtimeの ClientDisableCacheFor オプションに含まれているGVKである
  • Get または List で取得した結果を Unstructured(List) にbindしようとしている

のいずれかの条件にマッチした場合はcacheではなくclientが使われて、API Serverからデータを取得します。
それ以外についてはcacheからデータの取得を行います。

cache client

cache clientは mgr.GetCache() で取得することができるclientです。
GetList のメソッドがあって取得系の処理ではclientとみなすことができると思うのでこの記事ではclientとして紹介しています。
cacheもdelegatingClientと同様にオプションで独自cacheを返す関数を渡すことができますが、デフォルトでは cache.New 関数を通して生成されます。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cache/cache.go#L148-L168

delegatingClientの部分で軽く紹介してしまいましたが、delegatingClientは内部でこのcacheを取得系の処理に使用しているため、特に理由が無い限りはcache clientを利用する必要はなく、データ取得にはdelegatingClientを利用すれば問題ないと思います。

cache clientの仕組み

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cache/cache.go#L41-L70

cache clientのinterfaceは上記の様になっており

  • cache.Reader
  • Informers

のInterfaceのメソッドを持っていることになります。

1つ前の節で紹介した cache.New を見ると cache.New が返すstructは informerCache というものであるということがわかります。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cache/informer_cache.go#L53-L88

この informerCache が提供する GetList は上記のようになっており、 informerCache が持つ InformersMap.Get を通してデータを取得したいGroupKindVersion(GVK)別のcache clientを InformersMap が返し、取得したcache clientが持つ Reader から Get あるいは List を行っているのがわかります。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cache/internal/informers_map.go#L190-L214

InformersMap.Get の中身を見てみると上記のようになっており

  • ip.informersByGVK から対象GVKのcache clientを取り出して返す
  • ip.informersByGVK にまだ対象GVKのcache clientが無い場合は
    • cache clientを生成~起動
    • ip.informersByGVK に生成したcache clientを保存
    • cacheの同期を行ってから返す

という感じの実装になっています。

そのため実際のcache dataの保持は informerCache が持つ InformersMap 内部の informersByGVK のGVK別のcache clientが行っているということがわかりました。

client

clientは mgr.GetAPIReader() で取得することができるKubernetes clientです。
mgr.GetAPIReader() が返すのは Reader interfaceとなってるため基本的には GetList のみが使用できます。

このclientだけはオプションで独自のclientを返すことはできない設計になっており、 client.New 関数によって Client interfaceを満たしたclientが返されます。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/client.go#L75-L77

そのため実際には GetList 以外にも CreateUpdate なども利用可能で、delegatingClientの更新系の処理はこの client.New 関数によって生成されたclientが利用されています。

このclientは GetList によるデータ取得の際にも都度API Serverと通信を行った結果を返すため、厳密に最新のデータを利用しなくてはならないような場面では mgr.GetAPIReader() で取得したclientを使うことになります。

clientの仕組み

client.New 関数が内部で呼び出している newClient は以下のようになっています。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/client.go#L79-L148

色んな処理を行っていますが最終的にはclient

  • typedClient
  • unstructuredClient
  • metadataClient

という3種類の内部clientを設定してclientを生成していることがわかります。

そして Create などの中身では以下のように引数で渡されたobjectのstructに応じて対象となるclientを通してデータの取得や更新が行われる仕組みとなっています。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/client.go#L182-L191

内部clientについて

内部clientという呼び方についてはこの記事で適当に呼んでいるだけですが

  • typedClient
  • unstructuredClient
  • metadataClient

という3種類の内部clientの中身について見ていきます

typedClient

typedClientが使われるのはPodやDeploymentといったリソースのstructをobjectとして渡されたケースです。
そのため基本的にはこのtypedClientが使われることになります。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/typed_client.go#L36-L52

例えばtypedClientの Create の実装は上記の様になっており

  • c.cache.getObjMeta() で渡されたObjectのリソース用のREST clientを生成 or 取得
  • REST clientからHTTPリクエストを実行

という処理を行っています。
Create 以外の GETUPDATE なども同じような処理を行っています。

c.cache.getObjMeta() の際にObjectのリソース判定にはreflectが使用されており reflect.ValueOf(obj).Elem().Type() で取得できるObjectの reflect.Type をキーにしてclientの取得 or まだなければ生成するというフローになっています。

reflectパッケージには詳しくないですが、どうやら渡したObjectのstructのパッケージ情報なども含まれているようで、渡したObjectのstructの型情報以外ので利用するclientを変更することはできないようになっているようです。

unstructuredClient

unstructuredClientは Unstructured(List) をObjectとして渡した際に利用されるclientです。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/unstructured_client.go#L40-L65

unstructuredClientの Create の実装は上記の様になっており、typedClientと同様に

  • c.cache.getObjMeta() で渡されたObjectのリソース用のREST clientを生成 or 取得
  • REST clientからHTTPリクエストを実行

という処理を行っています。
Create 以外の GETUPDATE なども同じようになっています。

Unstructured(List) を利用した場合、どのリソース向けのREST clientを利用すればよいかはtypedClientが reflect.Type をキーにしていたのとは異なり、 Unstructured(List) に埋め込まれてある TypeMeta のGVK情報が利用されます。
このGVK情報は obj.GetObjectKind().SetGroupVersionKind() で設定することが可能です。

つまり、例えば Unstructured(List) に設定したデータが例えば Pod リソースのものだとしても

obj.GetObjectKind().SetGroupVersionKind(GroupVersionKind{
	Group:   "apps",
	Version: "v1",
	Kind:    "Deployment",
})

のように設定してunstracturedClientに渡せば Deployment のAPIをコールすることになります。

metadataClient

metadataClientは PartialObjectMetadata(List) をObjectとして渡した際に利用されるclientです。
metadataClientは特殊で、Objectのmetadataを取得したり更新したりするためのclientとなっています。
そのためサポートされている操作が

  • LIST
  • GET
  • Patch
  • StatusPatch
  • Delete
  • DeleteOfAll

となっており

  • Create
  • Update
  • StatusUpdate

についてはサポートされていません。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/client/metadata_client.go#L119-L139

metadataClientの GET の実装は上記のようになっており

  • TypeMeta のGVK情報から対象リソース向けのclient-goのmetadata clientを生成
  • HTTPリクエストを実行
  • metadata.SetGroupVersionKind(gvk) でObjectにGVK情報を設定

という流れになっています。
TypeMeta のGVK情報を利用してclientを生成するところはunstracturedClientと同様です。

内部clientまとめ

上記で紹介したように、clientは渡されたObjectの型に応じて

  • typedClient
  • unstructuredClient
  • metadataClient

という3種類の内部clientの中から利用する内部clientを選びAPIリクエストを実行するという実装になっていました。

cache clientの詳細

  • typedClient
  • unstructuredClient
  • metadataClient

について紹介できたのでcache clientについてもこのあたりを含めてもう少し深堀りします。

先程cache clientについて cache.NewinformerCache を生成するということを説明しました。
この informerCacheInformersMap というstructをembedしており、キャッシュ処理を行うInformerとしての役割の多くは実際にはこの InformersMap が担っています。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cache/internal/deleg_map.go#L48-L64

この InformersMap を生成する処理が上記の NewInformersMap であり InformersMap もclientと同様に

  • structured(typed)
  • unstructured
  • metadata

というObjectの型に応じた3種類のInformerMapを持っていることがわかります。

この3種類のInformerMapには specificInformersMap というstructが使われています。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cache/internal/informers_map.go#L89-L144

3種類のInformerMapを生成する際にはそれぞれ

  • newStructuredInformersMap
  • newUnstructuredInformersMap
  • newMetadataInformersMap

という関数が使用されています。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cache/internal/deleg_map.go#L110-L126

それぞれの関数の違いは newSpecificInformersMap に渡す createListWatcher という引数の関数で、この createStructuredListWatch によって対象となるリソースの LISTWATCH によるデータ取得 ~ 変更監視を行います。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cache/internal/informers_map.go#L190-L214

前のcache clientの項目で InformersMap.Get についての解説を行いましたが、上記のコードの通り ip.informersByGVK に対象GVKのcache clientがなかった場合には ip.addInformerToMap で新たにcache clientの生成を行います。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cache/internal/informers_map.go#L216-L265

addInformerToMap の実装は上記のようになっており、かいつまんでまとめると

  • ip.createListWatcherで対象GVK向けの ListWatcher を生成
  • cache.NewSharedIndexInformer() で対象GVK向けの SharedIndexInformer を生成
  • 生成した SharedIndexInformer を元に MapEntry を生成
  • 生成した MapEntryip.informersByGVK にセット
  • go i.Informer.Run(ip.stop) でInformerを起動してデータキャッシュを開始
  • 生成した MapEntry などを返す

という感じになります。

最終的に GetList によるcacheデータの読み込みは MapEntry が持つ CacheReaderGetList メソッドが呼び出されて実行されることになります。

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.3/pkg/cache/internal/cache_reader.go#L56-L176

以上でcache clientが行っている処理の大まかな処理の流れが説明できたかなと思います。
structure以外の

  • unstructured
  • metadata

もそれぞれ渡す createListWatcher 関数が異なるので ListWatch の処理は異なりますが、それ以外のフローは同じです。

まとめ

controller-runtimeのclientの種類やそれぞれが大まかにどんなことを行っているのかについてある程度コードレベルで紹介ができてかなと思います。
controller-runtimeのclientの仕組みなどについての理解が深まれば幸いです。

Discussion