クライアントの使い方

controller-runtimeでは、Kubernetes APIにアクセスするためのクライアントライブラリ(client.Client)を提供しています。

このクライアントは標準リソースとカスタムリソースを同じように扱うことができ、型安全で簡単に利用できます。

クライアントの作成

クライアントを作成するためにはまずSchemeを用意する必要があります。

SchemeはGoのstructとGroupVersionKindを相互に変換したり、異なるバージョン間でのSchemeの変換をおこなったりするための機能です。

main.go

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引数に渡した変数の型で自動的に判別されます。

main.go

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サーバー上でリソースの変更が発生した場合にキャッシュの更新をおこないます。

cache

このようなキャッシュの仕組みにより、コントローラーからAPIサーバーへのアクセスを減らすことが可能になっています。

なお、このようなキャッシュ機構を備えているため、実装上はGetしか呼び出していなくても、リソースのアクセス権限としてはListやWatchが必要となります。 RBACマニフェストの生成で解説したように、リソースの取得をおこなう場合はget, list, watchの権限を付与しておきましょう。

キャッシュの仕組みが必要ない場合は、ManagerのGetAPIReader()を利用してキャッシュ機能のないクライアントを取得できます。

Listの使い方

Listでは条件を指定して複数のリソースを一度に取得できます。

下記の例では、LabelSelectorやNamespaceを指定してリソースの取得をおこなっています。 なお、Namespaceを指定しなかった場合は、全Namespaceのリソースを取得します。

main.go

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
}

LimitContinueを利用することで、ページネーションをおこなうことも可能です。 下記の例では1回のAPI呼び出しで3件ずつリソースを取得して表示しています。

main.go

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リソースは以下のように作成できます。

main.go

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()という便利な関数が用意されています。

main.go

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.MergeFromclient.StrategicMergeFromを利用する方法と, Server-Side Applyを利用する方法があります。

client.MergeFromclient.StrategicMergeFromの違いは、リスト要素の更新方法です。 client.MergeFromでリストを更新すると指定した要素で上書きされますが、client.StrategicMergeFromではリストはpatchStrategyに応じて 要素が追加されたり更新されたりします。

client.MergeFromを利用してDeploymentのレプリカ数のみを更新する例を以下に示します。

main.go

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オプションを有効にすることが推奨されています。

main.go

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のコードが書けるようになりました。

main.go

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"}, &current)
    if err != nil && !errors.IsNotFound(err) {
        return err
    }

    currApplyConfig, err := appsv1apply.ExtractDeployment(&current, "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リソースのステータスを勝手に書き換えるべきではありません。)

main.go

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

最後にリソースを削除するDeleteDeleteAllOfを見てみましょう。

DeleteDeleteAllOfにはPreconditionsという特殊なオプションがあります。 以下のコードはPreconditionsオプションを利用した例です。

main.go

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は、以下のように指定した種類のリソースをまとめて削除できます。

main.go

func deleteAllOfDeployment(ctx context.Context, cli client.Client) error {
    err := cli.DeleteAllOf(ctx, &appsv1.Deployment{}, client.InNamespace("default"))
    return err
}

なお、ServiceリソースなどDeleteAllOfが利用できないリソースもあるので注意しましょう。

results matching ""

    No results matching ""