この連載が書籍になりました!『RancherによるKubernetes活用完全ガイド

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

2020年2月19日(水)
西脇 雄基
前回に続き、紙面の都合で「RancherによるKubernetes活用完全ガイド」に掲載されなかったパートをご紹介します。

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とコントローラの関係

Contextとコントローラの関係

これら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の挙動がうまくマッピングできるようになると思います。

興味のある方は、ぜひこちらも手に取っていただけると幸いです。

RancherによるKubernetes活用完全ガイド

LINE株式会社

Verda室 Verdaプラットフォーム開発室 マネージャ兼Senior Software Engineer

LINE株式会社にてOpenStackとManaged Kubernetes Clusterの開発/運営を担当しているVerdaプラットフォーム開発チームリード。大規模なクラスターの開発者およびオペレーターとして3年以上OpenStackに取り組んでいる。2018年からはRancherを利用したManaged Kubernetes Serviceの開発/運用も開始。

現在は両プロジェクトを担当し、安定したプライベートクラウドプラットフォームの提供に務める。

連載バックナンバー

クラウド技術解説
第11回

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

2020/2/19
前回に続き、紙面の都合で「RancherによるKubernetes活用完全ガイド」に掲載されなかったパートをご紹介します。
クラウド技術解説
第10回

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

2019/12/25
前回に続き、紙面の都合で「RancherによるKubernetes活用完全ガイド」に掲載されなかったパートをご紹介します。
クラウド技術解説
第9回

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

2019/11/5
今回からは、紙面の都合で「RancherによるKubernetes活用完全ガイド」に掲載されなかったパートをご紹介します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています