🤩

KubernetesでServer Side ApplyするときにClientで差分計算したい

3 min read

前回の記事では、Kubernetes v1.21で導入された、型安全なServer Side Applyの利用方法を紹介しました。

しかし前回の実装では、リソースを変更する必要がない場合でも必ずPatch関数を呼び出していたため、無駄なAPI呼び出しが発生していました。

そこで今回は、Server Side Applyを利用するときに、クライアント側(コントローラ側)で差分のチェックをおこなう方法について紹介します。

さて、Server Side Applyなのにクライアント側で差分検出をおこなうとはどういうことか?と思われるかもしれません。
本来Server Side Applyではサーバー側で差分をチェックしてリソースの更新をおこなうものだからです。

本記事で紹介するのは、カスタムコントローラの実装において、コントローラが前回適用した値と今回適用したい値の間に差分があったときに、リソースの更新処理をおこなうための実装方法です。
つまり同じFieldManagerが更新したフィールドのみに着目して差分を算出する必要があります。

ApplyConfigurationには、特定のFieldManagerが更新したフィールドのみを抽出するExtract関数が用意されています。
この関数はリソースの種類ごとに用意されていて、Deploymentであれば、ExtractDeploymentという関数を利用します。(なお、これらはExperimentalな関数なので、今後仕様が変わる可能性もあります。注意してご利用ください。)

https://pkg.go.dev/k8s.io/client-go@v0.21.2/applyconfigurations/apps/v1#ExtractDeployment

また、オブジェクトの比較にはequality.Semantic.DeepEqual関数を利用します。
この関数を利用すると、ポインタ型のフィールドであっても型が一致していれば値での比較がおこなわれます。

以下がコード例となります。(実コードはこちら

func (r *MyAppReconciler) reconcileDeploymentBySSAWithClientComparison(ctx context.Context, myapp *samplev1.MyApp) error {
	fieldManager := "myapp-operator"

	// 作成したいリソースを用意する
	expectedDeployment := 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"})).
			WithTemplate(corev1apply.PodTemplateSpec().
				WithLabels(map[string]string{"component": "nginx"}).
				WithSpec(corev1apply.PodSpec().
					WithContainers(corev1apply.Container().
						WithName("nginx").
						WithImage("nginx:latest")))))
	err := setControllerReference(myapp, expectedDeployment, r.Scheme)
	if err != nil {
		return err
	}

	// Server Side Apply用にパッチを作成する
	obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(expectedDeployment)
	if err != nil {
		return err
	}
	patch := &unstructured.Unstructured{
		Object: obj,
	}

	// 既存のリソースを取得する
	var orig appsv1.Deployment
	err = r.Get(ctx, client.ObjectKey{Namespace: myapp.Namespace, Name: myapp.Name + "-nginx"}, &orig)
	if err != nil && !errors.IsNotFound(err) {
		return err
	}

	// 既存のリソースから、このコントローラが変更したフィールドのみを抽出する
	origApplyConfig, err := appsv1apply.ExtractDeployment(&orig, fieldManager)
	if err != nil {
		return err
	}

	// 作成したいリソースと既存のリソースの間に差がなければ、何もせずに終了する
	if equality.Semantic.DeepEqual(expectedDeployment, origApplyConfig) {
		return nil
	}

	// 差分があった場合だけServer Side Applyでパッチを適用する
	return r.Patch(ctx, patch, client.Apply, &client.PatchOptions{
		FieldManager: fieldManager,
		Force: pointer.Bool(true),
	})
}

このように実装すれば、非常に簡潔に効率的なReconcile処理を書くことができます。
これが今後のカスタムコントローラ実装の決定版になるのではないかと予想します。