⚖️

Kubernetes のカスタムコントローラーを HPA に対応させる

2022/02/23に公開

本記事では、Kubernetes のカスタムコントローラーが作成した Deployment の Pod を HPA (Horizontal Pod Autoscaler) に対応させる方法について紹介したいと思います。

HPA に対応していないカスタムコントローラの問題点

まずは、カスタムコントローラーが HPA に対応していない場合、どのような問題があるのかみてみましょう。

この図のように、カスタムコントローラーが replicas を 2 で Deployment を作成したとします。
一方で HPA が、この Deployment の replicas を 3 に増やしたとします。

この場合、カスタムコントローラーは replicas を 3 に、 HPA コントローラーは 2 に収束させようとするため、replicas は 2 と 3 をいったりきたりすることになります。
これではオートスケールがうまく動きません。

対応方法

カスタムコントローラーを HPA に対応させる方法を 3 つ紹介します。

カスタムリソース自体を HPA に対応させる

まずは、カスタムリソース自体を HPA に対応させる方法です。
すなわち Deployment ではなく、カスタムリソースを HPA の対象とします。

これにより、 HPA コントローラーはカスタムリソースの replicas を変更し、その値に応じてカスタムコントローラーが Deployment の replicas を更新することになります。
これにより replicas の競合が発生することがなくなります。

カスタムリソースを HPA に対応させる方法は、以下の記事が詳しいので参考にしてみてください。

https://medium.com/@thescott111/autoscaling-kubernetes-custom-resource-using-the-hpa-957d00bb7993

カスタムコントローラーで HPA リソースを管理する

つぎに、カスタムコントローラーで HPA リソースを管理する方法です。

カスタムコントローラーが HPA リソースを作成し、replicas の変更は HPA コントローラーに任せ、カスタムコントローラーでは replicas の更新をおこなわないようにします。

Argo CD Operator はこの方式で実装されています。
以下のように、カスタムリソースに autoscale.enabled: true を指定すると、Argo CD Operator が HPA リソースを作成し、Deployment の replicas を直接編集しなくなります。

apiVersion: argoproj.io/v1alpha1
kind: ArgoCD
metadata:
  name: example-argocd
  labels:
    example: server
spec:
  server:
    autoscale:
      enabled: true
      hpa:
        maxReplicas: 3
        minReplicas: 1
        scaleTargetRef:
          apiVersion: extensions/v1beta1
          kind: Deployment
          name: example-argocd-server
        targetCPUUtilizationPercentage: 50

Server-Side Apply で衝突を検出する

最後は、Server-Side Apply で衝突をする方法です。
カスタムコントローラーと HPA コントローラーがともに Deployment の replicas を更新するのですが、カスタムコントローラー側ではコンフリクトを検出したら更新をやめるようにします。

Kubernetes 1.21 から HPA は Server-Side Apply に対応しました。

https://github.com/kubernetes/kubernetes/issues/94585

そのため、HPA コントローラが replicas を変更した場合、Deploment リソースに以下のような managedFields が設定されます。

managedFields:
- apiVersion: apps/v1
  fieldsType: FieldsV1
  fieldsV1:
    f:spec:
      f:replicas: {}
  manager: kube-controller-manager
  operation: Update
  subresource: scale

これにより、カスタムコントローラー側で replicas を更新する際に、すでに HPA によって replicas が更新済みだった場合はコンフリクトを検出することが可能になりました。

では、controller-runtime を利用して、 replicas の更新時にコンフリクトを検出する処理を実装してみましょう。

import (
	"context"

	appsv1 "k8s.io/api/apps/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	appsv1ac "k8s.io/client-go/applyconfigurations/apps/v1"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

func (r *MyAppReconciler) reconcileReplicas(ctx context.Context, name, namespace string, replicas int32) error {
	logger := log.FromContext(ctx)

	// 現在の Deployment リソースを取得する
	var current appsv1.Deployment
	err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &current)
	if err != nil && !apierrors.IsNotFound(err) {
		return err
	}

	// 現在の replicas が期待する値と一致する場合は何もしない
	if current.Spec.Replicas != nil && *current.Spec.Replicas == replicas {
		return nil
	}

	// replicas を変更するためのパッチを作成する
	dep := appsv1ac.Deployment(name, namespace).WithSpec(appsv1ac.DeploymentSpec().WithReplicas(replicas))
	obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep)
	if err != nil {
		return err
	}
	patch := &unstructured.Unstructured{
		Object: obj,
	}

	// Server-Side Apply で replicas を更新する
	err = r.Patch(ctx, patch, client.Apply, &client.PatchOptions{
		FieldManager: "my-custom-controller/replicas", // Deployment を作成する際の値と異なる名前にすること
		Force:        pointer.Bool(false),             // Conflict 時に更新しないように false を指定する
	})

	// Conflict した場合は何もせず正常終了
	if status, ok := apierrors.StatusCause(err, metav1.CauseTypeFieldManagerConflict); ok {
		logger.Info("conflict", "status", status)
		return nil
	}

	if err != nil {
		return err
	}
	return nil
}

なお、カスタムコントローラーが SSA で replicas を設定した後に、HPA コントローラーが replicas を更新した場合は、値が上書きされ、カスタムコントローラーの managedFields は削除されるようです。

以上の実装で、HPA コントローラーと更新処理が衝突することはなくなるでしょう。

まとめ

今回はカスタムコントローラーで作成した Deployment の Pod を HPA に対応させる 3 つの方法を紹介しました。
この中では、カスタムリソース自体を HPA に対応させるのがもっとも素直な方法でしょう。

一方、Server-Side Apply で衝突を検出する方法は HPA 対応以外の場面でも役立つテクニックなので、ぜひ活用してみてください。

Discussion