😊

Kubernetes v1.21で導入される型安全なServer Side Apply

2021/04/02に公開

2021/07/16追記: Server Side ApplyするときにClientで差分検出して無駄なリクエストを発生させない方法を書きました。こちらの記事もご覧ください。


前回の記事では、Reconcile処理において無駄な更新処理を発生させないさまざまな実装方法を紹介しました。
その中で、Server Side Apply方式では更新に必要のないフィールドがリクエストに含まれてしまうために、それを取り除く処理を書く必要があり面倒だと書きました。

まもなくリリースされるKubernetes 1.21では、Server Side Applyのこの問題を解決する機能が導入されるようです。

この新機能では各リソース用にApplyやApplyStatusなどのメソッドが追加されます。
例えばDeployment用のApplyメソッドは以下のような定義です。

func (c *deployments) Apply(ctx context.Context, deployment *appsv1.DeploymentApplyConfiguration, opts metav1.ApplyOptions) (result *v1.Deployment, err error)

ここで特徴的なのが、引数としてk8s.io/api/apps/v1Deploymentではなく、k8s.io/client-go/applyconfigurations/apps/v1DeploymentApplyConfigurationを利用していることです。

この構造体の定義を見てみると、すべてのフィールドがポインタになっていて、omitemptyタグがついていることが分かります。

type DeploymentSpecApplyConfiguration struct {
	Replicas                *int32                                    `json:"replicas,omitempty"`
	Selector                *v1.LabelSelectorApplyConfiguration       `json:"selector,omitempty"`
	Template                *corev1.PodTemplateSpecApplyConfiguration `json:"template,omitempty"`
	Strategy                *DeploymentStrategyApplyConfiguration     `json:"strategy,omitempty"`
	MinReadySeconds         *int32                                    `json:"minReadySeconds,omitempty"`
	RevisionHistoryLimit    *int32                                    `json:"revisionHistoryLimit,omitempty"`
	Paused                  *bool                                     `json:"paused,omitempty"`
	ProgressDeadlineSeconds *int32                                    `json:"progressDeadlineSeconds,omitempty"`
}

この特徴により、クライアント側で指定しなかったフィールドはnilとなるため、Applyしたときに不要なフィールドの更新が発生しません。

では前回の記事で紹介したReconcile処理を、ApplyConfigurationを使って書き直してみます。

コードはこちらに置いてあります。

func (r *MyAppReconciler) reconcileDeploymentByTypesafeSSA(ctx context.Context, myapp *samplev1.MyApp) error {
	dep := appsv1apply.Deployment(myapp.Name+"-nginx", myapp.Namespace).
		WithLabels(map[string]string{"component": "nginx"}).
		WithSpec(appsv1apply.DeploymentSpec().
			WithReplicas(1).
			WithSelector(metav1apply.LabelSelector().WithMatchLabels(map[string]string{"component": "nginx"})))

	var podTemplate *corev1apply.PodTemplateSpecApplyConfiguration
	if myapp.Spec.PodTemplate != nil {
		podTemplate = myapp.Spec.PodTemplate.Template
	} else {
		podTemplate = corev1apply.PodTemplateSpec()
	}
	podTemplate.WithLabels(map[string]string{"component": "nginx"})

	if podTemplate.Spec == nil {
		podTemplate.WithSpec(corev1apply.PodSpec())
	}
	hasNginxContainer := false
	for _, c := range podTemplate.Spec.Containers {
		if *c.Name == "nginx" {
			hasNginxContainer = true
		}
	}
	if !hasNginxContainer {
		podTemplate.Spec.WithContainers(
			corev1apply.Container().WithName("nginx").WithImage("nginx:latest"))
	}
	dep.Spec.WithTemplate(podTemplate)

	err := setControllerReference(myapp, dep, r.Scheme)
	if err != nil {
		return err
	}

	obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep)
	if err != nil {
		return err
	}
	patch := &unstructured.Unstructured{
		Object: obj,
	}

	return r.Patch(ctx, patch, client.Apply, &client.PatchOptions{
		FieldManager: "myapp-operator",
		Force: pointer.Bool(true),
	})
}

他の方式と比べてもすっきりしていていい感じです。
ただし、kube-apiserverに毎回リクエストを投げる必要があるという点には注意が必要です。

CRDの定義も、以下のように従来のstructではなくApplyConfigurationを利用しています。

 import (
-       corev1 "k8s.io/api/core/v1"
+       corev1apply "k8s.io/client-go/applyconfigurations/core/v1"
 )

 type MyAppSpec struct {
-       PodTemplate *corev1.PodTemplate `json:"podTemplate"`
+       PodTemplate *corev1apply.PodTemplateApplyConfiguration `json:"podTemplate"`
 }

なお、Kubernetes 1.21はリリース前なので現在のcontroller-runtime(v0.8.3)はこの新方式に正式には対応していません。
今回のコードは無理矢理動くように、自動生成されたDeepCopy関数を書き換えたりしています。
リリースを楽しみに待ちましょう。

Discussion