クライアントの使い方
controller-runtimeでは、Kubernetes APIにアクセスするためのクライアントライブラリ(client.Client)を提供しています。
このクライアントは標準リソースとカスタムリソースを同じように扱うことができ、型安全で簡単に利用できます。
クライアントの作成
クライアントを作成するためにはまずSchemeを用意する必要があります。
SchemeはGoのstructとGroupVersionKindを相互に変換したり、異なるバージョン間でのSchemeの変換をおこなったりするための機能です。
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(viewv1.AddToScheme(scheme))
//+kubebuilder:scaffold:scheme
}
最初にruntime.NewScheme()
で新しいscheme
を作成します。
clientgoscheme.AddToScheme
では、PodやServiceなどKubernetesの標準リソースの型をschemeに追加しています。
viewv1.AddToScheme
では、MarkdownViewカスタムリソースの型をschemeに追加しています。
このSchemeを利用することで、標準リソースとMarkdownViewリソースを扱うことができるクライアントを作成できます。
つぎにGetConfigOrDieでクライアントの設定を取得しています。
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
})
client := mgr.GetClient()
GetConfigOrDie関数は、下記のいずれかの設定を読み込みます。
- コマンドラインオプションの
--kubeconfig
指定された設定ファイル - 環境変数
KUBECONFIG
で指定された設定ファイル - Kubernetesクラスター上でPodとして動いているのであれば、カスタムコントローラーが持つサービスアカウントの認証情報を利用
カスタムコントローラーは通常Kubernetesクラスター上で動いているので、サービスアカウントの認証情報が利用されます。
このSchemeとConfigを利用してManagerを作成し、GetClient()
でクライアントを取得できます。
ただし、ManagerのStart()
を呼び出す前にクライアントは利用できないので注意しましょう。
Get/List
クライアントを利用して、リソースを取得する方法を見ていきます。
Getの使い方
リソースを取得するには、下記のように第2引数で欲しいリソースのnamespaceとnameを指定します。 そして第3引数に指定した変数で結果を受け取ることができます。 なお、どの種類のリソースを取得するのかは、第3引数に渡した変数の型で自動的に判別されます。
func get(ctx context.Context, cli client.Client) error {
var deployment appsv1.Deployment
err := cli.Get(ctx, client.ObjectKey{Namespace: "default", Name: "sample"}, &deployment)
if err != nil {
return err
}
fmt.Printf("Got Deployment: %#v\n", deployment)
return nil
}
クライアントのキャッシュ機構
Kubernetes上ではいくつものコントローラーが動いており、そのコントローラーはそれぞれたくさんのリソースを扱っています。 これらのコントローラーが毎回APIサーバーにアクセスしてリソースの取得をおこなうと、APIサーバーやそのバックエンドにいるetcdの負荷が高まってしまうという問題があります。
そこで、controller-runtimeの提供するクライアントはキャッシュ機構を備えています。
このクライアントはGet()
やList()
でリソースを取得すると、同一namespace内の同じKindのリソースをすべて取得してインメモリにキャッシュします。
そして対象のリソースをWatchし、APIサーバー上でリソースの変更が発生した場合にキャッシュの更新をおこないます。
このようなキャッシュの仕組みにより、コントローラーからAPIサーバーへのアクセスを減らすことが可能になっています。
なお、このようなキャッシュ機構を備えているため、実装上はGetしか呼び出していなくても、リソースのアクセス権限としてはListやWatchが必要となります。
RBACマニフェストの生成で解説したように、リソースの取得をおこなう場合はget, list, watch
の権限を付与しておきましょう。
キャッシュの仕組みが必要ない場合は、ManagerのGetAPIReader()
を利用してキャッシュ機能のないクライアントを取得できます。
Listの使い方
Listでは条件を指定して複数のリソースを一度に取得できます。
下記の例では、LabelSelectorやNamespaceを指定してリソースの取得をおこなっています。 なお、Namespaceを指定しなかった場合は、全Namespaceのリソースを取得します。
func list(ctx context.Context, cli client.Client) error {
var pods corev1.PodList
err := cli.List(ctx, &pods, &client.ListOptions{
Namespace: "default",
LabelSelector: labels.SelectorFromSet(map[string]string{"app": "sample"}),
})
if err != nil {
return err
}
for _, pod := range pods.Items {
fmt.Println(pod.Name)
}
return nil
}
Limit
とContinue
を利用することで、ページネーションをおこなうことも可能です。
下記の例では1回のAPI呼び出しで3件ずつリソースを取得して表示しています。
func pagination(ctx context.Context, cli client.Client) error {
token := ""
for i := 0; ; i++ {
var pods corev1.PodList
err := cli.List(ctx, &pods, &client.ListOptions{
Limit: 3,
Continue: token,
})
if err != nil {
return err
}
fmt.Printf("Page %d:\n", i)
for _, pod := range pods.Items {
fmt.Println(pod.Name)
}
fmt.Println()
token = pods.ListMeta.Continue
if len(token) == 0 {
return nil
}
}
}
.ListMeta.Continue
にトークンが入っているを利用して、続きのリソースを取得できます。
トークンが空になるとすべてのリソースを取得したということになります。
Create/Update
リソースの作成はCreate()
、更新にはUpdate()
を利用します。
例えば、Deploymentリソースは以下のように作成できます。
func create(ctx context.Context, cli client.Client) error {
dep := appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "sample",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Replicas: pointer.Int32Ptr(1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "nginx"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": "nginx"},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx:latest",
},
},
},
},
},
}
err := cli.Create(ctx, &dep)
if err != nil {
return err
}
return nil
}
なお、リソースがすでに存在する状態でCreate()
を呼んだり、リソースが存在しない状態でUpdate()
を呼び出したりするとエラーになります。
CreateOrUpdate
Get()
でリソースを取得して、リソースが存在しなければCreate()
を呼び、存在すればUpdate()
を呼び出すという処理は頻出パターンです。
そこで、controller-runtimeにはCreateOrUpdate()
という便利な関数が用意されています。
func createOrUpdate(ctx context.Context, cli client.Client) error {
dep := &appsv1.Deployment{}
dep.SetNamespace("default")
dep.SetName("sample")
op, err := ctrl.CreateOrUpdate(ctx, cli, dep, func() error {
dep.Spec.Replicas = pointer.Int32Ptr(1)
dep.Spec.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "nginx"},
}
dep.Spec.Template = corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": "nginx"},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx:latest",
},
},
},
}
return nil
})
if err != nil {
return err
}
if op != controllerutil.OperationResultNone {
fmt.Printf("Deployment %s\n", op)
}
return nil
}
この関数の第3引数に渡すオブジェクトには、NameとNamespaceのみを指定します(ただしクラスターリソースの場合はNamespace不要)。
リソースが存在した場合、この第3引数で渡した変数に既存のリソースの値がセットされます。
その後、第4引数で渡した関数の中でそのdep
変数を書き換え、更新処理を実行します。
リソースが存在しない場合は、第4引数で渡した関数を実行した後、リソースの作成処理が実行されます。
Patch
Update()
やCreateOrUpdate()
による更新処理は、リソースを取得してから更新するまでの間に、他の誰かがリソースを書き換えてしまう可能性があります
(これをTOCTTOU: Time of check to time of useと呼びます)。
すでに書き換えられたリソースを更新しようとすると、以下のようなエラーが発生してしまいます。
Operation cannot be fulfilled on deployments.apps "sample": the object has been modified; please apply your changes to the latest version and try again
そこでPatch()
を利用すると、競合することなく変更したいフィールドの値だけを更新できます。
Patchにはclient.MergeFrom
やclient.StrategicMergeFrom
を利用する方法と, Server-Side Applyを利用する方法があります。
client.MergeFrom
とclient.StrategicMergeFrom
の違いは、リスト要素の更新方法です。
client.MergeFrom
でリストを更新すると指定した要素で上書きされますが、client.StrategicMergeFrom
ではリストはpatchStrategyに応じて
要素が追加されたり更新されたりします。
client.MergeFrom
を利用してDeploymentのレプリカ数のみを更新する例を以下に示します。
func patchMerge(ctx context.Context, cli client.Client) error {
var dep appsv1.Deployment
err := cli.Get(ctx, client.ObjectKey{Namespace: "default", Name: "sample"}, &dep)
if err != nil {
return err
}
newDep := dep.DeepCopy()
newDep.Spec.Replicas = pointer.Int32Ptr(3)
patch := client.MergeFrom(&dep)
err = cli.Patch(ctx, newDep, patch)
return err
}
一方のServer-Side ApplyはKubernetes v1.14で導入されたリソースの更新方法です。
リソースの各フィールドを更新したコンポーネントを.metadata.managedFields
で管理することで、
サーバーサイドでリソース更新の衝突を検出できます。
Server-Side Applyでは、以下のようにUnstructured型のパッチを用意してリソースの更新をおこないます。
なお、公式ドキュメントに記述されているように、 カスタムコントローラでServer-Side Applyをおこなう際には、常にForceオプションを有効にすることが推奨されています。
func patchApply(ctx context.Context, cli client.Client) error {
patch := &unstructured.Unstructured{}
patch.SetGroupVersionKind(schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
})
patch.SetNamespace("default")
patch.SetName("sample2")
patch.UnstructuredContent()["spec"] = map[string]interface{}{
"replicas": 2,
"selector": map[string]interface{}{
"matchLabels": map[string]string{
"app": "nginx",
},
},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]string{
"app": "nginx",
},
},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:latest",
},
},
},
},
}
err := cli.Patch(ctx, patch, client.Apply, &client.PatchOptions{
FieldManager: "client-sample",
Force: pointer.Bool(true),
})
return err
}
上記のようにServer-Side ApplyはUnstructured型を利用するため、型安全なコードが記述できませんでした。
Kubernetes v1.21ではApplyConfigurationが導入され、以下のように型安全なServer-Side Applyのコードが書けるようになりました。
func patchApplyConfig(ctx context.Context, cli client.Client) error {
dep := appsv1apply.Deployment("sample3", "default").
WithSpec(appsv1apply.DeploymentSpec().
WithReplicas(3).
WithSelector(metav1apply.LabelSelector().WithMatchLabels(map[string]string{"app": "nginx"})).
WithTemplate(corev1apply.PodTemplateSpec().
WithLabels(map[string]string{"app": "nginx"}).
WithSpec(corev1apply.PodSpec().
WithContainers(corev1apply.Container().
WithName("nginx").
WithImage("nginx:latest"),
),
),
),
)
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep)
if err != nil {
return err
}
patch := &unstructured.Unstructured{
Object: obj,
}
var current appsv1.Deployment
err = cli.Get(ctx, client.ObjectKey{Namespace: "default", Name: "sample3"}, ¤t)
if err != nil && !errors.IsNotFound(err) {
return err
}
currApplyConfig, err := appsv1apply.ExtractDeployment(¤t, "client-sample")
if err != nil {
return err
}
if equality.Semantic.DeepEqual(dep, currApplyConfig) {
return nil
}
err = cli.Patch(ctx, patch, client.Apply, &client.PatchOptions{
FieldManager: "client-sample",
Force: pointer.Bool(true),
})
return err
}
Status.Update/Patch
Statusをサブリソース化している場合、これまで紹介したUpdate()
やPatch()
を利用してもステータスを更新できません。
Status更新用のクライアントを利用することになります。
Status().Update()
とStatus().Patch()
は、メインリソースのUpdate()
、Patch()
と使い方は同じです。
以下のようにstatusフィールドを変更し、Status().Update()
を呼び出します。
(このコードはあくまでもサンプルです。Deploymentリソースのステータスを勝手に書き換えるべきではありません。)
func updateStatus(ctx context.Context, cli client.Client) error {
var dep appsv1.Deployment
err := cli.Get(ctx, client.ObjectKey{Namespace: "default", Name: "sample"}, &dep)
if err != nil {
return err
}
dep.Status.AvailableReplicas = 3
err = cli.Status().Update(ctx, &dep)
return err
}
なお、現状ではカスタムリソースのStatusサブリソースはServer-Side Applyをサポートしていません。
Delete/DeleteAllOf
最後にリソースを削除するDelete
とDeleteAllOf
を見てみましょう。
Delete
とDeleteAllOf
にはPreconditions
という特殊なオプションがあります。
以下のコードはPreconditions
オプションを利用した例です。
func deleteWithPreConditions(ctx context.Context, cli client.Client) error {
var deploy appsv1.Deployment
err := cli.Get(ctx, client.ObjectKey{Namespace: "default", Name: "sample"}, &deploy)
if err != nil {
return err
}
uid := deploy.GetUID()
resourceVersion := deploy.GetResourceVersion()
cond := metav1.Preconditions{
UID: &uid,
ResourceVersion: &resourceVersion,
}
err = cli.Delete(ctx, &deploy, &client.DeleteOptions{
Preconditions: &cond,
})
return err
}
リソースを削除する際、リソース取得してから削除のリクエストを投げるまでの間に、同じ名前の別のリソースが作り直される場合があります。
そのようなケースでは、NameとNamespaceのみを指定してDeleteを呼び出した場合、誤って新しく作成されたリソースを削除される可能性があります。
そこでこの例では再作成したリソースを間違って消してしまわないように、Preconditions
オプションを利用してUIDとResourceVersionが一致するリソースを削除しています。
DeleteAllOf
は、以下のように指定した種類のリソースをまとめて削除できます。
func deleteAllOfDeployment(ctx context.Context, cli client.Client) error {
err := cli.DeleteAllOf(ctx, &appsv1.Deployment{}, client.InNamespace("default"))
return err
}
なお、ServiceリソースなどDeleteAllOf
が利用できないリソースもあるので注意しましょう。