操作员就像那些总是知道该做什么的优秀同事。它们扩展了 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 始终健康,您的集群永远可扩展!