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

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

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

Rancher API Serverの基礎となるNorman API Framework

ここまで紹介した、pkg/api/customization、pkg/api/storeは、このAPI Framework機能をふんだんに利用して実装しているため、実装されているロジックを理解するために、まずはこのAPI Framework自体を理解する必要があります。そのため、このAPI Frameworkの機能もRancher APIの実装を用いながら紹介します。

先ほど紹介したとおり、pkg/api/server/server.goは、Rancher APIに関する初期化処理に責任を持ち、このファイルの中で、NormanのAPI Serverも初期化されています。

リスト10:pkg/api/server/server.go

  1 package server
  2
  3 import (
... <省略>
  9     rancherapi "github.com/rancher/rancher/pkg/api"
... <省略>
 22 )
 23
 24 func New(ctx context.Context, scaledContext *config.ScaledContext, clusterManager *clustermanager.Manager,
 25     k8sProxy http.Handler) (http.Handler, error) {
... <省略>
 39     server, err := rancherapi.NewServer(scaledContext.Schemas)
 40     if err != nil {
 41         return nil, err
 42     }
... <省略>
 51     return server, err
 52 }

39行目のrancherapi.NewServerにSchema(vendor/github.com/rancher/types/apis/management.cattle.io/v3/schema/schema.go)を渡して、API Serverを初期化しています。

rancherapi.NewServerは、下記のpkg/api/server.go内で定義されています。

リスト11:pkg/api/server.go(の一部)

  1 package api
  2
  3 import (
... <省略>
  7     normanapi "github.com/rancher/norman/api"
... <省略>
 10 )
 11
 12 func NewServer(schemas *types.Schemas) (*api.Server, error) {
 13     server := normanapi.NewAPIServer()
 14     if err := server.AddSchemas(schemas); err != nil {
 15         return nil, err
 16     }
 17     server.CustomAPIUIResponseWriter(cssURL, jsURL, settings.APIUIVersion.Get)
 18     return server, nil
 19 }
 20
<省略>

NewServer内ではNormanのAPI Serverを初期化(13行目)し、Schemaを設定(14行目)しています。また17行目では、UI用のcss、jsのURLを設定しています。

このAPI Serverは、複数のSchema(types/types.go)オブジェクトを保持することができます。ServeHTTP関数を実装しており、HTTPリクエストを受けると、登録されているSchemaをバージョン、リソースタイプ名で検索し、適切なSchemaオブジェクトを探します。適切なSchemaオブジェクトが見つかった場合、SchemaオブジェクトにされているHandler関数を実行します。

Norman API Serverの挙動

Norman API Serverの挙動

Schemaオブジェクトは、次のような構造体になっています。

リスト12:Schemaオブジェクト(vendor/github.com/rancher/norman/types/types.goにある構造体)

type Schema struct {
    ID                   string            `json:"id,omitempty"`
    Embed                bool              `json:"embed,omitempty"`
    EmbedType            string            `json:"embedType,omitempty"`
    CodeName             string            `json:"-"`
    CodeNamePlural       string            `json:"-"`
    PkgName              string            `json:"-"`
    Type                 string            `json:"type,omitempty"`
    BaseType             string            `json:"baseType,omitempty"`
    Links                map[string]string `json:"links"`
    Version              APIVersion        `json:"version"`
    PluralName           string            `json:"pluralName,omitempty"`
    ResourceMethods      []string          `json:"resourceMethods,omitempty"`
    ResourceFields       map[string]Field  `json:"resourceFields"`
    ResourceActions      map[string]Action `json:"resourceActions,omitempty"`
    CollectionMethods    []string          `json:"collectionMethods,omitempty"`
    CollectionFields     map[string]Field  `json:"collectionFields,omitempty"`
    CollectionActions    map[string]Action `json:"collectionActions,omitempty"`
    CollectionFilters    map[string]Filter `json:"collectionFilters,omitempty"`
    DynamicSchemaVersion string            `json:"dynamicSchemaVersion,omitempty"`
    Scope                TypeScope         `json:"-"`

    InternalSchema      *Schema             `json:"-"`
    Mapper              Mapper              `json:"-"`
    ActionHandler       ActionHandler       `json:"-"`
    LinkHandler         RequestHandler      `json:"-"`
    ListHandler         RequestHandler      `json:"-"`
    CreateHandler       RequestHandler      `json:"-"`
    DeleteHandler       RequestHandler      `json:"-"`
    UpdateHandler       RequestHandler      `json:"-"`
    InputFormatter      InputFormatter      `json:"-"`
    Formatter           Formatter           `json:"-"`
    CollectionFormatter CollectionFormatter `json:"-"`
    ErrorHandler        ErrorHandler        `json:"-"`
    Validator           Validator           `json:"-"`
    Store               Store               `json:"-"`
}

このSchemaは、リソースごとに作成されることが期待されています。ここで、いくつか注目してほしいフィールドを紹介します。

Schemaオブジェクトのフィールド(一部)

フィールド名説明
IDリソースタイプが指定される
VersionAPIのVersionが指定される
CollectionMethodsこのSchemaが実装しているAPI Path(/<Version>/<ID>)に対するHTTP Methodが指定される
CollectionActionsこのSchemaが実装しているAPI Path(/<Version>/<ID>)に対するActionが指定されている
ResourceMethodsこのSchemaが実装しているAPI Path(/<Version>/<ID>/<Resource Name>)に対するHTTP Methodが指定される
API PathよりSchemaが特定できたとしても、対応していないHTTP Methodで呼ばれていた場合405ステータスコードが返却される
ResourceActionsこのSchemaが実装しているAPI Path(/<Version>/<ID>/<Resource Name>)に対するActionが指定されている
ResourceFieldsこのSchemaで登録されるリソースがどのようなフィールドをどのような型で持っているのかが指定されている
ActionHandleractionという名前のURLパラメータが指定された際に実行される関数
(/<Version>/<ID>?action=<Action Name>)
(/<Version>/<ID>/<Resource Name>?action=<Action Name>
ListHandlerGET /<Version>/<ID>が呼ばれた際に実行される関数
CreateHandlerPOST /<Version>/<ID>が呼ばれた際に実行される関数
UpdateHandlerPUT /<Version>/<ID>/<Resource Name>が呼ばれた際に実行される関数
DeleteHandlerDELETE /<Version>/<ID>/<Resource Name>が呼ばれた際に実行される関数
StoreByID、List、Create、Update、Delete、Watchを実装したオブジェクトが指定される
このSchemaのXXXHandlerを通じて、リソースの取得、更新、削除をする際は、このオブジェクトの関数を通して実現している
MapperStoreに保存される際(ToInternal)と、Storeから取得する際(FromInternal)に呼び出されるロジックを実装したオブジェクトが指定される

このように、Schemaでは特定のリソースタイプのAPI(/<Version>/<ID>)の具体的な処理(XXXHandler、Store、Mapper)、どのHTTP Methodでアクセス可能か(XXXXMethods)、リソースのフィールドはどのようなものがあるのか(ResourceFields)が指定されています。

Handler、Store、Mapperの関係性にいまいちピンとこない方もいらっしゃると思いますので、もう少し詳しく紹介したいと思います。Norman API Serverでは、SchemaのHandler、Store、Mapperを入れ替えることによって、リソースごとにロジックや保存先を決定することができます。しかしRancherは、基本的にはHandlerを入れ替えることはせず、デフォルトのHandler(api/handler/XXX.go)を利用して、Handler内で呼び出しているStoreを入れ替えて、保存先を指定しています。また、Storeについても基本的には、store/proxy/proxy_store.goで定義されているNewProxyStore関数を使って生成できるKubernetesにリソースを保存/取得するためのStoreオブジェクトを、リソースタイプごとに生成し利用しています。そのため、保存/取得の際のリソースの変換処理については、NewProxyStore関数で生成されるStore内で呼び出しているMapperで指定しています。

Rancherは、これらのStore、ActionHandler、Formatterなどを、pkg/api/server内で指定しています。また肝心のリソースごとのSchemaですが、Schemaはvendor/github.com/rancher/types/apis/management.cattle.io/v3/schemaで定義されています。実はSchemaオブジェクトを直接生成しているのではなく、Normanで提供されているSchemaの自動生成機能を利用しています。

Schemaの自動生成を使う場合は、まず対象となるリソースのフィールドを定義した構造体を用意する必要があります。この構造体は、特定のリソースタイプのみを対象とするKubernetes Client(typed client、listeners)を自動生成するためにも利用されており、Kubernetesのカスタムコントローラを実装する際は常に必要になります。

Rancherは、Rancherが作成するすべてのCRDについて、この構造体を下記のファイルに定義しています。

  • vendor/github.com/rancher/types/apis/management.cattle.io/v3/<Resourceの種類>_types.go
  • vendor/github.com/rancher/types/apis/management.cattle.io/v3public/<Resourceの種類>_types.go
  • vendor/github.com/rancher/types/apis/project.cattle.io/v3/<Resourceの種類>_types.go

サンプルとして、Catalogリソースの構造体定義を掲載します。

リスト13:vendor/github.com/rancher/types/apis/management.cattle.io/v3/catalog_types.go

  1 package v3
  2
  3 import (
  4     "github.com/rancher/norman/condition"
  5     "k8s.io/api/core/v1"
  6     metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  7 )
  8
  9 type Catalog struct {
 10     metav1.TypeMeta `json:",inline"`
 11     // Standard object’s metadata. More info:
 12     // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#metadata
 13     metav1.ObjectMeta `json:"metadata,omitempty"`
 14     // Specification of the desired behavior of the the cluster. More info:
 15     // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#spec-and-status
 16     Spec   CatalogSpec   `json:"spec"`
 17     Status CatalogStatus `json:"status"`
 18 }
... <省略>

Normanは、このリソース構造体を元に、このリソースに関するSchemaを自動生成することができます。Rancherは、下記のファイルで自動生成しています。Catalog関連のリソースのSchemaを自動生成するロジックを例に、どのように自動生成しているか紹介します。

リスト14:vendor/github.com/rancher/types/apis/management.cattle.io/v3/schema/schema.go

... <省略>
 52 func catalogTypes(schemas *types.Schemas) *types.Schemas {
 53     return schemas.
 54         AddMapperForType(&Version, v3.Catalog{},
 55             &m.Move{From: "catalogKind", To: "kind"},
 56             &m.Embed{Field: "status"},
 57             &m.Drop{Field: "helmVersionCommits"},
 58         ).
 59         MustImportAndCustomize(&Version, v3.Catalog{}, func(schema *types.Schema) {
 60             schema.ResourceActions = map[string]types.Action{
 61                 "refresh": {},
 62             }
 63             schema.CollectionActions = map[string]types.Action{
 64                 "refresh": {},
 65             }
 66         }).
 67         AddMapperForType(&Version, v3.Template{},
 68             m.DisplayName{},
 69         ).
 70         MustImport(&Version, v3.Template{}, struct {
 71             VersionLinks map[string]string
 72         }{}).
 73         MustImport(&Version, v3.TemplateVersion{}).
 74         MustImport(&Version, v3.TemplateContent{})
 75 }
... <省略>

このcatalogTypes関数は、Schemaを複数含有するSchemasオブジェクト(schemas)を引数に取ります。このオブジェクトは新しくSchemaを作成し、Schemasに追加するための関数をいくつか提供し、Rancherもそれを利用しています。

AddMapperForType(norman/types/reflection.go)

この関数は、Schemaの生成はせず、SchemasオブジェクトでリソースタイプをKeyに作成したMapperオブジェクトを保持します。そしてSchemasオブジェクト経由で該当リソースタイプのSchemaオブジェクトが生成された際に、MapperがSchemaに設定されます。

バージョン(第一引数)、リソース(第二引数)を指定し、該当のMapperを作成します。Mapperは、リソースの変換ルールを第三引数以降に複数指定することができます。上述のリストの55~57行目でリソースの変換ルールを定義しています

  • &m.Move{From: "catalogKind", To: "kind"}
    • Kubernetes上のCatalogリソースの"catalogKind"というフィールドをAPIリソースに変換する際に"kind"に変換します
  • &m.Embed{Field: "status"}
    • Kubernetes上のCatalogリソースの"status"というフィールドをAPIリソースに変換する際に、"status"フィールド内のサブフィールドを展開します
  • &m.Drop{Field: "helmVersionCommits"}
    • Kubernetes上のCatalogリソースの"helmVersionCommits"というフィールドをAPIリソースに変換する際に削除します

MustImport(norman/types/reflection.go)

バージョン(第一引数)、リソース(第二引数)を指定し、該当リソースタイプのSchemaを作成します。

MustImportAndCustomize(norman/types/reflection.go)

バージョン(第一引数)、リソース(第二引数)を指定し、該当リソースタイプのSchemaを作成します。この関数は、Schema作成後に、Schemaに対して実施したい追加ロジックを第三引数の関数で指定することができます。この関数は、Schema作成後にSchemaを引数に実行されます。59行目のCatalogリソースのMustImportAndCustomizeでは、生成したCatalogリソースのSchemaに対してActionを指定しています。

このような関数を利用して、Rancherはすべてのリソースに対してSchemaを自動生成し、Norman API Serverを初期化することで、Rancher Management APIを実装しています。

まとめ

この記事では、Rancher Agentのロジックが含まれるpkg/agentとRancher APIのロジックが含まれているpkg/api、またpkg/api内でも利用されているNormanのAPI Frameworkについて詳しく紹介しました。Rancher APIがどのように実現されているかある程度イメージを掴んでいただけたと思います。次回の記事では、Rancher Controller(pkg/controllers)がどのように実装されているのかを紹介します。

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のWebサイトにログインすることでさまざまな限定特典を入手できるようになります。

Think IT会員サービスの概要とメリットをチェック

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