自动化任务总是有其特殊之处。当我们想要执行某些任务时,我们需要能够对某些特定事件做出反应或被触发。但很多事件无法轻松监听,尤其是在 Kubernetes 集群中。所以今天,我们将看看如何尝试使用Operator来解决它。我们将了解如何创建 Kubernetes Operator!
Operator Pattern 简介
Operator 是 Kubernetes 的软件扩展,它利用自定义资源来管理应用程序及其组件。Operator 遵循 Kubernetes 原则,特别是控制循环。
Operator Pattern是什么?
这种模式允许 Kubernetes
用户创建自己的资源控制器,以便自动管理其应用程序/产品堆栈。
操作员模式使用CRD (自定义资源定义)来促进资源/任务配置。这是来自 Strimzi
的 CRD 示例,它让我们创建一个完整的 Kafka 集群。
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
创建我们的第一个运算符
初始化项目
首先要做的是使用以下命令初始化项目
代码语言:javascript复制Operator-sdk init — 域 [您的域] — 存储库 [您的代码存储库]
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
字段。
type MyProxySpec struct {
Name string `json:"name,omitempty"`
}
重要的 !!该文件用作为您的操作员构建大量 yaml 文件的基础。因此,此文件中的每次修改都执行这两个命令:
make manifests&make generate
控制器Controller
在此文件夹中,您将找到与我们之前生成的自定义资源相关的每个控制器。myproxy_controller.go
此文件夹是操作员可以执行的操作的中心位置。
在每个控制器文件中,您都会发现我们必须更新的两个方法:Reconcile
和SetupWithManager
。
设置管理器
// 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=[动词]
资源组必须只有一个值,但对于资源 name
和verbs
,您可以一次定义多个值,将所有值用 联接起来;。
// 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 的实例)。我们需要它来获取其规格,并能够更新其状态。
// 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
}
- 检索Operator管理的资源
现在我们有了“父”资源,我们要检索“子”资源。在我们的例子中,它是一个部署,它的名称是使用
MyProxy
中的字段定义的Name,并且必须位于test_ns
名称空间中。
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
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
。如果情况并非如此,我们将尝试更新资源,并在出现错误时管理错误。
更新生成的文件
一旦我们完成了控制器的更新,执行以下命令非常重要:
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 实例定义示例
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