client-go・api・apimachineryの機能と関係性

::: note info
これは [ZOZO Advent Calendar 2023](https://qiita.com/advent-calendar/2023/zozo) カレンダー Vol.5 の 1日目の記事です。
:::
# はじめに
KubernetesはGo言語で開発されており、開発に利用されるモジュールとして[client-go](https://github.com/kubernetes/client-go)が利用されることは割とよく知られているかと思います。 
またclient-goを触ると必ず登場するのが[`api`](https://github.com/kubernetes/api)・[`apimachinery`](https://github.com/kubernetes/apimachinery)というモジュールです。

自分がはじめてGo言語でKubernetes APIを操作するプログラムを書いた際には、これらのモジュールの機能・関係性がいまいちよく理解できずにいました。

本記事ではGo言語を使ってKubernetes APIを操作するプログラムを書く際に利用する`client-go`・`apimachinery`・`api`モジュールについてその機能と関係性について調べたことをまとめました。

# client-go
client-goはGo言語でkubeapi-serverへリクエストを送る際のクライアントとして機能します。

前提として、Kubernetesには複数の[コントロールプレーンコンポーネント](https://kubernetes.io/ja/docs/concepts/overview/components/)があり、そのうちの1つとしてkubeapi-serverが提供されています。  
kubeapi-serverはクラスタの内部・外部に対してHTTP APIを公開しており、こちらをクエリすることにより、PodやNodeといったKubernetesリソースのオブジェクトを操作することができます。

例えば、クラスタ外部からKubernetesオブジェクトの操作を行う場合、[kubectl CLI](https://github.com/kubernetes/kubectl)を利用することが一般的かと思います。こちらのCLIもGo言語で開発されており、client-goが利用されています。  
kubectlの[Factoryインターフェース](https://github.com/kubernetes/kubectl/blob/b35935138f5330d299e89b1278b5738487e4f015/pkg/cmd/util/factory.go#L41)の[RESTClientメソッド](https://github.com/kubernetes/kubectl/blob/b35935138f5330d299e89b1278b5738487e4f015/pkg/cmd/util/factory.go#L51)ではclient-goの[RESTClient](https://github.com/kubernetes/client-go/blob/84a6fe7e4032ae1b8bc03b5208e771c5f7103549/rest/client.go#L81)構造体のポインタを返すようになっており、[getコマンド](https://github.com/kubernetes/kubectl/blob/b35935138f5330d299e89b1278b5738487e4f015/pkg/cmd/get/get.go#L439)の実装を追ってみると[RESTClientメソッドでクライアントを生成](https://github.com/kubernetes/kubectl/blob/b35935138f5330d299e89b1278b5738487e4f015/pkg/cmd/get/get.go#L441)し、[作成したクライアントを利用してHTTP GETリクエストを呼んでいる](https://github.com/kubernetes/kubectl/blob/b35935138f5330d299e89b1278b5738487e4f015/pkg/cmd/get/get.go#L445C13-L445C13)ことがわかります。

client-goの中でもよく利用するのはClientset構造体かと思います。
次節ではClientset構造体について説明します。

## Clientset
`Clientset`の場合、ただ単一の`Client`ではなく`Client + set`となっているところが重要な点かと思っています。

client-goを利用する際、まず次のようなコードでclientsetを取得するかと思います。

```go
package main

import (
	"context"
	"flag"
	"fmt"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

var kubeconfig string

func main() {
	flag.StringVar(&kubeconfig, "kubeconfig", "", "kubeconfig path")
	flag.Parse()
    // kubeconfig fileの読み込み
	config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)

	// clientsetの初期化
	clientset, _ := kubernetes.NewForConfig(config)

	// Podオブジェクトの取得
	pod, _ := clientset.CoreV1().Pods("hoge").Get(context.TODO(), "fuga", metav1.GetOptions{})
	fmt.Println(pod.ObjectMeta.Name)
}

```
ここで`NewForConfig（）`の戻り値となる`Clientset`構造体について深掘ります。
こちらの構造体は次のインターフェースを満たす実装を持っています。

```client-go/kubernetes/clientset.go
type Interface interface {
	Discovery() discovery.DiscoveryInterface
 ...省略
	AppsV1() appsv1.AppsV1Interface
	AppsV1beta1() appsv1beta1.AppsV1beta1Interface
	AppsV1beta2() appsv1beta2.AppsV1beta2Interface
...省略
	CoreV1() corev1.CoreV1Interface
...省略
}
```

こちらのインターフェースにはKubernetes APIで定義される`Group/Version`ごとのメソッドが定義されていることがわかります。
そのうちの1つである`CoreV1()`メソッドの戻り値の型は`corev1.CoreV1Interface`となっており、`Corev1()`メソッドはこちらのインターフェースを満たす実装を持つオブジェクトを返します。

```client-go/kubernetes/typed/core/v1/core_client.go
type CoreV1Interface interface {
	RESTClient() rest.Interface
	ComponentStatusesGetter
	ConfigMapsGetter
	EndpointsGetter
	EventsGetter
	LimitRangesGetter
	NamespacesGetter
	NodesGetter
	PersistentVolumesGetter
	PersistentVolumeClaimsGetter
	PodsGetter
	PodTemplatesGetter
	ReplicationControllersGetter
	ResourceQuotasGetter
	SecretsGetter
	ServicesGetter
	ServiceAccountsGetter
}
```

インターフェースを構成する1番上のメソッドとして、kubectlの例で前述した`RESTClient()`メソッドが含まれていることがわかります。こちらは`Group/Version`ごとのクライアントから内部的に利用され、こちらを使ってリクエストが作成されます。
`RESTClient()`メソッドの戻り値は次のインターフェースを満たします。

```client-go/rest/client.go
type Interface interface {
	GetRateLimiter() flowcontrol.RateLimiter
	Verb(verb string) *Request
	Post() *Request
	Put() *Request
	Patch(pt types.PatchType) *Request
	Get() *Request
	Delete() *Request
	APIVersion() schema.GroupVersion
}
```
`Get()`や`Post()`といったメソッドを持っており、インターフェースで定義された各メソッドはHTTPリクエストによりKubernetes APIを操作する`Request`オブジェクトを返すことがわかるかと思います。

上記では`CoreV1()`メソッドのみを追いましたが、他のメソッドについても同様で、メソッド呼び出しをチェーンすることで各`Group/Version`に対応するクライアントを取得し、特定リソースについてKubernetes APIで操作することができます。

これらのことからClientset構造体は様々な`Group/Version`のリソースごとのクライアントの集合となっており、こちらを利用することで様々なリソースに対してKubernetes APIによる操作を行えることがわかります。

Clientsetは一例ですが、client-goモジュールについてはGo言語でKubernetes APIを操作する際のクライアントを提供してくれるものと認識しておくと良さそうです。

# api
apiモジュールはKubernetes APIで定義されたリソースをGoで操作するために必要なGoの構造体の集まりです。  
client-goでもリソースごとの構造体を取得する際にこちらのモジュールが参照されています。

モジュールのディレクトリ階層はKubernetes APIのGroup-Version-Resource構造と一致しています。
例えば`Deployment`リソースの場合、manifest作成時のapiVersionフィールドには`apps/v1`を指定するかと思いますが、`Deployment`リソースに対応するGoの構造体はapiライブラリの`k8s.io/api/app/v1`パッケージに定義されています。
`k8s.io/api/app/v1`パッケージの場合、パッケージには次の`.go`ファイルが含まれています。
- types.go
- register.go
- doc.go
- generated.pb.go
- types_swagger_doc_generated.go
- zz_generated.deepcopy.go

ここではtypes.goとregister.goについて中身を見ます。

## types.go
types.goは各パッケージにおいて中心的な役割をするファイルとなっており、全てのKubernetesリソース（Kind）の構造体と関連するサブ構造体が定義されています。
`Deployment`リソースに対応する構造体も`k8s.io/api/app/v1`パッケージの`types.go`の中に定義されており、次のような構造体となっています。

```api/apps/v1/types.go
type Deployment struct {
	metav1.TypeMeta `json:",inline"`
	// Standard object's metadata.
	// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	// Specification of the desired behavior of the Deployment.
	// +optional
	Spec DeploymentSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`

	// Most recently observed status of the Deployment.
	// +optional
	Status DeploymentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}
```

`Deployment`構造体のフィールドのうち`Spec`, `Status`フィールドはサブ構造体に該当し、こちらも同じ`types.go`ファイル内で定義されています。
`metav1.TypeMeta`, `metav1.ObjectMeta`フィールドについては全てのKindに共通するフィールドとなっており、こちらは後述するapimachineryライブラリで定義されています。

## register.go
`register.go`ではパッケージに関連する`Group/Version`を定義し、`Group/Version`に含まれるKindのリストを定義しています。
リソースの`Group/Version`を特定する必要がある場合、`register.go`で定義される`SchemeGroupVersion`オブジェクトから取得することができます。

```api/apps/v1/register.go
import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

// GroupName is the group name use in this package
const GroupName = "apps"

// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}
```

またregister.goには`AddToScheme`関数も定義されており、`Group/Version`と`Kinds`をSchemeに追加する際に利用できます。
各Kubernetesリソースのバージョン・KindをSchemeに追加することにより、Kubernetes API上で定義された`Group-Version-Resource`をGoの構造体にマッピングすることができ、Go言語で扱いやすくなっています。

apiモジュールについては、PodやDeploymentといったKubernetes APIのデフォルトリソースの型を利用したい場合に参照するものと認識しておくと良さそうです。

# apimachinery
apimachineryはKubernetes APIオブジェクトの規則に従うAPIオブジェクトと共に利用する汎用的な機能を持ったモジュールです。  
apiやclient-goといったモジュールから参照されており、前述の`TypeMeta`・`ObjectMeta`や`Scheme`などすべてのKubernetesリソースのオブジェクトで共通して利用されるインターフェースや関数・型を提供しています。

apimachineryの中でもよく使われるのが`Schema`です。
`Schema`は抽象化のための仕組みであり、以下のように利用されます。

- Schemeによる抽象化
    - APIオブジェクトを`Group-Version-Kinds`として登録する
    - 異なるバージョンのAPIオブジェクト間で変換を行う
    - APIオブジェクトのシリアライズ・デシリアライズを行う

## Scheme
apimachineryでScheme構造体は以下のように定義されています。

```apimachinery/pkg/runtime/scheme.go
type Scheme struct {
	// gvkToType allows one to figure out the go type of an object with
	// the given version and name.
	gvkToType map[schema.GroupVersionKind]reflect.Type

	// typeToGVK allows one to find metadata for a given go object.
	// The reflect.Type we index by should *not* be a pointer.
	typeToGVK map[reflect.Type][]schema.GroupVersionKind
    ...省略
}

```

`gvkToType`・`typeToGVK`の名前と型定義からわかる通り、こちらを利用してGoの構造体から`Group-Version-Kind`の取得またその逆を行うことができます。

client-goの実装を追うと、実際にScheme構造体のオブジェクトを初期化している箇所が見られます。
`client-go/kubernetes/scheme/register.go`の中でKubernetes APIのデフォルトリソースについて、前節で述べた`AddToScheme`関数が呼ばれており、Schemeオブジェクトの初期化が行われていることがわかります。  
```client-go/kubernetes/schema/register.go
var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
	admissionregistrationv1.AddToScheme,
	admissionregistrationv1alpha1.AddToScheme,
	admissionregistrationv1beta1.AddToScheme,
	internalv1alpha1.AddToScheme,
	appsv1.AddToScheme,
	appsv1beta1.AddToScheme,
	appsv1beta2.AddToScheme,
	authenticationv1.AddToScheme,
	authenticationv1alpha1.AddToScheme,
	authenticationv1beta1.AddToScheme,
	authorizationv1.AddToScheme,
	authorizationv1beta1.AddToScheme,
	autoscalingv1.AddToScheme,
	autoscalingv2.AddToScheme,
	autoscalingv2beta1.AddToScheme,
	autoscalingv2beta2.AddToScheme,
	batchv1.AddToScheme,
	batchv1beta1.AddToScheme,
	certificatesv1.AddToScheme,
	certificatesv1beta1.AddToScheme,
	certificatesv1alpha1.AddToScheme,
	coordinationv1beta1.AddToScheme,
	coordinationv1.AddToScheme,
	corev1.AddToScheme,
	discoveryv1.AddToScheme,
	discoveryv1beta1.AddToScheme,
	eventsv1.AddToScheme,
	eventsv1beta1.AddToScheme,
	extensionsv1beta1.AddToScheme,
	flowcontrolv1.AddToScheme,
	flowcontrolv1beta1.AddToScheme,
	flowcontrolv1beta2.AddToScheme,
	flowcontrolv1beta3.AddToScheme,
	networkingv1.AddToScheme,
	networkingv1alpha1.AddToScheme,
	networkingv1beta1.AddToScheme,
	nodev1.AddToScheme,
	nodev1alpha1.AddToScheme,
	nodev1beta1.AddToScheme,
	policyv1.AddToScheme,
	policyv1beta1.AddToScheme,
	rbacv1.AddToScheme,
	rbacv1beta1.AddToScheme,
	rbacv1alpha1.AddToScheme,
	resourcev1alpha2.AddToScheme,
	schedulingv1alpha1.AddToScheme,
	schedulingv1beta1.AddToScheme,
	schedulingv1.AddToScheme,
	storagev1beta1.AddToScheme,
	storagev1.AddToScheme,
	storagev1alpha1.AddToScheme,
}

var AddToScheme = localSchemeBuilder.AddToScheme

func init() {
	v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
	utilruntime.Must(AddToScheme(Scheme))
}
```

こちらで初期化したSchemeオブジェクトを参照することで、client-goではGoのオブジェクトの型から`Group-Version-Kind`を取得することができ、Go言語でKubernetes APIへのリクエストの生成を行うことができています。  

余談ですが、client-goでの`Clientset`での説明時に述べた簡単な実装を元にclient-goのソースコードを追ってみると、client-goが`RESTClient`メソッドや`Scheme`を利用することで異なるKubernetesリソースのオブジェクトをほぼ同一の流れで処理できていることがわかります。

apimachineryについては、KubernetesリソースをGo言語で扱うための規則となるような汎用的なインターフェース・関数・型が用意されているモジュールと覚えておくと良さそうです。

# まとめ
本記事ではGo言語でKubernetes周りの開発を行う際によく利用される`client-go`・`api`・`apimachinery`についてそれぞれの機能と簡単な関係性について述べました。

特に`apimachinery`については、Go言語でKubernetesを触る際に準拠すべき項目や汎用的に利用できる関数など多く提供されているため、Kubernetes関連のOSSのソースコードを読むなどして使い方の詳細を覚えておくと良さそうかと思いました。

# 参考
- [Kubernetes Programming with Go](https://www.amazon.co.jp/Kubernetes-Programming-Go-Clients-Operators/dp/1484290259)
- [Programming Kubernetes: Developing Cloud-native Applications](https://www.amazon.co.jp/Programming-Kubernetes-Developing-Cloud-native-Applications/dp/1492047104)