WEBKT

告别YAML地狱?深入剖析Kubernetes Operator设计模式与最佳实践

50 0 0 0

告别YAML地狱?深入剖析Kubernetes Operator设计模式与最佳实践

1. Operator 解决了什么问题?

2. Operator 的核心概念

3. Operator 的设计模式

4. 如何构建一个 Operator?

5. Operator 的最佳实践

6. Operator 的未来展望

7. 总结

告别YAML地狱?深入剖析Kubernetes Operator设计模式与最佳实践

作为一名每天与Kubernetes打交道的开发者,你是否也曾被无穷无尽的YAML配置折磨得死去活来?手动维护这些配置文件,不仅容易出错,而且难以扩展和自动化。有没有一种方法可以像管理原生Kubernetes资源一样,轻松管理复杂的应用呢?答案就是 Kubernetes Operator

Operator是一种扩展Kubernetes API的方式,它可以让你将特定应用程序的管理知识封装到可重用的代码中。简单来说,Operator就是Kubernetes的“应用管理员”,它知道如何部署、配置、升级和维护你的应用程序,而你只需要告诉它你的意图即可。

本文将深入剖析Kubernetes Operator的设计模式和最佳实践,并以有状态应用为例,手把手教你如何构建自己的Operator,告别YAML地狱,拥抱自动化运维的未来。

1. Operator 解决了什么问题?

在深入了解Operator的细节之前,我们先来回顾一下传统Kubernetes部署方式的痛点:

  • 手动配置繁琐: 部署一个复杂的应用,需要编写大量的YAML文件,包括Deployment、Service、ConfigMap等,配置过程繁琐且容易出错。
  • 运维操作复杂: 应用的升级、扩容、备份、恢复等运维操作,需要手动执行一系列kubectl命令,耗时且容易出错。
  • 缺乏自动化: 无法实现应用的自动化部署、配置和运维,依赖人工干预,效率低下。
  • 状态管理困难: 对于有状态应用,如数据库、消息队列等,状态管理更加复杂,需要考虑数据一致性、备份恢复等问题。

Operator的出现,正是为了解决这些痛点。它可以将应用程序的管理知识封装到代码中,实现自动化部署、配置和运维,简化应用管理,提高效率,并降低出错率。

2. Operator 的核心概念

要理解Operator的工作原理,需要掌握以下几个核心概念:

  • Custom Resource Definition (CRD): CRD是Kubernetes提供的一种扩展API的方式,可以定义新的资源类型。Operator通过CRD来定义应用程序的自定义资源,例如MyApp
  • Custom Resource (CR): CR是CRD定义的资源的一个实例。例如,你可以创建一个MyApp的CR,指定应用程序的名称、版本、配置等。
  • Controller: Controller是Operator的核心组件,它负责监听CR的变化,并根据CR的定义,执行相应的操作,例如创建Deployment、Service等。
  • Reconcile Loop: Controller通过Reconcile Loop来确保应用程序的状态与CR的定义一致。Reconcile Loop会定期检查应用程序的状态,如果发现不一致,就会执行相应的操作,使其恢复到期望的状态。

简单来说,Operator的工作流程如下:

  1. 用户通过kubectl创建或更新CR。
  2. Controller监听到CR的变化。
  3. Controller执行Reconcile Loop,根据CR的定义,创建或更新Kubernetes资源。
  4. Controller持续监控应用程序的状态,并进行必要的调整,以确保应用程序的状态与CR的定义一致。

3. Operator 的设计模式

Operator的设计模式主要分为以下几种:

  • Level-Based Reconciling: 这是最常用的设计模式,Controller会定期检查应用程序的状态,并将其与CR的定义进行比较,如果发现不一致,就会执行相应的操作,使其恢复到期望的状态。这种模式简单易懂,适用于大多数场景。
  • Event-Based Reconciling: 这种模式下,Controller会监听Kubernetes事件,例如Pod的创建、删除、更新等,并根据事件的类型,执行相应的操作。这种模式可以更快地响应应用程序状态的变化,适用于对实时性要求较高的场景。
  • State Machine: 这种模式下,Operator会维护一个状态机,用于描述应用程序的生命周期。Controller会根据应用程序的状态,执行相应的操作,并更新状态机。这种模式适用于复杂的应用程序,可以更好地管理应用程序的状态。

4. 如何构建一个 Operator?

构建Operator的工具有很多,例如Operator Framework、kubebuilder等。本文以Operator Framework为例,介绍如何构建一个Operator。

4.1 安装 Operator Framework

首先,需要安装Operator Framework CLI工具:

# 下载 Operator Framework CLI
curl -Lo ./operator-sdk https://github.com/operator-framework/operator-sdk/releases/download/v1.28.0/operator-sdk_linux_amd64
# 赋予执行权限
chmod +x ./operator-sdk
# 移动到 /usr/local/bin 目录下
sudo mv ./operator-sdk /usr/local/bin/
# 验证安装
operator-sdk version

4.2 创建 Operator 项目

使用operator-sdk init命令创建一个新的Operator项目:

operator-sdk init --domain example.com --repo github.com/example/my-app-operator
  • --domain: 指定Operator的域名,用于生成CRD的Group。
  • --repo: 指定Operator的代码仓库地址。

4.3 创建 CRD

使用operator-sdk create api命令创建一个新的CRD:

operator-sdk create api --group apps --version v1alpha1 --kind MyApp --resource --controller
  • --group: 指定CRD的Group。
  • --version: 指定CRD的版本。
  • --kind: 指定CRD的Kind,也就是资源类型。
  • --resource: 创建CRD的YAML文件。
  • --controller: 创建Controller的代码文件。

执行完以上命令后,Operator Framework会自动生成CRD的YAML文件和Controller的代码文件。

4.4 定义 CRD 的 Spec 和 Status

打开api/v1alpha1/myapp_types.go文件,定义CRD的Spec和Status:

package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// MyAppSpec defines the desired state of MyApp
type MyAppSpec struct {
// Size is the desired size of the MyApp
Size int32 `json:"size"`
// Image is the image to use for the MyApp
Image string `json:"image"`
}
// MyAppStatus defines the observed state of MyApp
type MyAppStatus struct {
// Nodes are the names of the nodes where the MyApp is running
Nodes []string `json:"nodes"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// MyApp is the Schema for the myapps API
type MyApp struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyAppSpec `json:"spec,omitempty"`
Status MyAppStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// MyAppList contains a list of MyApp
type MyAppList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []MyApp `json:"items"`
}
func init() {
SchemeBuilder.Register(&MyApp{}, &MyAppList{})
}
  • MyAppSpec: 定义应用程序的期望状态,包括SizeImage
  • MyAppStatus: 定义应用程序的观测状态,包括Nodes

4.5 实现 Controller 的 Reconcile 函数

打开controllers/myapp_controller.go文件,实现Controller的Reconcile函数:

package controllers
import (
"context"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
appsv1alpha1 "github.com/example/my-app-operator/api/v1alpha1"
)
// MyAppReconciler reconciles a MyApp object
type MyAppReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=apps.example.com,resources=myapps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps.example.com,resources=myapps/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps.example.com,resources=myapps/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the MyApp object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.0/pkg/reconcile
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx)
// TODO(user): your logic here
// Fetch the MyApp instance
myApp := &appsv1alpha1.MyApp{}
err := r.Get(ctx, req.NamespacedName, myApp)
if err != nil {
if errors.IsNotFound(err) {
l.Info("MyApp resource not found. Ignoring since object must be deleted")
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (another reconciliation loop will start when the object is deleted)
return ctrl.Result{}, nil
}
l.Error(err, "Failed to get MyApp")
// we'll requeue the event to be processed later
return ctrl.Result{}, err
}
// Define a new Deployment object
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: myApp.Name + "-deployment",
Namespace: myApp.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: &myApp.Spec.Size,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": myApp.Name,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": myApp.Name,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "my-app",
Image: myApp.Spec.Image,
Ports: []corev1.ContainerPort{
{
ContainerPort: 8080,
},
},
},
},
},
},
},
}
// Set the owner reference so that the Deployment is deleted when the MyApp is deleted
ctrl.SetControllerReference(myApp, deployment, r.Scheme)
// Check if the Deployment already exists
existingDeployment := &appsv1.Deployment{}
err = r.Get(ctx, client.ObjectKey{Name: deployment.Name, Namespace: deployment.Namespace}, existingDeployment)
if err != nil {
if errors.IsNotFound(err) {
l.Info("Creating a new Deployment", "Deployment.Namespace", deployment.Namespace, "Deployment.Name", deployment.Name)
err = r.Create(ctx, deployment)
if err != nil {
l.Error(err, "Failed to create new Deployment", "Deployment.Namespace", deployment.Namespace, "Deployment.Name", deployment.Name)
return ctrl.Result{}, err
}
// Deployment created successfully - return and requeue
return ctrl.Result{Requeue: true}, nil
}
l.Error(err, "Failed to get Deployment")
return ctrl.Result{}, err
}
// Ensure the deployment size is the same as the spec
size := myApp.Spec.Size
if *existingDeployment.Spec.Replicas != size {
l.Info("Updating Deployment replicas", "Deployment.Namespace", existingDeployment.Namespace, "Deployment.Name", existingDeployment.Name, "Replicas", size)
existingDeployment.Spec.Replicas = &size
err = r.Update(ctx, existingDeployment)
if err != nil {
l.Error(err, "Failed to update Deployment", "Deployment.Namespace", existingDeployment.Namespace, "Deployment.Name", existingDeployment.Name)
return ctrl.Result{}, err
}
// Spec updated - return and requeue
return ctrl.Result{Requeue: true}, nil
}
// Update the MyApp status with the pod names
podList := &corev1.PodList{}
listOpts := []client.ListOption{
client.InNamespace(myApp.Namespace),
client.MatchingLabels(map[string]string{"app": myApp.Name}),
}
if err = r.List(ctx, podList, listOpts...); err != nil {
l.Error(err, "Failed to list pods", "MyApp.Namespace", myApp.Namespace, "MyApp.Name", myApp.Name)
return ctrl.Result{}, err
}
podNames := make([]string, len(podList.Items))
for i, pod := range podList.Items {
podNames[i] = pod.Name
}
if !reflect.DeepEqual(podNames, myApp.Status.Nodes) {
myApp.Status.Nodes = podNames
err := r.Status().Update(ctx, myApp)
if err != nil {
l.Error(err, "Failed to update MyApp status")
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Owns(&appsv1.Deployment{}).
For(&appsv1alpha1.MyApp{}).
Complete(r)
}

Reconcile函数的主要逻辑如下:

  1. 获取MyApp实例。
  2. 定义一个新的Deployment对象。
  3. 设置Deployment的owner reference,以便在MyApp被删除时,Deployment也会被删除。
  4. 检查Deployment是否已经存在,如果不存在,则创建一个新的Deployment。
  5. 确保Deployment的副本数与MyApp的spec一致。
  6. 更新MyApp的status,包含Pod的名称。

4.6 构建和部署 Operator

使用make命令构建Operator:

make

然后,使用make install命令安装CRD:

make install

最后,使用make deploy命令部署Operator:

make deploy

4.7 测试 Operator

创建一个MyApp的CR:

apiVersion: apps.example.com/v1alpha1
kind: MyApp
metadata:
name: my-app
spec:
size: 3
image: nginx:latest

使用kubectl apply命令创建CR:

kubectl apply -f config/samples/apps_v1alpha1_myapp.yaml

查看Deployment是否创建成功:

kubectl get deployments

查看Pod是否运行正常:

kubectl get pods

5. Operator 的最佳实践

  • 使用Operator Framework或kubebuilder等工具: 这些工具可以帮助你快速构建Operator,并提供一些常用的功能,例如代码生成、测试、部署等。
  • 将应用程序的管理知识封装到代码中: Operator的核心价值在于将应用程序的管理知识封装到代码中,实现自动化部署、配置和运维。
  • 使用Level-Based Reconciling模式: 这种模式简单易懂,适用于大多数场景。
  • 编写完善的测试用例: 测试用例可以确保Operator的正确性,并提高代码的质量。
  • 监控Operator的运行状态: 监控可以帮助你及时发现Operator的问题,并进行修复。

6. Operator 的未来展望

Operator是Kubernetes生态系统中一个重要的组成部分,它可以帮助开发者简化应用程序的管理,提高效率,并降低出错率。随着Kubernetes的不断发展,Operator的应用场景也将越来越广泛。

未来,Operator将朝着以下几个方向发展:

  • 更加智能化: Operator将能够根据应用程序的运行状态,自动进行调整,例如自动扩容、自动备份等。
  • 更加通用化: Operator将能够管理各种类型的应用程序,例如数据库、消息队列、Web应用等。
  • 更加易用化: Operator的构建和部署将更加简单,开发者可以更加容易地创建自己的Operator。

7. 总结

Kubernetes Operator是一种强大的工具,它可以帮助开发者简化应用程序的管理,提高效率,并降低出错率。如果你正在使用Kubernetes,那么Operator绝对值得你学习和使用。

希望本文能够帮助你更好地理解Kubernetes Operator的设计模式和最佳实践,并能够帮助你构建自己的Operator,告别YAML地狱,拥抱自动化运维的未来。

参考资料:

YAML终结者 Kubernetes OperatorCRD自动化运维

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9792