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 を削除しましょう。
こちらの RemoveFinalizer
は defer
で呼ばないように注意しましょう。他の削除ロジックでエラーを返した場合も実行されて、意図せず 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
書こう思ったらすでに某書籍でしっかり書かれていたっぽいのでリンクだけ貼っておきます
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
名前だけでも良い場合もあると思いますが、kind
、apiVersion
によってコードによる参照がしやすくなります。
具体的には以下のような感じ
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 開発で使えそうなコードを紹介させていただきました。 他の書籍や記事と被っているかもしれません。