Manager

Managerは、 複数のコントローラーを管理し、リーダー選出機能やメトリクスやヘルスチェックサーバーなどの機能を提供します。

すでにこれまでManagerのいくつかの機能を紹介してきましたが、他にもたくさんの便利な機能を持ってるのでここで紹介していきます。

Leader Election

カスタムコントローラーの可用性を向上させたい場合、Deploymentの機能を利用してカスタムコントローラーのPodを複数個立ち上げます。 しかし、Reconcile処理が同じリソースに対して何らかの処理を実行した場合、競合が発生してしまいます。

そこで、Managerはリーダー選出機能を提供しています。 これにより複数のプロセスの中から1つだけリーダーを選出し、リーダーに選ばれたプロセスだけがReconcile処理を実行できるようになります。

リーダー選出の利用方法は、NewManagerのオプションのLeaderElectionにtrueを指定し、LeaderElectionIDにリーダー選出用のIDを指定するだけです。 リーダー選出は、同じLeaderElectionIDを指定したプロセスの中から1つだけリーダーを選ぶという挙動になります。

main.go

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
    Scheme:                 scheme,
    Metrics:                metricsServerOptions,
    WebhookServer:          webhookServer,
    HealthProbeBindAddress: probeAddr,
    LeaderElection:         enableLeaderElection,
    LeaderElectionID:       "3ca5b296.zoetrope.github.io",
    // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
    // when the Manager ends. This requires the binary to immediately end when the
    // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
    // speeds up voluntary leader transitions as the new leader don't have to wait
    // LeaseDuration time first.
    //
    // In the default scaffold provided, the program ends immediately after
    // the manager stops, so would be fine to enable this option. However,
    // if you are doing or is intended to do any operation such as perform cleanups
    // after the manager stops then its usage might be unsafe.
    // LeaderElectionReleaseOnCancel: true,
})

それでは、config/manager/manager.yamlreplicasフィールドを2に変更して、MarkdownViewコントローラーをデプロイしてみましょう。

デプロイされた2つのPodのログを表示させてみると、リーダーに選出された方のPodだけがReconcile処理をおこなっている様子が確認できます。 リーダーに選出されたPodを終了させると、もう片方のPodにリーダーが切り替わる様子を確認できます。

なお、Admission Webhook処理は競合の心配がないため、リーダーではないプロセスも呼び出されます。

Runnable

カスタムコントローラーの実装において、Reconcile Loop以外にもgoroutineを立ち上げて定期的に実行したり、何らかのイベントを待ち受けたりしたい場合があります。 Managerではそのような処理を実現するための仕組みを提供しています。

例えばTopoLVMでは、定期的なメトリクスの収集やgRPCサーバーの起動用にRunnableを利用しています。

Runnable機能を利用するためには、Runnableインタフェースを実装した以下のようなコードを用意します。 ここでは30秒周期で、MarkdownViewControllerにReconcileを実行するように通知するRunnerを実装しています。

runner.go

package controller

import (
    "context"
    "time"

    "github.com/go-logr/logr"
    viewv1 "github.com/zoetrope/markdown-view/api/v1"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/event"
)

type Runner struct {
    client   client.Client
    logger   logr.Logger
    interval time.Duration
    channel  chan<- event.TypedGenericEvent[*viewv1.MarkdownView]
}

func NewRunner(client client.Client, logger logr.Logger, interval time.Duration, channel chan<- event.TypedGenericEvent[*viewv1.MarkdownView]) *Runner {
    return &Runner{
        client:   client,
        logger:   logger,
        interval: interval,
        channel:  channel,
    }
}

func (r Runner) Start(ctx context.Context) error {
    ticker := time.NewTicker(r.interval)
    defer ticker.Stop()
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
            r.notify(ctx)
        }
    }
}

func (r Runner) notify(ctx context.Context) {
    var mdviewList viewv1.MarkdownViewList
    err := r.client.List(ctx, &mdviewList)
    if err != nil {
        r.logger.Error(err, "failed to list MarkdownView")
        return
    }

    for _, sts := range mdviewList.Items {
        r.channel <- event.TypedGenericEvent[*viewv1.MarkdownView]{
            Object: sts.DeepCopy(),
        }
    }
}

func (r Runner) NeedLeaderElection() bool {
    return true
}

StartメソッドはManagerのStartを呼び出した際に、goroutineとして呼び出されます。 引数のcontextによりManagerからの終了通知を受け取ることができます。

main.go

ch := make(chan event.TypedGenericEvent[*viewv1.MarkdownView])
runner := controller.NewRunner(mgr.GetClient(), mgr.GetLogger().WithName("Runner"), 30*time.Second, ch)
err = mgr.Add(runner)
if err != nil {
    setupLog.Error(err, "unable to add runner")
    os.Exit(1)
}

なお、このRunnerの処理は通常リーダーとして動作しているManagerでしか動きません。 リーダーでなくても常時動かしたい処理である場合、LeaderElectionRunnableインタフェースを実装し、 NeedLeaderElectionメソッドで false を返すようにします。

また、MarkdownViewControllerでは、Runnerからの通知を受けてReconcileを実行するように、WatchesRawSourceを利用します。

markdownview_controller.go

    return ctrl.NewControllerManagedBy(mgr).
        For(&viewv1.MarkdownView{}).
        Owns(&corev1.ConfigMap{}).
        Owns(&appsv1.Deployment{}).
        Owns(&corev1.Service{}).
        WatchesRawSource(source.Channel(ch, &handler.TypedEnqueueRequestForObject[*viewv1.MarkdownView]{})).
        Complete(r)

EventRecorder

カスタムリソースのStatusには、現在の状態が保存されています。 一方、これまでどのような処理が実施されてきたのかを記録したい場合、Kubernetesが提供するEventリソースを利用できます。

Managerはイベントを記録するための機能を提供しており、GetEventRecorderForEventRecorderを取得できます。 以下のように、Reconcilerを初期化する際にEventRecorderを渡します。

main.go

ctx := ctrl.SetupSignalHandler()
if err = (&controller.MarkdownViewReconciler{
    Client:   mgr.GetClient(),
    Scheme:   mgr.GetScheme(),
    Recorder: mgr.GetEventRecorderFor("markdownview-controller"),
}).SetupWithManager(ctx, mgr, ch); err != nil {
    setupLog.Error(err, "unable to create controller", "controller", "MarkdownView")
    os.Exit(1)
}

Reconcilerではこれをフィールドとして持っておきます。

markdownview_controller.go

// MarkdownViewReconciler reconciles a MarkdownView object
type MarkdownViewReconciler struct {
    client.Client
    Scheme   *runtime.Scheme
    Recorder record.EventRecorder
}

Eventを記録するための関数として、Event, Eventf, AnnotatedEventfなどが用意されています。 ここでは、ステータス更新時に以下のようなイベントを記録することにしましょう。なお、イベントタイプにはEventTypeNormal, EventTypeWarningのみ指定できます。

markdownview_controller.go

if meta.IsStatusConditionFalse(prevStatus.Conditions, viewv1.TypeMarkdownViewDegraded) &&
    meta.IsStatusConditionTrue(mdView.Status.Conditions, viewv1.TypeMarkdownViewDegraded) {
    r.Recorder.Event(&mdView, corev1.EventTypeWarning, "Degraded", fmt.Sprintf("MarkdownView(%s:%s) degraded", mdView.Namespace, mdView.Name))
}
if meta.IsStatusConditionFalse(prevStatus.Conditions, viewv1.TypeMarkdownViewAvailable) &&
    meta.IsStatusConditionTrue(mdView.Status.Conditions, viewv1.TypeMarkdownViewAvailable) {
    r.Recorder.Event(&mdView, corev1.EventTypeNormal, "Available", fmt.Sprintf("MarkdownView(%s:%s) available", mdView.Namespace, mdView.Name))
}

このEventリソースは第1引数で指定したリソースに結びいており、そのリソースと同じnamespaceにEventリソースが作成されます。 カスタムコントローラーがEventリソースを作成できるように、以下のようなRBACのマーカーを追加し、make manifestsでマニフェストを更新しておきます。

// +kubebuilder:rbac:groups=core,resources=events,verbs=create;update;patch

コントローラーを実行し、作成したEventリソースを確認してみましょう。なお、Eventリソースはデフォルト設定では1時間経つと消えてしまいます。

$ kubectl get events -n default
LAST SEEN   TYPE     REASON                    OBJECT                                             MESSAGE
4s          Normal   Available                 markdownview/markdownview-sample                   MarkdownView(default:markdownview-sample) available

HealthProbe

Managerには、ヘルスチェック用のAPIのエンドポイントを作成する機能が用意されています。

ヘルスチェック機能を利用するには、Managerの作成時にHealthProbeBindAddressでエンドポイントのアドレスを指定します。

main.go

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
    Scheme:                 scheme,
    Metrics:                metricsServerOptions,
    WebhookServer:          webhookServer,
    HealthProbeBindAddress: probeAddr,
    LeaderElection:         enableLeaderElection,
    LeaderElectionID:       "3ca5b296.zoetrope.github.io",
    // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
    // when the Manager ends. This requires the binary to immediately end when the
    // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
    // speeds up voluntary leader transitions as the new leader don't have to wait
    // LeaseDuration time first.
    //
    // In the default scaffold provided, the program ends immediately after
    // the manager stops, so would be fine to enable this option. However,
    // if you are doing or is intended to do any operation such as perform cleanups
    // after the manager stops then its usage might be unsafe.
    // LeaderElectionReleaseOnCancel: true,
})

そして、AddHealthzCheckAddReadyzCheckで、ハンドラの登録をおこないます。 デフォルトではhealthz.Pingという何もしない関数を利用していますが、独自関数の登録も可能です。

main.go

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
    setupLog.Error(err, "unable to set up health check")
    os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
    setupLog.Error(err, "unable to set up ready check")
    os.Exit(1)
}

カスタムコントローラーのマニフェストでは、このヘルスチェックAPIをlivenessProbereadinessProbeとして利用するように指定されています。

manager.yaml

livenessProbe:
  httpGet:
    path: /healthz
    port: 8081
  initialDelaySeconds: 15
  periodSeconds: 20
readinessProbe:
  httpGet:
    path: /readyz
    port: 8081
  initialDelaySeconds: 5
  periodSeconds: 10

FieldIndexer

クライアントの使い方で紹介したように、複数のリソースを取得する際にラベルやnamespaceで絞り込むことが可能です。 しかし、特定のフィールドの値に応じてフィルタリングしたいこともあるでしょう。 controller-runtimeではインメモリにキャッシュしているリソースに対してインデックスを張る仕組みが用意されています。

index

インデックスを利用するためには事前にManagerのGetFieldIndexer()を利用して、どのフィールドの値に基づいてインデックスを張るのかを指定します。 下記の例ではConfigMapリソースに対して、ownerReferencesに指定されているMarkdownViewリソースの名前でインデックスを作成しています。

markdownview_controller.go

const ownerControllerField = ".metadata.ownerReference.controller"

func indexByOwnerMarkdownView(obj client.Object) []string {
    cm := obj.(*corev1.ConfigMap)
    owner := metav1.GetControllerOf(cm)
    if owner == nil {
        return nil
    }
    if owner.APIVersion != viewv1.GroupVersion.String() || owner.Kind != "MarkdownView" {
        return nil
    }
    return []string{owner.Name}
}

markdownview_controller.go

err := mgr.GetFieldIndexer().IndexField(ctx, &corev1.ConfigMap{}, ownerControllerField, indexByOwnerMarkdownView)
if err != nil {
    return err
}

IndexFieldの第3引数のフィールド名には、どのフィールドを利用してインデックスを張っているのかを示す文字列を指定します。 ここでは、.metadata.ownerReference.controllerという文字列を指定しています。 実際にインデックスに利用しているフィールドのパスと一致していなくても問題はないのですが、一致させると可読性がよくなるのでおすすめです。

なおインデックスはGVKごとに作成されるので、異なるタイプのリソース間でフィールド名が同じになっても問題ありません。 またnamespaceスコープのリソースの場合は、内部的にフィールド名にnamespace名を付与して管理しているので、明示的にフィールド名にnamespaceを含める必要はありません。 インデクサーが返す値はスライスになっていることから分かるように、複数の値にマッチするようなインデックスの構成も可能です。

上記のようなインデックスを作成しておくと、List()を呼び出す際に特定のフィールドが指定した値と一致するリソースだけを取得できます。 例えば以下の例であれば、ownerReferenceに指定したMarkdownViewリソースがセットされているConfigMapだけを取得できます。

var cms corev1.ConfigMapList
err := r.List(ctx, &cms, client.MatchingFields(map[string]string{ownerControllerField: mdView.Name}))
if err != nil {
    return err
}

results matching ""

    No results matching ""