WEBKT

手把手教你用 Kubernetes Operator 自动化复杂应用部署?这几个坑你得避开!

65 0 0 0

Kubernetes Operator 是什么神兵利器?为啥大家都想用它?

别急着上手!先搞清楚 Operator 的基本概念

1. CRD (Custom Resource Definition) - 定义应用的“蓝图”

2. Controller - 实现“蓝图”的“工匠”

3. Operator - “蓝图”和“工匠”的完美结合

撸起袖子!手把手教你开发一个简单的 Operator

1. 准备工作

2. 创建项目

3. 定义 CRD

4. 实现 Controller

5. 部署 Operator

6. 创建 Guestbook 资源

7. 验证 Operator

Operator 开发中的常见坑,你踩过几个?

Operator 的未来:无限可能

Kubernetes Operator 是什么神兵利器?为啥大家都想用它?

作为一名身经百战的 Kubernetes 玩家,你肯定遇到过这样的场景:部署一个复杂的应用,光是 YAML 文件就写到手抽筋,更别提后续的升级、维护、故障处理了。每次都像在拆弹,生怕一不小心就炸了。

这时候,Kubernetes Operator 就如同救星般出现了。它可以将应用运维的知识编码到 Kubernetes 中,实现应用的自动化部署、升级、备份、恢复等操作。简单来说,Operator 就是一个“懂行”的机器人,帮你搞定那些繁琐的运维工作。

Operator 的核心思想:将运维人员的专业知识转化为可执行的代码,让 Kubernetes 具备管理复杂应用的能力。

Operator 的优势

  • 自动化:告别手动操作,一键部署、升级、回滚。
  • 标准化:统一应用的管理方式,降低学习成本。
  • 可扩展:轻松应对业务增长带来的挑战。
  • 自愈性:自动检测并修复应用故障,保障高可用。

别急着上手!先搞清楚 Operator 的基本概念

在深入代码之前,咱们先来捋一捋 Operator 的几个关键概念,就像盖房子之前要先打好地基。

1. CRD (Custom Resource Definition) - 定义应用的“蓝图”

Kubernetes 提供了 Deployment、Service 等内置资源对象,但它们并不能满足所有应用的需求。CRD 允许你自定义资源对象,描述应用的各种属性,比如:

  • 数据库的用户名、密码、存储大小
  • 消息队列的 Topic 数量、分区策略
  • 缓存集群的节点数量、内存大小

你可以把 CRD 理解为应用的“蓝图”,它定义了应用的结构和配置。

举个例子

假设你要部署一个 Redis 集群,你可以定义一个名为 RedisCluster 的 CRD,包含以下字段:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: redisclusters.example.com
spec:
group: example.com
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
size:
type: integer
description: Number of Redis instances in the cluster.
memory:
type: string
description: Memory limit for each Redis instance.
scope: Namespaced
names:
plural: redisclusters
singular: rediscluster
kind: RedisCluster
shortNames:
- rc

这个 CRD 定义了一个 RedisCluster 资源,它包含 size (Redis 实例数量) 和 memory (每个实例的内存大小) 两个属性。

2. Controller - 实现“蓝图”的“工匠”

CRD 定义了应用的“蓝图”,Controller 则负责根据“蓝图”创建、更新、删除应用实例。它会持续监听 CRD 资源的变化,并根据期望状态执行相应的操作,最终使应用达到期望状态。

你可以把 Controller 理解为“工匠”,它会读取 CRD 资源,然后创建 Pod、Service 等 Kubernetes 内置资源,最终构建出一个完整的应用。

Controller 的工作流程

  1. 监听:Controller 监听 CRD 资源的变化事件 (创建、更新、删除)。
  2. 对比:Controller 将 CRD 资源中定义的期望状态与当前状态进行对比。
  3. 调谐:如果期望状态与当前状态不一致,Controller 会执行相应的操作,使应用达到期望状态。
  4. 循环:Controller 不断重复上述过程,确保应用始终处于期望状态。

继续上面的例子

当你在 Kubernetes 中创建一个 RedisCluster 资源时,Controller 会读取 sizememory 属性,然后创建相应数量的 Redis Pod,并设置每个 Pod 的内存限制。

3. Operator - “蓝图”和“工匠”的完美结合

Operator 本质上就是一个 Controller,它通过 CRD 扩展 Kubernetes 的 API,实现对特定应用的自动化管理。Operator 将 CRD 和 Controller 组合在一起,形成一个完整的解决方案。

你可以把 Operator 理解为“包工头”,它既能设计“蓝图”,又能指挥“工匠”,最终交付一个完整的应用。

撸起袖子!手把手教你开发一个简单的 Operator

理论知识讲完了,现在咱们来点干货,手把手教你开发一个简单的 Operator。这里我们以 Golang 为例,使用 Kubebuilder 框架来快速搭建 Operator。

1. 准备工作

  • 安装 Kubectl:Kubernetes 命令行工具
  • 安装 Go:Golang 编程语言
  • 安装 Kubebuilder:Operator 开发框架
  • 安装 Docker:容器化平台

2. 创建项目

使用 Kubebuilder 创建一个新的项目:

mkdir my-operator && cd my-operator
kubebuilder init --domain example.com --owner "Your Name"

这个命令会创建一个名为 my-operator 的目录,并初始化项目结构。--domain 参数指定域名,--owner 参数指定作者。

3. 定义 CRD

使用 Kubebuilder 创建一个 CRD:

kubebuilder create api --group apps --version v1alpha1 --kind Guestbook

这个命令会创建一个名为 Guestbook 的 CRD,属于 apps 组,版本为 v1alpha1。Kubebuilder 会自动生成 CRD 的 YAML 文件和 Go 代码。

接下来,你需要编辑 api/v1alpha1/guestbook_types.go 文件,定义 Guestbook 资源的属性:

package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// GuestbookSpec defines the desired state of Guestbook
type GuestbookSpec struct {
// Size is the number of guestbook pods to run
Size int32 `json:"size,omitempty"`
}
// GuestbookStatus defines the observed state of Guestbook
type GuestbookStatus struct {
// Nodes are the names of the guestbook pods that are running Nodes []string `json:"nodes,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// Guestbook is the Schema for the guestbooks API
type Guestbook struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec GuestbookSpec `json:"spec,omitempty"`
Status GuestbookStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// GuestbookList contains a list of Guestbook
type GuestbookList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Guestbook `json:"items"`
}
func init() {
SchemeBuilder.Register(&Guestbook{}, &GuestbookList{})
}

这个代码定义了 Guestbook 资源的 Spec (期望状态) 和 Status (当前状态)。Spec 包含一个 Size 属性,表示 Guestbook Pod 的数量。Status 包含一个 Nodes 属性,表示正在运行的 Guestbook Pod 的名称。

4. 实现 Controller

接下来,你需要编辑 controllers/guestbook_controller.go 文件,实现 Controller 的逻辑:

package controllers
import (
"context"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
appsv1alpha1 "example.com/my-operator/api/v1alpha1"
)
// GuestbookReconciler reconciles a Guestbook object
type GuestbookReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=apps.example.com,resources=guestbooks,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps.example.com,resources=guestbooks/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps.example.com,resources=guestbooks/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 Guestbook 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.14.1/pkg/reconcile
func (r *GuestbookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// TODO(user): your logic here
// 1. Load the Guestbook by name
guestbook := &appsv1alpha1.Guestbook{}
err := r.Get(ctx, req.NamespacedName, guestbook)
if err != nil {
if apierrors.IsNotFound(err) {
// we'll ignore not-found errors, since they can't be fixed by an immediate
return ctrl.Result{}, nil
}
log.Error(err, "unable to fetch Guestbook")
return ctrl.Result{}, err
}
// 2. Check if the Deployment already exists, if not create a new one
deployment := &appsv1.Deployment{}
err = r.Get(ctx, types.NamespacedName{
Name: guestbook.Name,
Namespace: guestbook.Namespace,
}, deployment)
if err != nil {
if apierrors.IsNotFound(err) {
// Define a new Deployment
dep := r.deploymentForGuestbook(guestbook)
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
}
log.Error(err, "Failed to get Deployment")
return ctrl.Result{}, err
}
// 3. Ensure the deployment size is the same as the spec
size := guestbook.Spec.Size
if *deployment.Spec.Replicas != size {
deployment.Spec.Replicas = &size
err = r.Update(ctx, deployment)
if err != nil {
log.Error(err, "Failed to update Deployment", "Deployment.Namespace", deployment.Namespace, "Deployment.Name", deployment.Name)
return ctrl.Result{}, err
}
// Spec updated - return and requeue
return ctrl.Result{Requeue: true}, nil
}
// 4. Update the Guestbook status with the pod names
podList := &corev1.PodList{}
lis := &client.ListOptions{
Namespace: req.Namespace,
}
err = r.List(ctx, podList, lis)
if err != nil {
log.Error(err, "Failed to list pods", "Guestbook.Namespace", req.Namespace)
return ctrl.Result{}, err
}
var podNames []string
for _, pod := range podList.Items {
if pod.OwnerReferences[0].Name == guestbook.Name {
podNames = append(podNames, pod.Name)
}
}
status := appsv1alpha1.GuestbookStatus{
Nodes: podNames,
}
if !reflect.DeepEqual(guestbook.Status, status) {
guestbook.Status = status
err = r.Status().Update(ctx, guestbook)
if err != nil {
log.Error(err, "Failed to update Guestbook status", "Guestbook.Namespace", req.Namespace)
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// deploymentForGuestbook returns a guestbook Deployment object
func (r *GuestbookReconciler) deploymentForGuestbook(guestbook *appsv1alpha1.Guestbook) *appsv1.Deployment {
ls := map[string]string{
"app": "guestbook",
"guestbook_cr": guestbook.Name,
}
replicas := guestbook.Spec.Size
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: guestbook.Name,
Namespace: guestbook.Namespace,
Labels: ls,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: ls,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: ls,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Image: "k8s.gcr.io/echoserver:1.4",
Name: "guestbook",
Ports: []corev1.ContainerPort{{
ContainerPort: 8080,
}},
}},
},
},
},
}
// Set Guestbook instance as the owner and controller
ctrl.SetControllerReference(guestbook, dep, r.Scheme)
return dep
}
// SetupWithManager sets up the controller with the Manager.
func (r *GuestbookReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).For(&appsv1alpha1.Guestbook{}).Owns(&appsv1.Deployment{}).Complete(r)
}

这个代码实现了 Reconcile 函数,它是 Controller 的核心逻辑。Reconcile 函数会:

  1. 获取 Guestbook 资源:根据请求的命名空间和名称获取 Guestbook 资源。
  2. 检查 Deployment 是否存在:如果 Deployment 不存在,则创建一个新的 Deployment。
  3. 更新 Deployment 的副本数:如果 Deployment 的副本数与 Guestbook 资源中定义的 Size 不一致,则更新 Deployment 的副本数。
  4. 更新 Guestbook 状态:更新 Guestbook 资源的状态,包含正在运行的 Pod 的名称。

5. 部署 Operator

执行以下命令,生成 CRD 的 YAML 文件:

make manifests

执行以下命令,安装 CRD:

make install

执行以下命令,构建 Operator 镜像:

make docker-build docker-push IMG="your-docker-repo/my-operator:latest"

your-docker-repo 替换为你的 Docker 仓库地址。

执行以下命令,部署 Operator:

make deploy IMG="your-docker-repo/my-operator:latest"

6. 创建 Guestbook 资源

创建一个名为 guestbook-sample.yaml 的文件,定义一个 Guestbook 资源:

apiVersion: apps.example.com/v1alpha1
kind: Guestbook
metadata:
name: guestbook-sample
spec:
size: 3

这个文件定义了一个名为 guestbook-sampleGuestbook 资源,包含 3 个 Pod。

执行以下命令,创建 Guestbook 资源:

kubectl apply -f config/samples/apps_v1alpha1_guestbook.yaml

7. 验证 Operator

执行以下命令,查看 Guestbook 资源的状态:

kubectl get guestbooks

执行以下命令,查看 Deployment 的状态:

kubectl get deployments

你应该能看到一个名为 guestbook-sample 的 Deployment,包含 3 个 Pod。

Operator 开发中的常见坑,你踩过几个?

Operator 开发虽然强大,但也充满了挑战。以下是一些常见的坑,希望能帮你避开:

  1. CRD 设计不合理:CRD 定义了应用的模型,如果设计不合理,会导致 Operator 难以扩展和维护。在设计 CRD 时,要充分考虑应用的各种属性和关系,并遵循 Kubernetes 的设计原则。

  2. Controller 逻辑复杂:Controller 负责实现应用的自动化管理,如果逻辑过于复杂,会导致 Operator 难以理解和调试。在实现 Controller 时,要尽量保持逻辑简洁清晰,并编写充分的单元测试。

  3. 状态同步问题:Controller 需要不断同步应用的状态,如果状态同步出现问题,会导致 Operator 无法正确管理应用。在实现 Controller 时,要仔细考虑各种异常情况,并采取合适的策略来保证状态同步的正确性。

  4. 权限管理问题:Operator 需要访问 Kubernetes API,如果权限配置不当,会导致 Operator 无法正常工作。在部署 Operator 时,要仔细配置 RBAC 权限,确保 Operator 具有足够的权限来访问 Kubernetes API。

  5. 升级策略问题:Operator 需要支持应用的升级,如果升级策略不合理,会导致应用升级失败或数据丢失。在设计 Operator 时,要仔细考虑应用的升级策略,并编写相应的升级代码。

Operator 的未来:无限可能

Kubernetes Operator 正在成为云原生应用管理的主流方式。随着 Kubernetes 的不断发展,Operator 的应用场景也将越来越广泛。未来,我们可以期待 Operator 在以下方面发挥更大的作用:

  • AI 赋能:利用 AI 技术,实现 Operator 的智能化,例如自动优化应用配置、预测应用故障等。
  • 多云支持:扩展 Operator 的能力,使其能够管理跨多个云平台的应用。
  • Serverless 集成:将 Operator 与 Serverless 技术结合,实现应用的弹性伸缩和按需付费。

掌握 Kubernetes Operator 开发技术,将让你在云原生时代更具竞争力!

云原生老司机 Kubernetes Operator自动化部署CRD Controller

评论点评

打赏赞助
sponsor

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

分享

QRcode

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