操作员就像那些总是知道该做什么的优秀同事。它们扩展了 Kubernetes 的功能,使您能够自动化管理复杂的应用程序。可以把它们想象成您的个人应用程序保姆,时刻关注状态,在需要时进行更改,并确保一切顺利运行。

Kubernetes Operator SDK:您的新好朋友

现在,您可能会想,“太好了,又一个需要学习的工具。”但请稍等!Kubernetes Operator SDK 就像操作员开发的瑞士军刀(但更酷且不那么陈词滥调)。它是一个简化创建、测试和维护操作员过程的工具包。

使用 Operator SDK,您可以:

  • 比说“Java 运行时异常”更快地搭建您的操作员项目
  • 生成样板代码(因为谁有时间做这些呢?)
  • 测试您的操作员而不必牺牲一个集群给演示之神
  • 轻松打包和部署您的操作员

何时为您的 Java 应用程序定制操作员

说实话,有些 Java 应用程序就像那个在 2023 年仍坚持使用翻盖手机的朋友——它们很特别,需要额外的关注。当您需要以下情况时,可能需要一个自定义操作员:

  • 您的应用程序配置比您上一次的关系更复杂
  • 部署和更新需要火箭科学博士学位
  • 您需要让拉斯维加斯赌场嫉妒的故障转移策略
  • 管理依赖项感觉就像在赶猫

入门:Operator SDK 和 Java,Kubernetes 天作之合

好了,让我们卷起袖子,动手实践。首先,我们需要设置开发环境:

为您的自定义资源生成 API:


operator-sdk create api --group=app --version=v1alpha1 --kind=QuarkusApp

创建一个新的操作员项目:


mkdir quarkus-operator
cd quarkus-operator
operator-sdk init --domain=example.com --repo=github.com/example/quarkus-operator

安装 Operator SDK(因为没有工具就没有魔法):


# 对于 macOS 用户(假设您有 Homebrew)
brew install operator-sdk

# 对于勇敢使用 Linux 的人
curl -LO https://github.com/operator-framework/operator-sdk/releases/latest/download/operator-sdk_linux_amd64
chmod +x operator-sdk_linux_amd64
sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk

恭喜!您刚刚为您的 Quarkus 应用程序操作员奠定了基础。这就像种下一颗种子,只不过这颗种子会成长为一个成熟的应用程序管理系统。

制作您的自定义操作员:有趣的部分

现在我们已经设置好了项目,是时候添加一些真正的魔法了。我们将创建一个自定义资源定义 (CRD),描述我们的 Quarkus 应用程序的独特属性,以及一个控制器来管理其生命周期。

首先,让我们定义我们的 CRD。打开文件 api/v1alpha1/quarkusapp_types.go 并添加一些字段:


type QuarkusAppSpec struct {
	// 插入其他规格字段
	Image string `json:"image"`
	Replicas int32 `json:"replicas"`
	ConfigMap string `json:"configMap,omitempty"`
}

type QuarkusAppStatus struct {
	// 插入其他状态字段
	Nodes []string `json:"nodes"`
}

现在,让我们实现控制器逻辑。打开 controllers/quarkusapp_controller.go 并在 Reconcile 函数中添加一些内容:


func (r *QuarkusAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := r.Log.WithValues("quarkusapp", req.NamespacedName)

	// 获取 QuarkusApp 实例
	quarkusApp := &appv1alpha1.QuarkusApp{}
	err := r.Get(ctx, req.NamespacedName, quarkusApp)
	if err != nil {
		if errors.IsNotFound(err) {
			// 请求对象未找到,可能在协调请求后被删除。
			// 返回并不重新排队
			log.Info("QuarkusApp 资源未找到。忽略,因为对象必须被删除")
			return ctrl.Result{}, nil
		}
		// 读取对象时出错 - 重新排队请求。
		log.Error(err, "获取 QuarkusApp 失败")
		return ctrl.Result{}, err
	}

	// 检查部署是否已存在,如果不存在则创建一个新的
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: quarkusApp.Name, Namespace: quarkusApp.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// 定义一个新的部署
		dep := r.deploymentForQuarkusApp(quarkusApp)
		log.Info("创建新部署", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		err = r.Create(ctx, dep)
		if err != nil {
			log.Error(err, "创建新部署失败", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return ctrl.Result{}, err
		}
		// 部署成功创建 - 返回并重新排队
		return ctrl.Result{Requeue: true}, nil
	} else if err != nil {
		log.Error(err, "获取部署失败")
		return ctrl.Result{}, err
	}

	// 确保部署大小与规格相同
	size := quarkusApp.Spec.Replicas
	if *found.Spec.Replicas != size {
		found.Spec.Replicas = &size
		err = r.Update(ctx, found)
		if err != nil {
			log.Error(err, "更新部署失败", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
			return ctrl.Result{}, err
		}
		// 规格更新 - 返回并重新排队
		return ctrl.Result{Requeue: true}, nil
	}

	// 使用 pod 名称更新 QuarkusApp 状态
	// 列出此 QuarkusApp 部署的 pod
	podList := &corev1.PodList{}
	listOpts := []client.ListOption{
		client.InNamespace(quarkusApp.Namespace),
		client.MatchingLabels(labelsForQuarkusApp(quarkusApp.Name)),
	}
	if err = r.List(ctx, podList, listOpts...); err != nil {
		log.Error(err, "列出 pod 失败", "QuarkusApp.Namespace", quarkusApp.Namespace, "QuarkusApp.Name", quarkusApp.Name)
		return ctrl.Result{}, err
	}
	podNames := getPodNames(podList.Items)

	// 如果需要,更新 status.Nodes
	if !reflect.DeepEqual(podNames, quarkusApp.Status.Nodes) {
		quarkusApp.Status.Nodes = podNames
		err := r.Status().Update(ctx, quarkusApp)
		if err != nil {
			log.Error(err, "更新 QuarkusApp 状态失败")
			return ctrl.Result{}, err
		}
	}

	return ctrl.Result{}, nil
}

此控制器将为我们的 Quarkus 应用程序创建一个部署,确保副本数量与规格匹配,并使用 pod 名称列表更新状态。

让您的操作员坚不可摧

现在我们有了一个基本的操作员,让我们添加一些超能力,使其具有弹性和自愈能力。我们将实现基于应用程序状态的自动恢复和扩展。

将此添加到您的控制器中:


func (r *QuarkusAppReconciler) checkAndHeal(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// 检查 pod 的健康状况
	podList := &corev1.PodList{}
	listOpts := []client.ListOption{
		client.InNamespace(quarkusApp.Namespace),
		client.MatchingLabels(labelsForQuarkusApp(quarkusApp.Name)),
	}
	if err := r.List(ctx, podList, listOpts...); err != nil {
		return err
	}

	unhealthyPods := 0
	for _, pod := range podList.Items {
		if pod.Status.Phase != corev1.PodRunning {
			unhealthyPods++
		}
	}

	// 如果超过 50% 的 pod 不健康,则触发滚动重启
	if float32(unhealthyPods)/float32(len(podList.Items)) > 0.5 {
		deployment := &appsv1.Deployment{}
		err := r.Get(ctx, types.NamespacedName{Name: quarkusApp.Name, Namespace: quarkusApp.Namespace}, deployment)
		if err != nil {
			return err
		}

		// 通过更新注释触发滚动重启
		if deployment.Spec.Template.Annotations == nil {
			deployment.Spec.Template.Annotations = make(map[string]string)
		}
		deployment.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)

		err = r.Update(ctx, deployment)
		if err != nil {
			return err
		}
	}

	return nil
}

不要忘记在您的 Reconcile 循环中调用此函数:


if err := r.checkAndHeal(ctx, quarkusApp); err != nil {
	log.Error(err, "修复 QuarkusApp 失败")
	return ctrl.Result{}, err
}

自动化更新:因为谁有时间手动操作?

让我们添加一些自动化魔法来处理更新。我们将创建一个函数来检查我们的 Quarkus 应用程序的新版本,并在需要时触发更新:


func (r *QuarkusAppReconciler) checkAndUpdate(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// 在现实世界中,您会检查外部来源以获取最新版本
	// 在此示例中,我们将使用 CR 上的注释来模拟新版本
	newVersion, exists := quarkusApp.Annotations["newVersion"]
	if !exists {
		return nil // 没有新版本可用
	}

	deployment := &appsv1.Deployment{}
	err := r.Get(ctx, types.NamespacedName{Name: quarkusApp.Name, Namespace: quarkusApp.Namespace}, deployment)
	if err != nil {
		return err
	}

	// 将镜像更新为新版本
	for i, container := range deployment.Spec.Template.Spec.Containers {
		if container.Name == quarkusApp.Name {
			deployment.Spec.Template.Spec.Containers[i].Image = newVersion
			break
		}
	}

	// 更新部署
	err = r.Update(ctx, deployment)
	if err != nil {
		return err
	}

	// 删除注释以防止持续更新
	delete(quarkusApp.Annotations, "newVersion")
	return r.Update(ctx, quarkusApp)
}

再次,在您的 Reconcile 循环中调用此函数:


if err := r.checkAndUpdate(ctx, quarkusApp); err != nil {
	log.Error(err, "更新 QuarkusApp 失败")
	return ctrl.Result{}, err
}

与外部资源集成:因为没有应用程序是孤岛

大多数 Quarkus 应用程序需要与外部资源(如数据库或缓存)交互。让我们添加一些逻辑来管理这些依赖项:


func (r *QuarkusAppReconciler) ensureDatabaseExists(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// 检查 CR 中是否指定了数据库
	if quarkusApp.Spec.Database == "" {
		return nil // 不需要数据库
	}

	// 检查数据库是否存在
	database := &v1alpha1.Database{}
	err := r.Get(ctx, types.NamespacedName{Name: quarkusApp.Spec.Database, Namespace: quarkusApp.Namespace}, database)
	if err != nil && errors.IsNotFound(err) {
		// 数据库不存在,让我们创建它
		newDB := &v1alpha1.Database{
			ObjectMeta: metav1.ObjectMeta{
				Name:      quarkusApp.Spec.Database,
				Namespace: quarkusApp.Namespace,
			},
			Spec: v1alpha1.DatabaseSpec{
				Engine:  "postgres",
				Version: "12",
			},
		}
		err = r.Create(ctx, newDB)
		if err != nil {
			return err
		}
	} else if err != nil {
		return err
	}

	// 数据库存在,确保我们的应用程序具有正确的连接信息
	secret := &corev1.Secret{}
	err = r.Get(ctx, types.NamespacedName{Name: database.Status.CredentialsSecret, Namespace: quarkusApp.Namespace}, secret)
	if err != nil {
		return err
	}

	// 使用数据库连接信息更新 Quarkus 应用程序的环境变量
	deployment := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: quarkusApp.Name, Namespace: quarkusApp.Namespace}, deployment)
	if err != nil {
		return err
	}

	envVars := []corev1.EnvVar{
		{
			Name: "DB_URL",
			Value: fmt.Sprintf("jdbc:postgresql://%s:%d/%s",
				database.Status.Host,
				database.Status.Port,
				database.Status.Database),
		},
		{
			Name: "DB_USER",
			ValueFrom: &corev1.EnvVarSource{
				SecretKeyRef: &corev1.SecretKeySelector{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: secret.Name,
					},
					Key: "username",
				},
			},
		},
		{
			Name: "DB_PASSWORD",
			ValueFrom: &corev1.EnvVarSource{
				SecretKeyRef: &corev1.SecretKeySelector{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: secret.Name,
					},
					Key: "password",
				},
			},
		},
	}

	// 更新部署的环境变量
	for i, container := range deployment.Spec.Template.Spec.Containers {
		if container.Name == quarkusApp.Name {
			deployment.Spec.Template.Spec.Containers[i].Env = append(container.Env, envVars...)
			break
		}
	}

	return r.Update(ctx, deployment)
}

不要忘记在您的 Reconcile 循环中也调用此函数!

监控和日志记录:因为盲目飞行不好玩

为了监控我们的操作员和 Quarkus 应用程序,让我们添加一些监控和日志记录功能。我们将使用 Prometheus 进行指标监控,并与 Kubernetes 日志系统集成。

首先,让我们为我们的操作员添加一些指标。将此添加到您的控制器中:


var (
	reconcileCount = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quarkusapp_reconcile_total",
			Help: "每个 QuarkusApp 的协调总数",
		},
		[]string{"quarkusapp"},
	)
	reconcileErrors = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quarkusapp_reconcile_errors_total",
			Help: "每个 QuarkusApp 的协调错误总数",
		},
		[]string{"quarkusapp"},
	)
)

func init() {
	metrics.Registry.MustRegister(reconcileCount, reconcileErrors)
}

现在,更新您的 Reconcile 函数以使用这些指标:


func (r *QuarkusAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := r.Log.WithValues("quarkusapp", req.NamespacedName)

	// 增加协调计数
	reconcileCount.WithLabelValues(req.NamespacedName.String()).Inc()

	// ... 其余的协调逻辑 ...

	if err != nil {
		// 增加错误计数
		reconcileErrors.WithLabelValues(req.NamespacedName.String()).Inc()
		log.Error(err, "协调失败")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

对于日志记录,我们已经在使用 controller-runtime 的日志记录器。让我们添加一些更详细的日志记录:


log.Info("开始协调", "QuarkusApp", quarkusApp.Name)

// ... 在检查和修复之后 ...
log.Info("健康检查完成", "UnhealthyPods", unhealthyPods)

// ... 在更新之后 ...
log.Info("更新检查完成", "NewVersion", newVersion)

// ... 在确保数据库存在之后 ...
log.Info("数据库检查完成", "Database", quarkusApp.Spec.Database)

log.Info("协调成功完成", "QuarkusApp", quarkusApp.Name)

总结:您现在是 Kubernetes 操作员大师!

恭喜!您刚刚为您的古怪 Quarkus 应用程序创建了一个自定义 Kubernetes 操作员。让我们回顾一下我们完成的工作:

  • 使用 Kubernetes Operator SDK 设置项目
  • 为我们的 Quarkus 应用程序创建自定义资源定义
  • 实现控制器以管理应用程序的生命周期
  • 添加自愈和自动更新功能
  • 与外部资源(如数据库)集成
  • 为我们的操作员设置监控和日志记录

请记住,能力越大,责任越大。您的自定义操作员现在负责管理您的 Quarkus 应用程序,因此在将其投入生产集群之前,请务必彻底测试。

随着您继续探索 Kubernetes 操作员的世界,请继续探索和实验。可能性是无穷无尽的,谁知道呢?您可能会创造出云原生应用程序管理领域的下一个大事件。

现在,带着信心去操作吧,您这位出色的 Kubernetes 大师!

“在 Kubernetes 的世界里,操作员是魔杖,而你,我的朋友,是巫师。” - 如果邓布利多是一名 DevOps 工程师,他可能会这么说

祝编码愉快,愿您的 pod 始终健康,您的集群永远可扩展!