😎

[Kubernetes] sample-controllerを細かく解説する

2022/10/27に公開

はじめに

kubernetesのsample-controllerはご存知でしょうか?
もし知らない方のために説明しますと、kubernetesのdeploymentリソースをCustomResource経由で管理し、作成/更新/削除処理を自動化したcontrollerです。これからCustomControllerを開発しようとしている方の入門編としてのうってつけのsampleとなっています。
また、kubebuilderやoperator-SDKなどのSDKを使用せずclient-goとcode-generatorで書かれているので、Kubernetesのコードを読んでみようと思う方のコードリーディングの練習にもなります。
CustomController実装初心者がまずは読むべき本であり、私のバイブルである「実践入門 Kubernetesカスタムコントローラへの道」にて、既にsample-controllerの読み方は解説されておりますが、書面のページの都合上は端折られている箇所があります。
そこで、改めてコードリーディングし直したので、ステップ毎に細かく解説を入れてみました。

sample-controllerの動作

sample-controllerでは、CustomResourceDefinitionとCustomResourceをdeployすることで、CustomResourceに定義したdeploymentのnameとreplicasを使って、deploymentを自動作成します。
また、CustomResourceのreplicasを変更すると、自動でdeploymentも更新され、CustomResourceを削除することでdeploymentも削除される動作となります。

CustomResource定義例

以下のCustomResourceをdeployすることで、上記動作を実現することができます。

apiVersion: samplecontroller.k8s.io/v1alpha1
kind: Foo
metadata:
  name: example-foo
spec:
  deploymentName: example-foo
  replicas: 1

Let’s コードリーディング

api

CustomResource定義には以下のspecとstatusがあります。
ユーザでは、specを指定しますが、statusはContollerによって自動で更新されます。
https://github.com/kubernetes/sample-controller/blob/master/pkg/apis/samplecontroller/v1alpha1/types.go

main関数

sample-controllerはmain.goのmain関数から読んでいきます。まず、はじめにシャットダウンシグナルのsetupを行います。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/main.go#L44-L45

SetupSignalHandler関数

シャットダウンシグナルのsetupを行う、SetupSignalHandler関数を解説します。
goroutineでSignal受信を待ち、SIGINTまたはSIGTERMが発生した場合、channelをcloseします。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/pkg/signals/signal.go#L24-L43
Signal定義は以下の通りです。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/pkg/signals/signal_posix.go#L27

以上で、SetupSignalHandler関数の解説を終了します。
main関数の解説に戻ります。

次に、clientset生成します。
kubeClientが標準Resource(deployment)用、
exampleClientがCustomResource用です。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/main.go#L47-L60

clientsetの作成が完了すると、InformerFactory生成されます。
kubeInformerFactoryが標準Resource(deployment)用、
exampleClientがCustomResource用です。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/main.go#L62-L63

clientsetとInformerFactoryを使用し、controllerの生成を行います。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/main.go#L65-L67

Controller構造体のメンバは以下の通りです。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L64-L85

NewController関数

https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L88-L145
controller生成を行う処理を解説します。
長いため、ステップ毎に解説していきます。
まず、schemeの生成します。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L97

次にEvent recorderを生成します。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L99-L102

Controller構造体に、main関数で作成したclientsetやinformerなどの必要コンポーネントを代入します。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L104-L113

ちなみに、<InformerFactory>.Informer関数を実行することで、informerのメンバに値を代入しています。

informer追加の流れ

Informer関数でf.factory.InformerFor関数が呼ばれます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/pkg/generated/informers/externalversions/samplecontroller/v1alpha1/foo.go#L80-L86

InformerFor関数にはdefaultInformer関数の定義も渡されているので、newFunc(f.client, resyncPeriod)の行でdefaultInformer関数が呼ばれます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/pkg/generated/informers/externalversions/factory.go#L169-L190

defaultInformer関数から、NewFilteredFooInformer関数が呼ばれます。
NewFilteredFooInformer関数では、cache.NewSharedIndexInformer関数が呼ばれ、cache.SharedIndexInformer interfaceにcache.sharedIndexInformer構造体が代入されます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/pkg/generated/informers/externalversions/samplecontroller/v1alpha1/foo.go#L55-L78

cache.NewSharedIndexInformer関数定義
https://github.com/kubernetes/client-go/blob/62756eea6f0e65cf1bda27ac7f96e754f2f90951/tools/cache/shared_informer.go#L225-L238

informerとなるcache.SharedInformerの定義
https://github.com/kubernetes/client-go/blob/62756eea6f0e65cf1bda27ac7f96e754f2f90951/tools/cache/shared_informer.go#L35-L198

Eventごとに実行される関数を定義します。
fooInformerではcontroller.enqueueFoo関数が、deploymentInformerではcontroller.handleObject関数が実行されます。
各Event毎に実行される関数は後述します。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L116-L142

NewController関数内で、Event毎に実行される関数は以下の通りです。

enqueueFoo関数

cache.MetaNamespaceKeyFunc関数が呼ばれ、"namespace/name"形式でobjectのkeyを生成し、workqueueに追加します。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L338-L346

handleObject関数

deploymentのOwner Referenceを取得し、Owner名がCustomResourceの名前と同じであれば、上記enqueueFooを呼び出して、workqueueに追加します。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L353-L386

以上で、NewController関数の解説を終了します。
main関数の解説に戻ります。

標準ResourceとCustomResourceのinformerを開始します。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/main.go#L69-L72

最後にcontrollerを開始します。
第一引数の2はWorker数です。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/main.go#L74-L76

Reconcile処理

Reconcile処理ではLoopを使い、本来あるべき姿を保とうとします。
sample-controllerの場合、CustomResourceに定義されたdeploymentのnameとreplicasを使って、以下の処理を自動化します。

  • deployment自動作成
  • deploymentが削除されたら自動再作成
  • CustomResourceの定義が変更されたら、deploymentの更新
  • deploymentのreplicasを使って、CustomResourceのstatus更新

それではReconcile処理の解説をしていきます。

Run関数

Run関数では、In-Memory-Cacheを同期させて、Run関数の引数に指定された数だけWorkerを実行します。
cache.wait.Until関数で無限ループを実行することでReconcile Loopを実現しています。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L158-L168

Reconcile Loopの流れ

以下の流れで最終的にBackoffUntil関数が呼び出されます。
https://github.com/kubernetes/apimachinery/blob/cf171ba0bfc7223d20f45d95eda6a9b32b9918cc/pkg/util/wait/wait.go#L86-L93
https://github.com/kubernetes/apimachinery/blob/cf171ba0bfc7223d20f45d95eda6a9b32b9918cc/pkg/util/wait/wait.go#L124-L136

BackoffUntil関数には、controller.runWorker関数も引数で渡されているため、f()の箇所で実行され、SIGINTもしくはSIGTERMが発生するまで継続されます。
https://github.com/kubernetes/apimachinery/blob/cf171ba0bfc7223d20f45d95eda6a9b32b9918cc/pkg/util/wait/wait.go#L138-L178

runWorker関数

runWorker関数はLoopでprocessNextWorkItem関数を呼び出すのみとなっており、戻り値がtrueの間継続されます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L177-L183

processNextWorkItem関数

https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L185-L238
長いため、ステップ毎に解説します。
まず、workqueue.Get関数にてReconcileの処理対象itemを取得します。
もしqueueが空なら、即座にfalseを返し、processNextWorkItem関数を終了します。
falseをrunWorker関数に返すことでrunWorker関数を終了させます。
つまり、processNextWorkItem関数はqueueが空になるまで、runWorker関数によって実行されます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L188-L192

queueから取り出したitemの処理は以下のように無名関数で実行されます。
https://github.com/kubernetes/sample-controller/blob/master/controller.go#L194-L238

上記関数をステップ毎に順を追って解説します。

processNextWorkItem関数処理終了後にはworkqueue.Done関数が完了させます。
もし、workqueue.Forget関数が実行されていれば、workqueueからitemを取り除かれます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L202

queueに入っているitemはリソースの"namespace/name"形式のstringで入っています。
まず、型アサーションでkeyを取り出します。もしstringでない場合は、workqueue.Forget関数がitemの追跡を停止します。その後、deferで前述したworkqueue.Done関数が実行され、workqueueからitemを取り除かれます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L205-L217

取り出したkeyを引数にsyncHandle関数でdeploymentを作成します。
もしエラーが返ってきたら、AddRateLimited関数で遅延してrequeueし、再度処理対象とします。
この時、workqueue.Done関数では、itemは取り除かれません。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L218-L224

syncHandle関数が正常終了した場合は、workqueue.Forget関数でitemの追跡を停止し、deferでworkqueueからitemを取り除きます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L225-L229

以上無名関数の解説でした。
processNextWorkItem関数の解説に戻ります。

無名関数でエラーが発生した場合は、エラーを出力しtrueを返し、エラーがない場合はtrueを返すのみとします。いずれにしても、Reconcile Loopは継続されます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L232-L238

Reconcile Loop中はEvent発生→enqueueFoo関数でitemが入り続け、Loopが継続されます。

次にReconcileの中核であり、リソース作成を行うsyncHandler関数の解説を行います。

syncHandler関数

https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L240-L319

まず、keyは"namespace/name"の形式なので、/を区切り文字としてsplitします。
もしerrorが発生すれば、nilを返し、processNextWorkItem関数にworkqueueからitemを取り除きます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L244-L249

次に、listerを使用し、CustomResource objectをIn-Memory-Cacheから取得します。
errors.IsNotFound(err)はerrがNot Foundエラーか否かを判定する関数で、もしNot Foundエラーであればtrueを返し、そうでなければfalseを返します。
以下のコードでは、取得に失敗した場合かつ、Not Foundエラーであればnilをreturnします。
また、Not Foundエラーでなく、単純に取得に失敗しているのであれば、errorをreturnします。
この処理にて、もしCustomResource objectが存在いるにも関わらずエラーが返る場合、processNextWorkItem関数にerrorを返し、workqueue.AddRateLimited関数で遅延requeue後、再度処理を行うことが可能であるためです。
もしerrors.IsNotFound(err)での判定がなければ、CustomResource objectが存在しない場合、requeueが繰り返されます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L251-L263

CustomResource定義の.Spec.DeploymentNameからdeploymentの名前を生成し、もし名前が生成できない場合は、nilをreturnします。nilをreturnしているので、requeueせずに、itemがworkqueueから削除されます。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L264-L271

deployment objectをIn-Memory-Cacheから取得し、Not Foundエラーが返るのであれば、リソースを作成します。存在しているがobjectの取得ができない、もしくはリソースの作成に失敗する場合はrequeue処理に戻し、再度GetとCreateを試みます。
deploymentリソースの定義設定は、newDeployment関数で行うため、後ほど後述します。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L273-L285

この処理ではdeploymentのReplicasの更新を行います。
もし、CustomResourceの.Spec.Replicasと、deploymentの.Spec.Replicasが異なる場合は、CustomResourceの.Spec.Replicasを用いてdeploymentを更新します。
もし更新に失敗した場合は遅延requeueし、再度更新処理を行います。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L295-L308

updateFooStatus関数にて、CustomResourceのstatusを更新します。
もし更新に失敗する場合は遅延requeueし、再度更新処理を行います。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L310-L315

最後にEventを出力し、nilをreturnします。
これにてworkqueueがForget→Doneで完了します。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L317-L318

以上で、Reconcile処理を行う関数の解説を終了します。
最後に、deploymentの定義作成とCustomResourceのstatus更新を行う関数の解説を行います。

newDeployment関数

CustomResourceに定義されているdeployment定義より、deployment定義が作成されます。
また、”OwnerReferences: []metav1.OwnerReference”の箇所でOwnerReferenceが追加されており、CustomResource削除時にGCによりdeploymetも削除されるようになっています。
https://github.com/kubernetes/sample-controller/blob/master/controller.go#L388-L424

updateFooStatus関数

CustomResourceの.Status.AvailableReplicasと、deploymentの.Status.AvailableReplicasが異なる場合は、CustomResourceの.Status.AvailableReplicas更新します。
https://github.com/kubernetes/sample-controller/blob/0c9c744bef5e2dddb6ac76cb787276d12b2a6d98/controller.go#L321-L333

最後に

sample-controllerを細かく掘り下げてみました。
「実践入門 Kubernetesカスタムコントローラへの道」のsample-controllerの章の読書時や、これからsample-controllerをコードリーディングしようとしている方に、本記事が細かい箇所の手助けとなれば幸いです。

Discussion