Manager

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

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

Leader Election

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

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

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

main.go

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
    Scheme:                 scheme,
    MetricsBindAddress:     metricsAddr,
    Port:                   9443,
    LeaderElection:         enableLeaderElection,
    LeaderElectionID:       "27475f02.example.com",
    HealthProbeBindAddress: probeAddr,
})

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

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

リーダー選出の機能にはConfigMapが利用されています。 下記のようにConfigMapを表示させてみると、metadata.annotations["control-plane.alpha.kubernetes.io/leader"]に、現在のリーダーの情報が保存されていることがわかります。

$ kubectl get -n tenant-system configmap 27475f02.example.com -o yaml
apiVersion: v1
kind: ConfigMap
metadata:
  annotations:
    control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"tenant-controller-manager-5d6f8bbd95-h5jpx_85d3882f-1419-42dc-928b-bd7d7dfb8cff","leaseDurationSeconds":15,"acquireTime":"2020-07-25T07:10:29Z","renewTime":"2020-07-25T10:31:41Z","leaderTransitions":10}'
  creationTimestamp: "2020-07-18T09:00:57Z"
  name: 27475f02.example.com
  namespace: tenant-system
  resourceVersion: "1206094"
  selfLink: /api/v1/namespaces/tenant-system/configmaps/27475f02.example.com
  uid: bb91b084-8c8e-4361-9454-071930a1d67c

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

Runnable

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

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

Runnable機能を利用するためには、まずRunnableインタフェースを実装した以下のようなコードを用意します。

runner.go

package runners

import (
    "fmt"
    "time"
)

type Runner struct {
}

func (r Runner) Start(ch <-chan struct{}) error {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ch:
            return nil
        case <-ticker.C:
            fmt.Println("run something")
        }
    }
}

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

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

err = mgr.Add(&runners.Runner{})

Runnable インタフェースを実装しただけだと、リーダーとして動作している Manager でしか動かないようになります。 リーダーでなくても常時動かしたい処理である場合、LeaderElectionRunnableインタフェースを実装し、NeedLeaderElectionメソッドで false を返すようにします。

recorderProvider

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

Managerはイベントを記録するための機能を提供しており、以下のように取得することができます。

recorder := mgr.GetEventRecorderFor("tenant-controller")

このEventRecorderをReconcilerに渡して利用します。

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

// Reconcileによる更新処理が成功した場合に、Normalタイプのイベントを記録
r.Recorder.Event(&tenant, corev1.EventTypeNormal, "Updated", "the tenant was updated")

// Reconcileによる処理が失敗した場合に、Warningタイプのイベントを記録
r.Recorder.Eventf(&tenant, corev1.EventTypeWarning, "Failed", "failed to reconciled: %s", err.Error())

このEventリソースは第1引数で指定したリソースに結びいており、namespace-scopedリソースの場合はそのリソースと同じnamespaceにEventリソースが作成されます。 一方cluster-scopedリソースの場合は、default namespaceにEventリソースが作成されます。

テナントリソースはcluster-scopedリソースなのでEventはdefault namespaceに作成されます。 そこで下記のようなRoleとRoleBindingを用意して、テナントコントローラがdefault namespaceにEventリソースを作成できるように設定しておきましょう。

event_recorder_rbac.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: eventrecorder-role
  namespace: default
rules:
  - apiGroups:
      - ""
    resources:
      - events
    verbs:
      - create
      - delete
      - get
      - list
      - patch
      - update
      - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: eventrecorder-rolebinding
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: eventrecorder-role
subjects:
  - kind: ServiceAccount
    name: default
    namespace: tenant-system

それでは、作成されたEventリソースを確認してみましょう。なお、Eventリソースはデフォルトで1時間経つと消えてしまいます。

$ kubectl get events -n default
LAST SEEN   TYPE     REASON    OBJECT                 MESSAGE
6s          Normal   Updated   tenant/tenant-sample   the tenant was updated

healthProbeListener

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

まずは、Managerの作成時にHealthProbeBindAddressでエンドポイントのアドレスを指定します。

main.go

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
    Scheme:                 scheme,
    MetricsBindAddress:     metricsAddr,
    Port:                   9443,
    LeaderElection:         enableLeaderElection,
    LeaderElectionID:       "27475f02.example.com",
    HealthProbeBindAddress: probeAddr,
})

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

main.go

err = mgr.AddHealthzCheck("ping", healthz.Ping)
if err != nil {
    setupLog.Error(err, "unable to add healthz check")
    os.Exit(1)
}
err = mgr.AddReadyzCheck("ping", healthz.Ping)
if err != nil {
    setupLog.Error(err, "unable to add readyz check")
    os.Exit(1)
}

これでコントローラにヘルスチェック用のAPIが実装できました。 マニフェストにlivenessProbereadinessProbeの設定を追加しておきましょう。

manager.yaml

        livenessProbe:
          httpGet:
            path: /healthz
            port: 9090
          initialDelaySeconds: 3
          periodSeconds: 3
        readinessProbe:
          httpGet:
            path: /readyz
            port: 9090
          initialDelaySeconds: 5
          periodSeconds: 10

Inject

Managerには、ReconcilerやRunnerなどに特定のオブジェクトをインジェクトする機能があります。 InjectClientやInjectLoggerなどのインタフェースを実装すると、managerのStartメソッドを実行したタイミングで、 Kubernetesクライアントやロガーのオブジェクトを受け取ることが可能です。

利用可能なインタフェースについては下記のパッケージを参照してください。

これらのオブジェクトのほとんどは、Getメソッドで取得することができるため、injectの使いみちはそれほど多くありません。 しかし、StopChannelだけはinjectでしか取得することができないため、Reconcilerの中でmanagerからの 終了通知を受け取りたい場合は、以下のようにInjectStopChannelを利用することになります。

inject.go

package controllers

import "context"

func (r *TenantReconciler) InjectStopChannel(ch <-chan struct{}) error {
    r.stopCh = ch
    return nil
}

func contextFromStopChannel(ch <-chan struct{}) context.Context {
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        defer cancel()
        <-ch
    }()
    return ctx
}

このStopChannelからcontext.Contextを作成しておけば、Reconcile Loopの中でmanagerからの終了通知を受け取れます。

type TenantReconciler struct {
    stopCh   <-chan struct{}
}

func (r *TenantReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    ctx := contextFromStopChannel(r.stopCh)

ただし、Reconcileは短時間で終了することが望ましいとされていますので、長時間ブロックするような処理はなるべく記述しないようにしましょう。

results matching ""

    No results matching ""