Kubernetes Operator简介与构建

2023-11-06 15:23:23 浏览数 (1)

自动化任务总是有其特殊之处。当我们想要执行某些任务时,我们需要能够对某些特定事件做出反应或被触发。但很多事件无法轻松监听,尤其是在 Kubernetes 集群中。所以今天,我们将看看如何尝试使用Operator来解决它。我们将了解如何创建 Kubernetes Operator!

Operator Pattern 简介

Operator 是 Kubernetes 的软件扩展,它利用自定义资源来管理应用程序及其组件。Operator 遵循 Kubernetes 原则,特别是控制循环。

Operator Pattern是什么?

这种模式允许 Kubernetes 用户创建自己的资源控制器,以便自动管理其应用程序/产品堆栈。 操作员模式使用CRD (自定义资源定义)来促进资源/任务配置。这是来自 Strimzi 的 CRD 示例,它让我们创建一个完整的 Kafka 集群。

代码语言:javascript复制
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
  name: my-cluster
spec:
  kafka:
    version: 3.4.0
    replicas: 3
    listeners:
      - name: plain
        port: 9092
        type: internal
        tls: false
      - name: tls
        port: 9093
        type: internal
        tls: true
    config:
      offsets.topic.replication.factor: 3
      transaction.state.log.replication.factor: 3
      transaction.state.log.min.isr: 2
      default.replication.factor: 3
      min.insync.replicas: 2
      inter.broker.protocol.version: "3.4"
    storage:
      type: jbod
      volumes:
      - id: 0
        type: persistent-claim
        size: 100Gi
        deleteClaim: false
  zookeeper:
    replicas: 3
    storage:
      type: persistent-claim
      size: 100Gi
      deleteClaim: false
  entityOperator:
    topicOperator: {}
    userOperator: {}
```*Exemple d'un CRD de Strimzi permettant de créer un cluster Kafka en quelques lignes.*
```yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
  name: my-cluster
spec:
  kafka:
    version: 3.4.0
    replicas: 3
    listeners:
      - name: plain
        port: 9092
        type: internal
        tls: false
      - name: tls
        port: 9093
        type: internal
        tls: true
    config:
      offsets.topic.replication.factor: 3
      transaction.state.log.replication.factor: 3
      transaction.state.log.min.isr: 2
      default.replication.factor: 3
      min.insync.replicas: 2
      inter.broker.protocol.version: "3.4"
    storage:
      type: jbod
      volumes:
      - id: 0
        type: persistent-claim
        size: 100Gi
        deleteClaim: false
  zookeeper:
    replicas: 3
    storage:
      type: persistent-claim
      size: 100Gi
      deleteClaim: false
  entityOperator:
    topicOperator: {}
    userOperator: {}

由于它遵循许多 Kubernetes 原则作为控制循环,因此您的操作员将(取决于您的配置和您在代码中编写的内容)能够监视它已创建的资源和/或集群上的其他资源。 以下是来自RedHat 博客的一个方案,说明了上下文。

https://developers.redhat.com/articles/2021/06/22/kubernetes-operators-101-part-2-how-operators-work#deploying_workloads_in_kubernetes

image.png

例子

为了更具体,这里举一些例子。

1. 自动化以避免重复多次部署

如果您所在的团队部署并授予公司每个团队访问完整工具堆栈来收集和可视化其指标的权限,则您需要为每个团队手动部署以下堆栈:

  • Prometheus
  • OpenTelemetry
  • Grafana
  • Postgres

在这种情况下,您不必重复相同的任务来部署所有这些应用程序和所有相关组件(服务、配置映射、服务帐户...),您只需创建一个操作员,让它使用 CRD 为您管理此类任务即可包含所有可以更新的配置。 例如,可以命名 CRD ObservabilityStack,并且每次创建该资源的新实例时,它都会自动创建您定义的所有资源。

2. 自动化以避免人工错误

已经管理过StatefulSet的人知道我要说什么。 当我们管理一些应用程序(尤其是带有Volume的应用程序)时,我们可能需要按照特定的顺序执行一些特定的任务,以便创建、更新或删除某些内容。

因此,如果您只有 3 4 个此类资源,则使用 bash 脚本或 ansible 脚本来完成此操作可能是正确的。但如果你有50个呢?100?或者更多?

在这种情况下,运算符模式也可以为您提供帮助。如果您能够定义需要执行的所有任务,它们将在所需的情况下执行。 因此,在更新过程中,该应用程序将不再依赖于您。它会让你的生活更轻松。

3. 自动化配置

在此示例中,假设您在一个管理 Nginx 的团队中,该 Nginx 公开了您公司的所有 API。所有 API 和 nginx 都位于同一个 Kubernetes 集群中。部署新端点后,您需要在所有环境中使用新端点更新配置文件。此外,您的公司喜欢微服务,因此您每周都会有新的 API 和更新。

其中一些还被重命名、移动甚至删除。但您并不总是处于循环状态,因此如果一个 api 不再工作,您会收到电话以了解发生了什么情况。

正如您已经了解的那样,运算符是在这种情况下为您提供帮助的解决方案。由于您能够跟踪所有集群上的资源,因此您可以查看是否添加、重命名或删除了某些部署!因此,有了这个,您就可以在发生此类事件时触发,并且可以更新您的配置文件!

通过所有这些示例,我想您已经了解了该模式的原理和实用性。

创建Kubernetes Operator

与每个开源解决方案一样,如果您需要做某事,就会有一堆具有各自特殊性的工具。如果你想查看列表,请查看Kubernetes 文档。在本系列中,我们将使用Operator Framework和KubeBuilder。 Kubernetes文档:https://kubernetes.io/docs/concepts/extend-kubernetes/operator/#writing-operator Operator Framework: https://operatorframework.io/ KubeBuilder: https://book.kubebuilder.io/

Operator Framework

关于 Operator Framework 的几句话,我们将使用Go SDK,但您需要知道您也可以将其与Ansible和Helm一起使用。

设置

Homebrew

如果您使用的是Homebrew,则可以使用以下命令安装Operator Framework SDK :

代码语言:javascript复制
brew install operator-sdk

From Github Release

代码语言:javascript复制
# Define informations about your platform
export ARCH=$(case $(uname -m) in x86_64) echo -n amd64 ;; aarch64) echo -n arm64 ;; *) echo -n $(uname -m) ;; esac)
export OS=$(uname | awk '{print tolower($0)}')

# Download the binary for your platform
export OPERATOR_SDK_DL_URL=https://github.com/operator-framework/operator-sdk/releases/download/v1.28.0
curl -LO ${OPERATOR_SDK_DL_URL}/operator-sdk_${OS}_${ARCH}
# Install the binary
chmod  x operator-sdk_${OS}_${ARCH} && sudo mv operator-sdk_${OS}_${ARCH} /usr/local/bin/operator-sdk

创建我们的第一个运算符

初始化项目

首先要做的是使用以下命令初始化项目

Operator-sdk init — 域 [您的域] — 存储库 [您的代码存储库]

代码语言:javascript复制
operator-sdk init --domain adaendra.org --repo github.com/adaendra/test-operator

它将生成这样的文件夹结构:

您将能够找到一些通用文件、许多常见文件(例如 Makefile 或 Dockerfile)以及以main.go.

注意:默认情况下,您的命名空间能够监视集群中任何位置的资源。 所以如果你想限制它的视野,你可以更新管理器的定义来添加该Namespace选项。 mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{Namespace: "dummy_ns"})

有关运算符范围的更多信息,请查看SDK 文档

创建 API、控制器和 CRD

在很多情况下,当我们使用运算符时,我们希望创建一个自定义资源定义,它将用作我们任务的参考。 在本教程中,我们将在网关组中创建一个自定义资源MyProxy,该资源将为每个实例部署一个Nginx 部署。

生成代码的命令

代码语言:javascript复制
operator-sdk create api --group gateway --version v1alpha1 --kind MyProxy --resource --controller

执行后,您可以看到两个新文件夹:api和controllers。

应用程序编程接口API

在此文件夹中,我们唯一感兴趣的文件是myproxy_types.go. 在此文件中,我们将定义Spec中所需的所有字段,但我们也将在这里定义Status结构! 对于我们的示例,我们将仅在MyProxySpec中定义一个Name字段。

代码语言:javascript复制
type MyProxySpec struct {  
   Name string `json:"name,omitempty"`  
}

重要的 !!该文件用作为您的操作员构建大量 yaml 文件的基础。因此,此文件中的每次修改都执行这两个命令:make manifests&make generate

控制器Controller

在此文件夹中,您将找到与我们之前生成的自定义资源相关的每个控制器。myproxy_controller.go此文件夹是操作员可以执行的操作的中心位置。 在每个控制器文件中,您都会发现我们必须更新的两个方法:ReconcileSetupWithManager。 设置管理器

代码语言:javascript复制
// SetupWithManager sets up the controller with the Manager.
func (r *MyProxyReconciler) SetupWithManager(mgr ctrl.Manager) error {  
   return ctrl.NewControllerManagedBy(mgr).  
      For(&gatewayv1alpha1.MyProxy{}).  
      Owns(&appsv1.Deployment{}).  
      Complete(r)  
}

在这个例子中(这也是我们的实现),我们可以看到:

  • ctrl.NewControllerManagedBy(mgr)这将创建一个具有基本选项的新控制器。(通过这种方法,您可以个性化控制器选项,例如您想要并行的最大协调数量)
  • For(&gatewayv1alpha1.MyProxy{})将声明如果特定类型的资源上发生添加/更新/删除事件,我们希望触发协调。(这里是MyProxy)您可以将它用于您想要观看的每种资源。(例如,如果您想通过 Nginx 动态公开所有部署,则很有用)
  • Owns(&appsv1.Deployment{})For 非常相似,因此它会声明我们希望在发生添加/更新/删除事件时触发协调。但它也会添加一个过滤器,因为只有当操作员拥有带有事件的资源时才会触发对帐。(因此,如果您更新另一个部署,您的操作员中不会发生任何事情)

调和 该方法是操作员的核心,并且是每次触发对帐时都会执行的方法。但在深入了解该功能之前,有一件重要的事情需要先了解。在方法上方,您可以看到一些以 开头的注释// kubebuilder。此评论定义了您的操作员的权限! 因此,正确定义它们非常重要!在我们的例子中,我们需要向操作员添加一些权限,以便能够读取/创建和更新Deployments。 每个评论的定义如下:

// kubebuiler:rbac:groups=[资源组],resources=[资源名称],verbs=[动词]

资源组必须只有一个值,但对于资源 nameverbs,您可以一次定义多个值,将所有值用 联接起来;。

代码语言:javascript复制
//  kubebuilder:rbac:groups=gateway.adaendra.org,resources=myproxies,verbs=get;list;watch;create;update;patch;delete  
//  kubebuilder:rbac:groups=gateway.adaendra.org,resources=myproxies/status,verbs=get;update;patch  
//  kubebuilder:rbac:groups=gateway.adaendra.org,resources=myproxies/finalizers,verbs=update  
//  kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete

现在我们可以深入研究函数代码。如前所述,每次触发reconciliation时都会调用该函数。因此,我们必须小心我们在这里所做的事情! 例如,如果我们想要创建资源,我们必须确保它们尚未存在于集群上!如果它已经存在,我们必须检查它并根据需要进行一些更新! 1. 检索我们的自定义资源 因此,第一步是尝试检索自定义资源的实例(此处为 MyProxy 的实例)。我们需要它来获取其规格,并能够更新其状态。

代码语言:javascript复制
// Retrieve the resource
myProxy := &gatewayv1alpha1.MyProxy{}
err := r.Get(ctx, req.NamespacedName, myProxy)

if err != nil {
    // If we have an error and this error said "not found", we ignore the error
    if errors.IsNotFound(err) {
        log.Info("Resource not found. Error ignored as the resource must have been deleted.")
        return ctrl.Result{}, nil
    }
    // If it's another error, we return it
    log.Error(err, "Error while retrieving MyProxy instance")
    return ctrl.Result{}, err
}
  1. 检索Operator管理的资源 现在我们有了“父”资源,我们要检索“子”资源。在我们的例子中,它是一个部署,它的名称是使用 MyProxy 中的字段定义的Name,并且必须位于test_ns名称空间中。
代码语言:javascript复制
found := &appsv1.Deployment{}  
err = r.Get(ctx, types.NamespacedName{Name: myProxy.Spec.Name, Namespace: "test_ns"}, found)

3.检查资源是否存在 接下来的步骤包括检查我们在上一步中得到的内容。如果该变量err是未找到错误,我们就知道该资源不存在,因此我们可以创建它!如果它包含另一个错误,我们将返回它。 我们的实现将如下所示:

代码语言:javascript复制
if err != nil && errors.IsNotFound(err) {  
   // Define a new deployment  
   dep := r.deploymentForExample(myProxy)  
   log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)  
   err = r.Create(ctx, dep)  
   if err != nil {  
      log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)  
      return ctrl.Result{}, err  
   }  
   // Deployment created successfully - return and requeue  
   return ctrl.Result{Requeue: true}, nil  
} else if err != nil {  
   log.Error(err, "Failed to get Deployment")  
   return ctrl.Result{}, err  
}

这是一个非常简单的例子deploymentForExample

代码语言:javascript复制
func (r *MyProxyReconciler) deploymentForExample(myproxy *gatewayv1alpha1.MyProxy) *appsv1.Deployment {  
   dep := &appsv1.Deployment{}  
   dep.Namespace = "test_ns"  
   dep.Name = myproxy.Spec.Name  
   var replicas int32 = 2  
   labels := map[string]string{  
      "test_label": myproxy.Spec.Name,  
   }  
   dep.Spec = appsv1.DeploymentSpec{  
      Replicas: &replicas,  
      Template: corev1.PodTemplateSpec{  
         Spec: corev1.PodSpec{  
            Containers: []corev1.Container{  
               {  
                  Name:  "nginx",  
                  Image: "nginx",  
               },  
            },  
         },  
      },  
   }  
   dep.Labels = labels  
   dep.Spec.Template.Labels = labels  
   return dep  
}

4.更新资源 如果我们在尝试检索资源时没有收到错误,则意味着我们能够正确获取资源。因此我们可以检查它的参数并在某些值发生更改时更新它。

代码语言:javascript复制
var size int32 = 2  
if *found.Spec.Replicas != size {  
   found.Spec.Replicas = &size  
   err = r.Update(ctx, found)  
   if err != nil {  
      log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)  
      return ctrl.Result{}, err  
   }  
   // Spec updated - return and requeue  
   return ctrl.Result{Requeue: true}, nil  
}

在我们的示例中,我们将检查 pod 的数量是否仍然等于2。如果情况并非如此,我们将尝试更新资源,并在出现错误时管理错误。 更新生成的文件 一旦我们完成了控制器的更新,执行以下命令非常重要:

代码语言:javascript复制
make manifests

我们之前看到,我们可以找到一些定义控制器 RBAC 权限的注释。所以我们需要执行这个命令来(至少) 生成RBAC定义文件。

Operator构建

现在我们的Operator已准备好使用,我们可以在部署之前构建它。

构建之前

默认情况下,构建的镜像将被命名controller:latest并可以推送到example.com/tmpoperator. 正如您可以想象的那样,它可能会产生一些问题。 因此,如果您想更新这些信息,您必须:

  • 更新Makefile中的IMG变量IMAGE_TAG_BASE
  • 更新config/manager/manager.yaml中的镜像名称

构建Build

要执行构建,请使用此命令

代码语言:javascript复制
make docker-build

如果您想将镜像推送到远程docker注册表,则可以使用此选项

代码语言:javascript复制
make docker-push

部署Deploy

要部署您的Operator,您必须执行 2 个命令: 在集群上部署所有自定义资源定义

代码语言:javascript复制
make install

部署您的Operator

代码语言:javascript复制
make deploy

测试Test

完成上述所有操作后,您可以尝试部署一个实例MyProxy,您应该会看到一个 nginx 部署出现! MyProxy 实例定义示例

代码语言:javascript复制
apiVersion: gateway.example.com/v1alpha1  
kind: MyProxy  
metadata:  
  labels:  
    app.kubernetes.io/name: myproxy  
    app.kubernetes.io/instance: myproxy-sample  
    app.kubernetes.io/part-of: tmpoperator  
    app.kubernetes.io/managed-by: kustomize  
    app.kubernetes.io/created-by: tmpoperator  
  name: myproxy-sample  
spec:  
  name: toto

这部分相当长,但有必要向您展示如何创建一个运算符并看看我们可以用它做什么。

文章翻译 https://blog.devgenius.io/what-is-the-operator-pattern-af8b127b1152

0 人点赞