Rancherコードリーディング入門(3/3)

pkg/controllers/management、pkg/controllers/user
「3種類に分けられるRancher Controller」と「Rancher ControllerのベースとなるNorman Generic Controller」について学んだところで、本題のRancher Controllerの実装に話題を戻したいと思います。まずは、pkg/controllers配下のディレクトリとファイルの一覧を確認してみます。
リスト5:pkg/controllers/managementのファイル
pkg/controllers/management/ ├── auth ├── catalog ├── clusterdeploy ├── clusterevents ├── clustergc ├── clusterprovisioner ├── clusterstats ├── clusterstatus ├── compose ├── controller.go ├── node ├── nodedriver ├── nodepool ├── podsecuritypolicy ├── test └── usercontrollers
リスト6:pkg/controllers/userのファイル
pkg/controllers/user/ ├── alert ├── approuter ├── controllers.go ├── dnsrecord ├── endpoints ├── eventssyncer ├── externalservice ├── healthsyncer ├── helm ├── ingress ├── ingresshostgen ├── logging ├── networkpolicy ├── noderemove ├── nodesyncer ├── nslabels ├── pipeline ├── rbac ├── resourcequota ├── secret ├── systemimage ├── targetworkloadservice └── workload
pkg/controllers配下には、managementとuserの2つのディレクトリが存在します。また、pkg/controllers/management/とpkg/controllers/user/は、それぞれ、Management ControllersとUser Controllersを実現しており、それぞれのディレクトリが複数のコントローラを含んでいることがわかります。
Rancherは複数のコントローラを実装していますが、それぞれ特定のルールに従って、ビジネスロジックをNormanのGeneric ControllerというKubernetes Custom Controllerを開発するためのFrameworkに登録しているだけですので、ルールさえわかってしまえば、コントローラの実装を把握するのは、容易です。ここではそのルールを理解するために、もう少しコードに焦点を当てて、下記の点を紹介します。
- Controllerの初期化のエントリポイント
- 各Controllerの初期化の流れについて
Controllerの初期化のエントリポイント
まずは、Controllerの初期化のエントリポイントから紹介します。RancherのManagement Controller、User Controllerを初期化するための関数は、それぞれ、pkg/controllers/management/controller.go、pkg/controllers/user/controller.goに含まれています。
どちらのファイルも似たような構成になっているため、ここではManagement Controllerを例に紹介します。
リスト7:pkg/controllers/management/controller.go
1 package management
2
3 import (
4 "context"
5
6 "github.com/rancher/rancher/pkg/clustermanager"
7 "github.com/rancher/rancher/pkg/controllers/management/auth"
8 "github.com/rancher/rancher/pkg/controllers/management/catalog"
9 "github.com/rancher/rancher/pkg/controllers/management/clusterdeploy"
10 "github.com/rancher/rancher/pkg/controllers/management/clusterevents"
11 "github.com/rancher/rancher/pkg/controllers/management/clustergc"
12 "github.com/rancher/rancher/pkg/controllers/management/clusterprovisioner"
13 "github.com/rancher/rancher/pkg/controllers/management/clusterstats"
14 "github.com/rancher/rancher/pkg/controllers/management/clusterstatus"
15 "github.com/rancher/rancher/pkg/controllers/management/compose"
16 "github.com/rancher/rancher/pkg/controllers/management/node"
17 "github.com/rancher/rancher/pkg/controllers/management/nodedriver"
18 "github.com/rancher/rancher/pkg/controllers/management/nodepool"
19 "github.com/rancher/rancher/pkg/controllers/management/podsecuritypolicy"
20 "github.com/rancher/rancher/pkg/controllers/management/usercontrollers"
21 "github.com/rancher/types/config"
22 )
23
24 func Register(ctx context.Context, management *config.ManagementContext, manager *clustermanager.Manager) {
25 // auth handlers need to run early to create namespaces that back clusters and projects
26 // also, these handlers are purely in the mgmt plane, so they are lightweight compared to those that interact with machines and clusters
27 auth.RegisterEarly(ctx, management)
28 usercontrollers.RegisterEarly(ctx, management, manager)
29
30 // a-z
31 catalog.Register(ctx, management)
32 clusterdeploy.Register(management, manager)
33 clusterevents.Register(ctx, management)
34 clustergc.Register(management)
35 clusterprovisioner.Register(management)
36 clusterstats.Register(management, manager)
37 clusterstatus.Register(management)
38 compose.Register(management, manager)
39 nodedriver.Register(management)
40 nodepool.Register(management)
41 node.Register(management)
42 podsecuritypolicy.Register(management)
43
44 // Register last
45 auth.RegisterLate(ctx, management)
46 }
このRegister関数は、app/app.goでmanagement.Registerとして呼び出されています。このRegisterで実施していることは、同じディレクトリ配下に存在するManagement Controllerをimportし、それぞれRegister関数を呼び出しています。このように各Controllerは、Register関数を実装し、初期化に関するコードは、Register関数内に定義することがルールになっています。
User Controllerの場合は、Importしているパッケージが違うだけで、基本的に同じルールを採用しており、各コントローラのRegister関数を呼び出し初期化を実施しています。
1つのコントローラのRegister関数がどのように実装されているのかを、Management Controllerの1つであるnodepoolコントローラ(pkg/controllers/management/nodepool/nodepool.go)を例に見ていきます。ここでは、コントローラの初期化処理というトピックに集中したいため、nodepoolのビジネスロジック部分のコードについては省略しています
リスト8:pkg/controllers/management/nodepool/nodepool.go
1 package nodepool
2
3 import (
4 "fmt"
5 "regexp"
6 "sort"
7 "strconv"
8 "time"
9
10 "reflect"
11
12 "github.com/rancher/rancher/pkg/ref"
13 "github.com/rancher/rke/services"
14 "github.com/rancher/types/apis/management.cattle.io/v3"
15 "github.com/rancher/types/config"
16 "github.com/sirupsen/logrus"
17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18 "k8s.io/apimachinery/pkg/labels"
19 "k8s.io/apimachinery/pkg/runtime"
20 )
21
22 var (
23 nameRegexp = regexp.MustCompile("^(.*?)([0-9]+)$")
24 )
25
26 type Controller struct {
27 NodePoolController v3.NodePoolController
28 NodePoolLister v3.NodePoolLister
29 NodePools v3.NodePoolInterface
30 NodeLister v3.NodeLister
31 Nodes v3.NodeInterface
32 }
33
34 func Register(management *config.ManagementContext) {
35 p := &Controller{
36 NodePoolController: management.Management.NodePools("").Controller(),
37 NodePoolLister: management.Management.NodePools("").Controller().Lister(),
38 NodePools: management.Management.NodePools(""),
39 NodeLister: management.Management.Nodes("").Controller().Lister(),
40 Nodes: management.Management.Nodes(""),
41 }
42
43 // Add handlers
44 p.NodePools.AddLifecycle("nodepool-provisioner", p)
45 management.Management.Nodes("").AddHandler("nodepool-provisioner", p.machineChanged)
46 }
47
48 func (c *Controller) Create(nodePool *v3.NodePool) (*v3.NodePool, error) {
49 return nodePool, nil
50 }
51
52 func (c *Controller) Updated(nodePool *v3.NodePool) (*v3.NodePool, error) {
... <省略>
57 }
58
59 func (c *Controller) Remove(nodePool *v3.NodePool) (*v3.NodePool, error) {
... <省略>
80 }
81
82 func (c *Controller) machineChanged(key string, machine *v3.Node) error {
... <省略>
97 }
98 <省略>
始めに、引数に渡されている「management *config.ManagementContext」ですが、このManagementContextは、app/app.go内で初期化され、Rancherが起動しているKubernetesクラスタを利用するためのKubernetes API Client、NormanのGeneric Controller(正確には、Generic Controllerをラップした各リソースごとのController)を含んでいます。
続いて、そのManagementContextを利用して、下記のような3つの関数を呼び出して、コントローラの実行に必要なKubernetes ClientやGeneric Controllerを用意しています。
- management.Management.NodePools("").Controller()
- management.Management.NodePools("").Controller().Lister()
- management.Management.NodePools("")
management.Management.NodePools("")は、NodePoolリソースに対するKubernetes Clientを返します。返り値のClientを利用して、NodePoolの取得や更新、またはNodePoolリソースに関するGeneric Controllerの取得ができます。このClientは、vendor/github.com/rancher/types/apis/management.cattle.io/v3/zz_generated_node_pool_controller.goで定義されています。
management.Management.NodePools("").Controller()は、Generic Controllerを返します。返り値のGeneric Controllerに対してAddHandlerやAddLifecyleでNodePoolの変更イベントでトリガされる関数を登録することができます。
management.Management.NodePools("").Controller().Lister()は、List関数とGet関数を実装したNodePools用のKubernetes Client(実際は、Informerによってメンテナンスされるキャッシュへのアクセスインターフェース)を返します。このClientを使ってNodePoolの一覧やNodePoolの単体の情報を取得することができます。
Kubernetes ClientとGeneric Controllerが用意できたら、次に実施するのは、Generic Controllerに対するビジネスロジック(Handler、Lifecycle)の登録です。下記の2行がそれに当たります。
リスト9:nodepool.goの一部
43 // Add handlers
44 p.NodePools.AddLifecycle("nodepool-provisioner", p)
45 management.Management.Nodes("").AddHandler("nodepool-provisioner", p.machineChanged)
44行目のp.NodePools.AddLifecycleは、NodePool Kubernetes Clientに対して呼び出されていますが、Kubernetes Client側で、LifecycleオブジェクトをHandlerに変換し、Generic ControllerにHandlerとして登録しています。ここで登録しているLifecycleオブジェクトは、Create、Updated、Remove関数がきちんと実装されていることがわかります。
45行目の management.Management.Nodes("").AddHandlerも、NodePool Kubernetes Clientに対して呼び出されています。このAddHandler関数は、特に変換処理などはせず、渡された引数をそのままGeneric ControllerのAddHandler関数に渡し実行しています。
ここまでで、コントローラの初期化処理は終了です。各コントローラの初期化処理が一通り終わった後(pkg/controllers/management/controller.goのRegister関数が評価し終わった後)、ManagementContextのStart関数を実行するとManagemetContextに紐づくすべてのGeneric Controllerが起動され、リソースに変更があった時にRegister関数で登録したHandlerが実行されるようになります。
Management Controllersでは、config.ManagementContextがRegister関数の引数に渡され、ManagementContextのKubernetes Client、Controllerを利用していましたが、User Controllersの場合は、config.UserContext と config.UserOnlyContextのKubernetes Client、Controllerを利用します。config.UserContextとconfig.UserOnlyContextの違いは、Rancherが起動しているKubernetesクラスタへのKubernets Clientの参照を持つかどうかです。
ここまでの情報を元にすれば、その他のコントローラの初期化処理も以前より読みやすくなっているかと思います。
ContextとController
コントローラの初期化処理を説明する中で、ContextからKubernetes Client、Generic Controllerを取得すると説明しました。Rancherのコードを読む上で欠かせないのは、Contextの理解です。Contextは、様々な関数に渡され、関数内でContextから必要なデータ、Clientを取得して処理を実施しています。
Rancherは4種類のContextを生成し、各Contextは特定のコントローラと紐づけられています。
- ScaledContextは、API Controllers
- ManagementContextは、Management Controllers
- UserContext、UserOnlyContextは、User Controllers
これらContextはStart関数を実装しており、Handlerがすべて登録し終わった後にStart関数を実行し、GeneriContollerを起動することが想定されています。
重要な関連レポジトリとの関わり
ここまで、rancher/rancher内の各ファイル、ディレクトリの責任範囲や実装を紹介しました。しかしRancherは、Rancher Lab社が開発しているその他のレポジトリにも強く依存しています。依存度が比較的高いものを1つずつどのようなものなのか、どのように使われているのか紹介します。
rancher/types
Rancherが利用する共通の構造体を、このレポジトリ内で定義しています。具体的には、Rancherのすべてのリソースの構造体、リソースごとのKubernetes Client、リソースごとのGeneric Controller、リソースごとのSchema、4種類のContextなどを定義しています。
rancher/rancherは、これらの構造体を利用して、Rancher APIやRancher Controllerの実装を実施しています。
rancher/rke
Rancherが利用している、Kubernetesインストーラー。RancherでKubernetesを構築する際に、Managed Kubernetes(GKE、EKS……)、Import Kubernetesを利用せずに進めた場合、このインストーラがkontainer-engineを通して利用されます。
rancher/kontainer-engine
Rancherが利用している、Kubernetes Provisioningツール。kontainer-engineは、オンプレミスサーバを含む複数のCloud ProviderのProvisioningを抽象化し、統一されたインターフェースでKubernetesを構築することができるツールです。
現在対応しているCloud Providerは、kontainer-engine/driversで確認でき、次の通りです
- aks
- eks
- gke
- import
- rke
Rancherは複数のCloud Providerに対応していますが、それらの違いは、このkontainer-engineのレイヤーで抽象化しています。そのためRancher Server内のCluster Provisioningに責任を持つ、pkg/controllers/management/clusterprovisionerコントローラは、このkontainer-engineを利用してKubernetesのProvisioningを実施します
rancher/norman
Rancher Serverの実装のベースとなるFrameworkです。代表的なものに下記のようなものを提供しています。
- SchemaベースのAPI Server
- Kubernetes Custom Controllerのベースの実装となるGeneric Controller
- Leader Electionのためのclient-goのラッパー
- Conditionを利用して、特定の処理の実行制御をするための仕組み
Rancher Serverは、これらを利用し、実際のビジネスロジックを実装しています。
rancher/machine
rancher/machineは、docker-machineのforkになります。RancherでCloud Provider上で作成されるServer上にKubernetesを構築するオプションを指定してKubernetesを構築する際、pkg/controllers/management/nodeコントローラがCloud ProviderのAPIを呼び出しServerの構築、Dockerdのインストール、Rancher Node Agentの起動を実施しますが、このServerの構築とDockerdのインストールは、docker-machineを呼び出すことで実現されています。
本連載のまとめ
Rancherのコードリーディング入門の最後として、Rancher Controllerの実装とRancherが依存しているその他のProjectについて紹介させていただきました。
Rancherは非常に多くの機能を提供していることもあり、ボリュームの都合上すべてを紹介することはできていませんが、コードリーディング入門の記事を片手に、実際にrancher/rancherのソースコードを改めて読んでみると、以前よりはソースコードの全体像がつかめるようになると思います。
今回のコードリーディングの連載では、コードと実際のRancherの機能、挙動をマッピングすることに焦点を当てているため、コードリーディング入門からRancherの挙動や動作を知ろうとすると、どうしてもこの連載だけでは苦労してしまうと思います。
例えば、ここまで読んでいただいた方で、次のような疑問をお持ちになられている方はいらっしゃるのではないでしょうか。
- docker run rancher/rancherでRancher Serverを起動した時にEtcdやKubernetesはどこにどうやって起動されているのか
- RancherがどのようにKubernetesを活用しているのか
- Rancherが作成する40種類以上のCRDにはどのようなものがあり、それらがどのようにRancherに利用されているのか
- Rancher APIの認証、認可はどう実装されているのか
- 100種類以上存在するRancher APIはどのように実装されており、どのように使われているのか
- Rancher Agentはどのように、Rancher Serverとやりとりをしているのか
- Rancher ServerのHA機能はどのように実装されているのか
- 3種類に分類されるRancher Controllerは具体的にどんなものが存在し、どのように動くのか
などなど、これらのRancherの挙動については、書籍の方に詳細にまとめていますので、ぜひ書籍も手に取っていただければと思います。その後改めてコードリーディング入門を読んでいただくと、さらにコードとRancherの挙動がうまくマッピングできるようになると思います。
興味のある方は、ぜひこちらも手に取っていただけると幸いです。
連載バックナンバー
Think ITメルマガ会員登録受付中
全文検索エンジンによるおすすめ記事
- Rancherコードリーディング入門(2/3)
- Rancherコードリーディング入門(1/3)
- Kubernetes環境の選択肢
- Oracle Cloud Hangout Cafe Season5 #1「Kubernetes Operator 超入門」(2022年1月19日開催)
- RailsでAction Controllerについて学んでみた
- Phalcon Devtoolsを使ったMVCプロジェクト作成からHelloWorldまで
- Rancherを構成するソフトウェア
- Phalconに触れながらMVCプロジェクトの構成と構造を理解しよう
- fabric-samplesのfabcarを用いてchaincodeの中身を記述する
- Kubernetes上のアプリケーション開発を加速させるツール(1) Skaffold





