Kubernetes のカスタムコントローラーを HPA に対応させる
本記事では、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 に対応させる方法は、以下の記事が詳しいので参考にしてみてください。
カスタムコントローラーで 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 に対応しました。
そのため、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}, ¤t)
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