利用 Kubernetes 扩展 API 自定义资源控制器

前言

随着 Kubernetes 的广泛应用,它成为了现代化应用程序交付的事实标准。在 Kubernetes 中,我们可以方便地管理许多容器化工作负载,如 Pod、Service、Deployment 等。但在实际的项目中,我们还经常会遇到需要扩展 Kubernetes API 的需求。例如,我们的项目可能需要另外一种资源类型,如数据库,为此我们需要自定义一个资源类型以管理该数据库。

本文主要介绍如何在 Kubernetes 中创建自定义资源控制器。

Kubernetes 扩展 API

在 Kubernetes 中,我们可以使用自定义资源定义(Custom Resource Definition,CRD)为 Kubernetes API 添加新的资源类型。你可以注册自己的资源类型,用于存储和管理通过 Kubernetes 运行的应用程序的特定配置。注册自定义资源类型后,我们可以使用 Kubernetes API 对其进行 CRUD 操作。

扩展 Kubernetes API 的需求案例

假设我们现在需要实现一个简单的书籍管理系统,其中包括书籍信息和作者信息。我们的系统需要支持以下操作:

  1. 添加新书籍;
  2. 修改现有书籍;
  3. 删除书籍;
  4. 查看所有书籍列表。

针对这个需求,我们需要自定义一个 Book 资源类型,包括以下属性:

  • id:每本书的唯一标识符;
  • title:书籍标题;
  • author:作者名字;
  • publisher:出版社。

我们的 Book 资源控制器将管理所有 Book 资源的生命周期。

创建自定义资源控制器

在 Kubernetes 中,自定义资源定义通过 API Server 中的 API 扩展机制实现。因此,为了创建一个自定义资源控制器,我们首先需要定义一种用于描述新资源类型的对象,该对象是 Kubernetes API 中现有对象的一种扩展。

CRD 由三部分组成:

  1. 自定义资源的名称和版本;
  2. 该资源的 API 结构,用于描述资源类型和标准 Kubernetes API 功能的元数据;
  3. 自定义控制器,用于实现资源逻辑,更新我们定义的自定义资源。

以下是一个简单的 Book 资源的定义:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: books.example.com
spec:
  group: example.com
  versions:
    - name: v1
      served: true
      storage: true
  scope: Namespaced
  names:
    plural: books
    singular: book
    kind: Book
    shortNames:
    - b
  subresources:
    status: {}

上述配置描述了一个名为 Book 的自定义资源类型,其中:

  • group:所属组;
  • versions:API 版本;
  • scope:命名空间;
  • names:自定义资源名称;
  • subresources:定义自定义资源支持的子资源。

接下来,我们需要创建一个 Kubernetes 控制器,用于实现上述自定义资源类型的逻辑操作。

以下是一个简单的 Book 资源控制器示例,它实现了 Book 的增删改查操作:

package main

import (
    "context"
    "fmt"
    "time"

    appsv1 "k8s.io/api/apps/v1"
    batchv1 "k8s.io/api/batch/v1"
    v1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/schema"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/client/config"
    ctrl "sigs.k8s.io/controller-runtime"
    bookv1 "github.com/example.com/books/api/v1"
)

const (
    timeout = time.Second * 15
)

func main() {
    cfg := config.GetConfigOrDie()
    k8sclient, err := client.New(cfg, client.Options{Scheme: scheme})
    if err != nil {
        panic(err)
    }

    if err := (&BookReconciler{
        Client: k8sclient,
    }).SetupWithManager(ctrl.NewManager(cfg, ctrl.Options{
        Scheme: scheme,
    })); err != nil {
        panic(err)
    }

    if err := ctrl.NewWebhookManagedBy(mgr).For(&bookv1.Book{}).Complete(); err != nil {
        panic(err)
    }
}

type BookReconciler struct {
    client.Client
}

func (r *BookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    logger := log.FromContext(ctx)

    book := &bookv1.Book{}
    if err := r.Get(ctx, req.NamespacedName, book); err != nil {
        if errors.IsNotFound(err) {
            logger.Info("resource not found", "name", req.NamespacedName)
            return ctrl.Result{}, nil
        }
        logger.Error(err, "failed to reconcile book")
        return ctrl.Result{}, err
    }

    logger.Info("reconciling book", "name", book.Name)

    if book.DeletionTimestamp != nil {
        logger.Info("deleting book", "name", book.Name)
        if err := r.deleteResource(ctx, book); err != nil {
            logger.Error(err, "failed to delete book")
            return ctrl.Result{}, err
        }
        return ctrl.Result{}, nil
    }

    if book.Status.Status != bookv1.BookStatusSucceeded {
        logger.Info("creating book", "name", book.Name)
        if err := r.createResource(ctx, book); err != nil {
            logger.Error(err, "failed to create book")
            return ctrl.Result{}, err
        }
        return ctrl.Result{}, nil
    }

    logger.Info("book status already succeeded", "name", book.Name)
    return ctrl.Result{}, nil
}

func (r *BookReconciler) createResource(ctx context.Context, book *bookv1.Book) error {
    // Create new book
    // ...

    book.Status.Status = bookv1.BookStatusSucceeded
    if err := r.Status().Update(ctx, book); err != nil {
        return err
    }

    return nil
}

func (r *BookReconciler) updateResource(ctx context.Context, book *bookv1.Book) error {
    // Update existing book
    // ...

    return nil
}

func (r *BookReconciler) deleteResource(ctx context.Context, book *bookv1.Book) error {
    // Delete book
    // ...

    return nil
}

func init() {
    SchemeBuilder.Register(&bookv1.Book{}, &bookv1.BookList{})
}

var scheme = runtime.NewScheme()

func addKnownTypes(scheme *runtime.Scheme) error {
    gv := schema.GroupVersion{Group: bookv1.GroupVersion.Group, Version: bookv1.GroupVersion.Version}
    scheme.AddKnownTypes(gv,
        &bookv1.Book{},
        &bookv1.BookList{},
    )
    metav1.AddToGroupVersion(scheme, gv)
    return nil
}

上述代码定义了 Book 控制器,其包括以下逻辑:

  1. 根据 Book 自定义资源来触发控制器;
  2. 如果 Book 资源不存在,则创建新资源;
  3. 如果 Book 资源已被删除,则删除 Book 资源;
  4. 否则更新 Book 资源。

部署自定义资源控制器

部署自定义资源控制器的过程基本上与部署 Kubernetes 中的任何其他控制器相同。

  1. 克隆完整的示例代码:
git clone https://github.com/example.com/books
cd books
  1. 预添加 CRD:
kubectl apply -f deploy/crd.yaml
  1. 部署控制器:
kubectl apply -f deploy/operator.yaml

总结

在 Kubernetes 中,创建自定义资源控制器可以帮助我们扩展 Kubernetes API 以管理我们的应用程序的特定配置。本文介绍了如何创建一个简单的自定义资源控制器,并提供了示例代码以供参考。无论您是需要将现有应用程序移植到 Kubernetes 上,还是要为 Kubernetes 开发新应用程序,创建自定义资源控制器都是非常重要和强大的最佳实践。

来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/65af83a5add4f0e0ff8f5845