コントローラ実装入門

controller-runtimeは非常にたくさんの機能を提供しています。 ここではまずcontroller-runtimeの基本的な機能に触れ、簡単なコントローラの実装方法を学んでいきます。 より詳細を知りたい方は次ページ以降をご覧ください。

Reconcileの実装

まずはKubebuilderによって生成されたcontrollers/tenant_controller.goを開いてみましょう。 以下のようなReconcileという関数がみつかると思います。 カスタムコントローラの実装は、このReconcile関数の中に書いていくことになります。

func (r *TenantReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    _ = context.Background()
    _ = r.Log.WithValues("tenant", req.NamespacedName)

    // your logic here

    return ctrl.Result{}, nil
}

このReconcile関数は、Tenantカスタムリソースを作成したり、更新・削除をおこなったタイミングで呼び出されます。 Kubernetesクラスタの状態をTenantリソースで指定された内容と一致させるための処理をReconcile関数に実装していきます。

リソースの取得

Reconcile関数の引数であるreconcile.Requestには、このカスタムコントローラの管理対象であるTenantリソースのNamespaceとNameが含まれています。 この情報を利用して、APIサーバーからテナントリソースの情報を取得してみましょう。

func (r *TenantReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    ctx := context.Background()
    log := r.Log.WithValues("tenant", req.NamespacedName)

    var tenant multitenancyv1.Tenant
    err := r.Get(ctx, req.NamespacedName, &tenant)
    if err != nil {
        return ctrl.Result{}, err
    }
    log.Info("got tenant", "tenant", tenant)

    return ctrl.Result{}, nil
}

make runなどで実行してみると、取得したTenantリソースの内容がログに出力されていることがわかります。

リソースの作成

つぎに、Tenantリソースに記述された内容に応じてリソースの作成をおこなってみましょう。

前述したようにReconcile関数は冪等、すなわち何度呼び出されても同じ結果になるように実装しなければなりません。 そこでcontroller-runtimeには、リソースが存在しなければ作成し、存在すれば更新するCreateOrUpdate()という便利な関数が用意されています。

この関数を利用して、TenantリソースのNamespacesフィールドに指定された名前のNamespaceリソースを作成してみましょう。 なお、CreateOrUpdate関数の第4引数にはリソースの更新処理をおこなうための関数を指定します。

func (r *TenantReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    ctx := context.Background()
    log := r.Log.WithValues("tenant", req.NamespacedName)

    var tenant multitenancyv1.Tenant
    err := r.Get(ctx, req.NamespacedName, &tenant)
    if err != nil {
        return ctrl.Result{}, err
    }
    log.Info("got tenant", "tenant", tenant)

    for _, name := range tenant.Spec.Namespaces {
        ns := &corev1.Namespace{
            ObjectMeta: metav1.ObjectMeta{
                Name: name,
            },
        }
        _, err = ctrl.CreateOrUpdate(ctx, r, ns, func() error {
            return nil
        })
        if err != nil {
            return ctrl.Result{}, err
        }
    }

    return ctrl.Result{}, nil
}

下記のようなTenantリソースのマニフェストをKubernetesクラスタに適用し、上記のカスタムコントローラを実行してみましょう。 sample1, sample2という名前のNamespaceが作成されていることが確認できるでしょう。

apiVersion: multitenancy.example.com/v1
kind: Tenant
metadata:
  name: tenant-sample
spec:
  namespaces:
    - sample1
    - sample2

リソースの削除

作成したリソースは1つ1つDelete関数を呼び出して削除することもできますが、Kubernetesにはリソースをガベージコレクションするための機能が用意されています。 詳細はリソースの削除で解説しますが、ガベージコレクション機能を利用すると、ある親リソースが削除されるとそのリソースを親に持つ子リソースが自動的に削除されます。

controller-runtimeでは、リソースに親リソースを指定するためにcontrollerutil.SetControllerReferenceという関数が用意されています。

下記のように、CreateOrUpdateの第4引数に指定した関数内でcontrollerutil.SetControllerReferenceを呼んでみます。

func (r *TenantReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    ctx := context.Background()
    log := r.Log.WithValues("tenant", req.NamespacedName)

    var tenant multitenancyv1.Tenant
    err := r.Get(ctx, req.NamespacedName, &tenant)
    if err != nil {
        return ctrl.Result{}, err
    }
    log.Info("got tenant", "tenant", tenant)

    for _, name := range tenant.Spec.Namespaces {
        ns := &corev1.Namespace{
            ObjectMeta: metav1.ObjectMeta{
                Name: name,
            },
        }
        _, err = ctrl.CreateOrUpdate(ctx, r, ns, func() error {
            return ctrl.SetControllerReference(&tenant, ns, r.Scheme)
        })
        if err != nil {
            return ctrl.Result{}, err
        }
    }

    return ctrl.Result{}, nil
}

この状態で先ほど作成したtenant-sampleというリソースを削除してみましょう。 すると、そのリソースを親に持つsample1, sample2というNamespaceリソースも自動的に削除されることが確認できます。

ステータスの更新

カスタムコントローラが何らかの処理をおこなったら、その結果を利用者に知らせることは重要です。 そのような情報を伝えるためにカスタムリソースのステータスを利用します。

先ほど利用したCreateOrUpdate関数は、リソースの作成や更新をおこなったのか、それとも何もおこなわなかったのかを戻り値で返します。 これを利用して、リソースの作成や更新処理がおこなわれた場合にステータスを更新してみましょう。

func (r *TenantReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    ctx := context.Background()
    log := r.Log.WithValues("tenant", req.NamespacedName)

    var tenant multitenancyv1.Tenant
    err := r.Get(ctx, req.NamespacedName, &tenant)
    if err != nil {
        return ctrl.Result{}, err
    }
    log.Info("got tenant", "tenant", tenant)

    updated := false
    for _, name := range tenant.Spec.Namespaces {
        ns := &corev1.Namespace{
            ObjectMeta: metav1.ObjectMeta{
                Name: name,
            },
        }
        op, err := ctrl.CreateOrUpdate(ctx, r, ns, func() error {
            return ctrl.SetControllerReference(&tenant, ns, r.Scheme)
        })
        if err != nil {
            return ctrl.Result{}, err
        }
        if op != controllerutil.OperationResultNone {
            updated = true
        }
    }
    if updated {
        tenant.Status = multitenancyv1.TenantStatus{
            Conditions: []multitenancyv1.TenantCondition{
                {
                    Type:               multitenancyv1.ConditionReady,
                    Status:             corev1.ConditionTrue,
                    LastTransitionTime: metav1.NewTime(time.Now()),
                },
            },
        }
        err := r.Status().Update(ctx, &tenant)
        if err != nil {
            return ctrl.Result{}, err
        }
    }

    return ctrl.Result{}, nil
}

再度Tenantリソースを作成してみましょう。 成功すると、Tenantリソースのステータスが更新されていることが確認できます。

以上がカスタムコントローラ実装の基本となります。 Kubernetesやcontroller-runtimeには、カスタムコントローラを実装するために必要な機能が他にもたくさん用意されています。 以降のページではそれらの詳細について解説していきます。

results matching ""

    No results matching ""