これは ZOZO Advent Calendar 2023 カレンダー Vol.5 の 1日目の記事です。
はじめに
KubernetesはGo言語で開発されており、開発に利用されるモジュールとしてclient-goが利用されることは割とよく知られているかと思います。
またclient-goを触ると必ず登場するのがapi
・apimachinery
というモジュールです。
自分がはじめてGo言語でKubernetes APIを操作するプログラムを書いた際には、これらのモジュールの機能・関係性がいまいちよく理解できずにいました。
本記事ではGo言語を使ってKubernetes APIを操作するプログラムを書く際に利用するclient-go
・apimachinery
・api
モジュールについてその機能と関係性について調べたことをまとめました。
client-go
client-goはGo言語でkubeapi-serverへリクエストを送る際のクライアントとして機能します。
前提として、Kubernetesには複数のコントロールプレーンコンポーネントがあり、そのうちの1つとしてkubeapi-serverが提供されています。
kubeapi-serverはクラスタの内部・外部に対してHTTP APIを公開しており、こちらをクエリすることにより、PodやNodeといったKubernetesリソースのオブジェクトを操作することができます。
例えば、クラスタ外部からKubernetesオブジェクトの操作を行う場合、kubectl CLIを利用することが一般的かと思います。こちらのCLIもGo言語で開発されており、client-goが利用されています。
kubectlのFactoryインターフェースのRESTClientメソッドではclient-goのRESTClient構造体のポインタを返すようになっており、getコマンドの実装を追ってみるとRESTClientメソッドでクライアントを生成し、作成したクライアントを利用してHTTP GETリクエストを呼んでいることがわかります。
client-goの中でもよく利用するのはClientset構造体かと思います。
次節ではClientset構造体について説明します。
Clientset
Clientset
の場合、ただ単一のClient
ではなくClient + set
となっているところが重要な点かと思っています。
client-goを利用する際、まず次のようなコードでclientsetを取得するかと思います。
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
構造体について深掘ります。
こちらの構造体は次のインターフェースを満たす実装を持っています。
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()
メソッドはこちらのインターフェースを満たす実装を持つオブジェクトを返します。
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()
メソッドの戻り値は次のインターフェースを満たします。
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
の中に定義されており、次のような構造体となっています。
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
オブジェクトから取得することができます。
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オブジェクトのシリアライズ・デシリアライズを行う
- APIオブジェクトを
Scheme
apimachineryでScheme構造体は以下のように定義されています。
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オブジェクトの初期化が行われていることがわかります。
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のソースコードを読むなどして使い方の詳細を覚えておくと良さそうかと思いました。