SWEet

A Software Engineer Is Eating Technologies

Custom Controller の開発で使えそうなコード集 #Kubernetes2 Advent Calendar 2020 16日目

この記事は Kubernetes2 Advent Calendar 2020qiita.com の16日目の記事です。

Custom Controller で よく使う、使えそう なコードの断片をラフに紹介してみようと思います。

想定する環境

  • Kubebuilder v2
  • go 1.14

今回実装した CRD の example

apiVersion: nginx.k8s-2020.advent.calendar/v1
kind: Nginx
metadata:
  name: nginx-sample
spec:
  name: nginx-1
  replices: 1

Custom Controller のロジック

  • nginxリソースが作成されると nginx の Deployment と Service を作成
  • nginxリソースが削除されると nginx の Deployment と Service を削除

サンプルなので非常に簡単なものになっています。

以降のコードではこちらの CR をアプライ、Controller が処理する前提で話を進めます。

Delete Reconcile の開始

if !nginx.DeletionTimestamp.IsZero() {
    return r.reconcileDelete(ctx, logger, nginx)
}

Reconcile する Object の Timestamp を判定して削除処理に分岐させるコード。 reconcileDelete 関数は各自で実装しましょう。

NotFound を判別するコード

import (
        apierrors "k8s.io/apimachinery/pkg/api/errors"
)
    err := r.Get(ctx, req.NamespacedName, nginx)
    if err != nil {
        if apierrors.IsNotFound(err) {
            return reconcile.Result{}, nil
        }
        return reconcile.Result{}, err
    }

ok みたいな別の値で存在可否を返さないこともあるのでこういう書き方でチェックできます。

Patch Helper による Patch

    patchHelper, err := patch.NewHelper(nginx, r)
    if err != nil {
        return ctrl.Result{}, err
    }

    defer func() {
        if err := patchHelper.Patch(ctx, nginx); err != nil {
            if reterr == nil {
                reterr = errors.Wrapf(err, "error patching Nginx %s/%s", nginx.Namespace, nginx.Name)
            }
        }
    }()

sigs.k8s.io/cluster-api/util/patch パッケージを使用して、Reconcile 内で副作用的にリソースを書き換えた場合、必ず Patch 処理で更新を行うコード。 Before な Object のパラメータを Controller の内部で直接書き換えるのはどうなんだろうと思いますが、後述する Finalizer の実装などと相性が良いです。

Event Recorder で 処理内容を Event Resource として保存

Event Resource を用いることで、Controller の処理結果を kubectl get events で確認できるようにもなります。 まず、Reconciler に record.EventRecorder フィールドを追加しましょう。

import (
    ...
    "k8s.io/client-go/tools/record"
)


// NginxReconciler reconciles a Nginx object
type NginxReconciler struct {
    client.Client
    Log      logr.Logger
    Scheme   *runtime.Scheme
    Recorder record.EventRecorder
}

Reconciler の初期化で Recorder の方も初期化します。

   if err = (&controllers.NginxReconciler{
        Client:   mgr.GetClient(),
        Log:      ctrl.Log.WithName("controllers").WithName("Nginx"),
        Scheme:   mgr.GetScheme(),
        Recorder: mgr.GetEventRecorderFor("nginx-controller"),
    }).SetupWithManager(mgr); err != nil {
        setupLog.Error(err, "unable to create controller", "controller", "Nginx")
        os.Exit(1)
    }

Event を記録する場合は次のようにします

r.Recorder.Eventf(nginx, corev1.EventTypeNormal, "Reconcile", "Reconcile Nginx resource %q", nginx.Name)

EventType は他にもあるので用途に応じて使い分けましょう。

Controller のログにも同時に出力されるようになります

2020-12-15T20:59:28.936+0900 DEBUG   controller-runtime.manager.events   Normal  {"object": {"kind":"Nginx","namespace":"default","name":"nginx-sample","uid":"7787eec9-ef85-4d56-ad75-dd4807b077d0","apiVersion":"nginx.k8s-2020.advent.calendar/v1","resourceVersion":"21597"}, "reason": "Reconcile", "message": "Reconcile Nginx resource \"nginx-sample\""}

kubectl get events でも確認できます

2m8s        Normal    Reconcile                 nginx/nginx-sample                                           Reconcile Nginx resource "nginx-sample"

Finalizer の付与と削除

Finalizer はざっくり説明すると、その Object が削除される前に行う処理を定義するものです。 CR に従属する別のリソースを CR が削除される前に削除するというのを保証するために使われます。

Custom Controller ではこの Finalizer キーを Object に付与、削除する処理を書くことでこれらを実現することができます。

Finalizer の付与

import (
    "context"


    "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    ...
)
    ...
    controllerutil.AddFinalizer(nginx, nginxv1.NginxFinalizer)

kubebuidler books には Object の metadata フィールドにキーを append するように書いていますが、こちらの方が楽に書けます。 先ほどの patchHelper を併用することで、キーを付与後に行う更新忘れもなくなります。

Finalizer の削除

CR の削除の前処理が終わったら最後は Finalizer を削除しましょう。 こちらの RemoveFinalizerdefer で呼ばないように注意しましょう。他の削除ロジックでエラーを返した場合も実行されて、意図せず CR が削除されてしまいます。 そうなった場合、従属する Object を kubectl で手で削除していかなくてはいけません。

func (r *NginxReconciler) reconcileDelete(ctx context.Context, logger logr.Logger, nginx *nginxv1.Nginx) (ctrl.Result, error) {
     // Delete some objects

    controllerutil.RemoveFinalizer(nginx, nginxv1.NginxFinalizer)
    return ctrl.Result{}, nil
}

OwnerRefrence 周りのTips

書こう思ったらすでに某書籍でしっかり書かれていたっぽいのでリンクだけ貼っておきます

github.com

ObjectReference で外部リソースへの依存を表現

import (corev1 "k8s.io/api/core/v1")

type NginxSpec struct {
     HogeRef *corev1.ObjectReference
}

これによって CR のフィールドに他リソースへの参照を持たせることができます。

apiVersion: nginx.k8s-2020.advent.calendar/v1
kind: Nginx
metadata:
  name: nginx-sample
spec:
  name: nginx-1
  replices: 1
  hogeRef:
    apiVersion: hoge.io
    kind: Hoge
    name: hogefuga

名前だけでも良い場合もあると思いますが、kindapiVersion によってコードによる参照がしやすくなります。 具体的には以下のような感じ

import (
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

if nginx.Spec.HogeRef != nil {
    obj, err := GetExternalResource(ctx, r.Client, nginx.Spec.HogeRef, nginx.Namespace)
    
    // do something
}

func GetExternalResource(ctx context.Context, c client.Client, ref *corev1.ObjectReference, namespace string) (*unstructured.Unstructured, error) {
    obj := new(unstructured.Unstructured)
    obj.SetAPIVersion(ref.APIVersion)
    obj.SetKind(ref.Kind)
    obj.SetName(ref.Name)
    key := client.ObjectKey{Name: obj.GetName(), Namespace: namespace}
    if err := c.Get(ctx, key, obj); err != nil {
        return nil, errors.Wrapf(err, "failed to retrieve %s external object %q/%q", obj.GetKind(), key.Namespace, key.Name)
    }
    return obj, nil
}

注意しなくてはいけないのは、obj*Unstructured で返してるので、Goの構造体としてフィールドにメンバ変数を介してアクセスしたい場合はそれぞれの Object にマッピングする必要があります。 その場合は runtime パッケージの FromUnstructured を使うと楽にできます

import (
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

obj := &unstructured.Unstructured{}
out = &nginxv1.Hoge{}
runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &hoge)

番外編: PodTemplateSpec から Hashを作成する関数

Deployment から作成された Pod って <deployment name>-<PodTemplateSpec Hash>-<よくわからないハッシュ値っぽいやつ> という命名規則っぽいです(ドキュメントとかに書いてあるのかわからなかったので推測です この Spec からハッシュ値を作成するコードを下に書いておきます。

func DeepHashObject(hasher hash.Hash, objectToWrite interface{}) {
    hasher.Reset()
    printer := spew.ConfigState{
        Indent:         " ",
        SortKeys:       true,
        DisableMethods: true,
        SpewKeys:       true,
    }
    printer.Fprintf(hasher, "%#v", objectToWrite)
}

// DeepHashObjectToString creates a unique hash string from a go object.
func DeepHashObjectToString(objectToWrite interface{}) string {
    hasher := md5.New()
    hash.DeepHashObject(hasher, objectToWrite)
    return hex.EncodeToString(hasher.Sum(nil)[0:])
}

この DeepHashObject 関数を用いて上位8byte を抽出して replicaset の名前にしたものが下記のコードになります

func ComputeHash(template *v1.PodTemplateSpec, collisionCount *int32) string {
         podTemplateSpecHasher := fnv.New32a()
    hashutil.DeepHashObject(podTemplateSpecHasher, *template)

    // Add collisionCount in the hash if it exists.
    if collisionCount != nil {
        collisionCountBytes := make([]byte, 8)
        binary.LittleEndian.PutUint32(collisionCountBytes, uint32(*collisionCount))
        podTemplateSpecHasher.Write(collisionCountBytes)
    }

    return rand.SafeEncodeString(fmt.Sprint(podTemplateSpecHasher.Sum32()))
}

コード自体は k8s.io/kubernetes/pkg/util/hash とかにあるんですが、go mod の依存関係のせいか、直接は import できないようです。

まとめ

いくつか Kubernetes の Custom Controller 開発で使えそうなコードを紹介させていただきました。 他の書籍や記事と被っているかもしれません。

参考

GitHub - jetstack/kubebuilder-sample-controller: k8s.io/sample-controller written with kubebuilder v2