controller-runtime clientについて
KubernetesでOperatorやControllerを開発する際に利用するフレームワークであるcontroller-runtimeのclientについて調べたのでまとめます。
この記事の目的は以下のような感じになります:
- controller-runtimeが提供するKubernetes clientの概要についてまとめること
- controller-runtime client周りの追加の不明点などがあった場合には、この記事をベースにコードベースで調べたいことをすぐに調べられる程度にはコードレベルで詳しい内容をまとめること
- 以下についてわかるようになること
- 各種内部clientについて理解できるようになること
- typedClient
- unstructuredClient
- metadataClient
- Informerの仕組みについてコードレベルで理解できるようになること
- TODO(今後追記するかも): DynamicRESTMapperについて理解すること
- 各種内部clientについて理解できるようになること
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
インターフェイスを通しているため Get
と List
のみが提供されています)
delegatingClient
delegatingClientは mgr.GetClient()
で取得することができるclientです。
delegatingClientは DefaultNewClient
関数を通して生成されます。
delegatingClientは
- GetやListなどの読み込み系の処理を行う際には基本的にキャッシュからデータを返す(都度API Serverとの通信を行わない)
- CreateやUpdateなどの更新系の処理の際は都度API Serverとやり取りをして処理を行う
という特徴を持っています。
そのためcontoroller-runtimeを使ったControllerの開発では、基本的にはこのdelegatingClientを使っていくことになります。
また、GetやListの際に利用するキャッシュというのは上記の図で赤で囲った部分のような仕組みになっており
- はじめにListで対象リソース一覧を取得
- 取得したリソースをインメモリにキャッシュ
- 以降Watchで変更があった際に変更内容を取得しキャッシュを更新
という方法でキャッシュの同期を行っています。
delegatingClientの仕組み
NewDelegatingClient
の中身は以下のようになっています。
見ればなんとなくイメージがつくかも知れませんが内部では
- Reader
- Writer
をembedしています。
更にReaderの中には
- CacheReader
- ClientReader
を持っている構成になっています。
embedしている Writer
のinterfaceを見ると以下の様になっており、 Create
や Update
などの更新系の処理は NewDelegatingClient
の引数で渡されたclientがそのまま使われていることがわかります。
一方で Reader
のinterfaaceは以下の様になっており、Get
と List
が定義されています。
この Reader
には delegatingReader
というものが使われています。
delegatingReader
の Get
と List
は上記のようになっており、Get
と List
で取得するリソースについて
- controller-runtimeの
ClientDisableCacheFor
オプションに含まれているGVKである -
Get
またはList
で取得した結果をUnstructured(List)
にbindしようとしている
のいずれかの条件にマッチした場合はcacheではなくclientが使われて、API Serverからデータを取得します。
それ以外についてはcacheからデータの取得を行います。
cache client
cache clientは mgr.GetCache()
で取得することができるclientです。
Get
と List
のメソッドがあって取得系の処理ではclientとみなすことができると思うのでこの記事ではclientとして紹介しています。
cacheもdelegatingClientと同様にオプションで独自cacheを返す関数を渡すことができますが、デフォルトでは cache.New
関数を通して生成されます。
delegatingClientの部分で軽く紹介してしまいましたが、delegatingClientは内部でこのcacheを取得系の処理に使用しているため、特に理由が無い限りはcache clientを利用する必要はなく、データ取得にはdelegatingClientを利用すれば問題ないと思います。
cache clientの仕組み
cache clientのinterfaceは上記の様になっており
cache.Reader
Informers
のInterfaceのメソッドを持っていることになります。
1つ前の節で紹介した cache.New
を見ると cache.New
が返すstructは informerCache
というものであるということがわかります。
この informerCache
が提供する Get
と List
は上記のようになっており、 informerCache
が持つ InformersMap.Get
を通してデータを取得したいGroupKindVersion(GVK)別のcache clientを InformersMap
が返し、取得したcache clientが持つ Reader
から Get
あるいは List
を行っているのがわかります。
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となってるため基本的には Get
と List
のみが使用できます。
このclientだけはオプションで独自のclientを返すことはできない設計になっており、 client.New
関数によって Client
interfaceを満たしたclientが返されます。
そのため実際には Get
と List
以外にも Create
や Update
なども利用可能で、delegatingClientの更新系の処理はこの client.New
関数によって生成されたclientが利用されています。
このclientは Get
と List
によるデータ取得の際にも都度API Serverと通信を行った結果を返すため、厳密に最新のデータを利用しなくてはならないような場面では mgr.GetAPIReader()
で取得したclientを使うことになります。
clientの仕組み
client.New
関数が内部で呼び出している newClient
は以下のようになっています。
色んな処理を行っていますが最終的にはclient
- typedClient
- unstructuredClient
- metadataClient
という3種類の内部clientを設定してclientを生成していることがわかります。
そして Create
などの中身では以下のように引数で渡されたobjectのstructに応じて対象となるclientを通してデータの取得や更新が行われる仕組みとなっています。
内部clientについて
内部clientという呼び方についてはこの記事で適当に呼んでいるだけですが
- typedClient
- unstructuredClient
- metadataClient
という3種類の内部clientの中身について見ていきます
typedClient
typedClientが使われるのはPodやDeploymentといったリソースのstructをobjectとして渡されたケースです。
そのため基本的にはこのtypedClientが使われることになります。
例えばtypedClientの Create
の実装は上記の様になっており
-
c.cache.getObjMeta()
で渡されたObjectのリソース用のREST clientを生成 or 取得 - REST clientからHTTPリクエストを実行
という処理を行っています。
Create
以外の GET
や UPDATE
なども同じような処理を行っています。
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です。
unstructuredClientの Create
の実装は上記の様になっており、typedClientと同様に
-
c.cache.getObjMeta()
で渡されたObjectのリソース用のREST clientを生成 or 取得 - REST clientからHTTPリクエストを実行
という処理を行っています。
Create
以外の GET
や UPDATE
なども同じようになっています。
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
についてはサポートされていません。
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.New
で informerCache
を生成するということを説明しました。
この informerCache
は InformersMap
というstructをembedしており、キャッシュ処理を行うInformerとしての役割の多くは実際にはこの InformersMap
が担っています。
この InformersMap
を生成する処理が上記の NewInformersMap
であり InformersMap
もclientと同様に
- structured(typed)
- unstructured
- metadata
というObjectの型に応じた3種類のInformerMapを持っていることがわかります。
この3種類のInformerMapには specificInformersMap
というstructが使われています。
3種類のInformerMapを生成する際にはそれぞれ
- newStructuredInformersMap
- newUnstructuredInformersMap
- newMetadataInformersMap
という関数が使用されています。
それぞれの関数の違いは newSpecificInformersMap
に渡す createListWatcher
という引数の関数で、この createStructuredListWatch
によって対象となるリソースの LIST
と WATCH
によるデータ取得 ~ 変更監視を行います。
前のcache clientの項目で InformersMap.Get
についての解説を行いましたが、上記のコードの通り ip.informersByGVK
に対象GVKのcache clientがなかった場合には ip.addInformerToMap
で新たにcache clientの生成を行います。
addInformerToMap
の実装は上記のようになっており、かいつまんでまとめると
- ip.createListWatcherで対象GVK向けの
ListWatcher
を生成 -
cache.NewSharedIndexInformer()
で対象GVK向けのSharedIndexInformer
を生成 - 生成した
SharedIndexInformer
を元にMapEntry
を生成 - 生成した
MapEntry
をip.informersByGVK
にセット -
go i.Informer.Run(ip.stop)
でInformerを起動してデータキャッシュを開始 - 生成した
MapEntry
などを返す
という感じになります。
最終的に Get
や List
によるcacheデータの読み込みは MapEntry
が持つ CacheReader
の Get
や List
メソッドが呼び出されて実行されることになります。
以上でcache clientが行っている処理の大まかな処理の流れが説明できたかなと思います。
structure以外の
- unstructured
- metadata
もそれぞれ渡す createListWatcher
関数が異なるので List
と Watch
の処理は異なりますが、それ以外のフローは同じです。
まとめ
controller-runtimeのclientの種類やそれぞれが大まかにどんなことを行っているのかについてある程度コードレベルで紹介ができてかなと思います。
controller-runtimeのclientの仕組みなどについての理解が深まれば幸いです。
Discussion