Kubernetes Operator 开发
Kubernetes Operator 开发
什么是 Operator
Kubernetes 原生资源(如 Deployment、Service、ConfigMap)提供了通用的应用编排能力。但对于有状态的复杂应用——如数据库集群、消息队列、分布式缓存——这些通用资源远远不够。部署一个 PostgreSQL 高可用集群需要:
- 初始化主节点和多个从节点
- 配置流复制和自动故障转移
- 管理备份和恢复
- 处理滚动升级
- 扩缩容时重新平衡数据分片
这些操作需要深厚的领域知识(如 DBA 经验),且每个步骤都有大量的边界情况需要处理。传统的做法是编写运维脚本或使用 Helm Chart,但 Helm Chart 只能处理"部署"这一步,无法持续维护应用的运行状态。
Operator 模式是解决这个问题的 Kubernetes 原生方案。它的核心思想是:
将领域专家的运维知识编码为软件,让 Kubernetes 像管理原生资源一样管理复杂应用。
Operator 由两部分组成:
- CRD(Custom Resource Definition):扩展 Kubernetes API,定义新的资源类型。例如定义一个
PostgresCluster资源,用户可以用 YAML 声明"我需要一个 3 节点的 PostgreSQL 集群" - Controller(控制器):持续运行的守护进程,监听 CRD 资源的变化,不断将期望状态(YAML 中定义的)同步到实际状态(集群中实际运行的)
Operator 成熟度模型
CNCF 定义了 Operator 的五级成熟度模型:
| 级别 | 名称 | 能力 |
|---|---|---|
| L1 | 基本安装 | 自动安装应用 |
| L2 | 生命周期管理 | 支持升级、备份、恢复 |
| L3 | 全自动运维 | 自动扩缩容、故障转移、自动修复 |
| L4 | 深度洞察 | 集成监控、告警、指标采集 |
| L5 | 自动伸缩 | 基于负载自动调整资源和规模 |
CRD 定义
CRD 结构
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: webapps.apps.example.com
spec:
group: apps.example.com # API Group
versions:
- name: v1 # API 版本
served: true # 是否提供 API 服务
storage: true # 是否为存储版本
schema:
openAPIV3Schema:
type: object
required: ["spec"]
properties:
spec:
type: object
required: ["image", "replicas"]
properties:
image:
type: string
description: "容器镜像地址"
pattern: '^.*:.*$' # 正则验证:必须包含 tag
replicas:
type: integer
minimum: 1
maximum: 100
description: "副本数量"
dbRef:
type: string
description: "关联的数据库引用"
resources:
type: object
properties:
cpu:
type: string
memory:
type: string
status:
type: object
properties:
ready:
type: boolean
replicas:
type: integer
message:
type: string
conditions:
type: array
items:
type: object
properties:
type:
type: string
status:
type: string
lastTransitionTime:
type: string
format: date-time
reason:
type: string
# 启用子资源
subresources:
status: {} # 支持 /status 子资源
scale: # 支持 /scale 子资源
specReplicasPath: .spec.replicas
statusReplicasPath: .status.replicas
# 打印列(kubectl get 时显示)
additionalPrinterColumns:
- name: Ready
type: string
jsonPath: .status.ready
- name: Replicas
type: integer
jsonPath: .spec.replicas
- name: Image
type: string
jsonPath: .spec.image
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
scope: Namespaced # 作用域:Namespaced 或 Cluster
names:
plural: webapps # 复数形式
singular: webapp # 单数形式
kind: WebApp # 资源类型名
shortNames:
- wa # 简写CRD 实例
# 定义 CRD 后,用户按此 YAML 创建资源
apiVersion: apps.example.com/v1
kind: WebApp
metadata:
name: my-webapp
namespace: production
spec:
image: registry.example.com/web:v2.1.0
replicas: 3
dbRef: postgres-cluster
resources:
cpu: "1"
memory: 512Mi# 操作自定义资源
kubectl get webapps -n production
kubectl describe webapp my-webapp -n production
kubectl delete webapp my-webapp -n production
# 查看 CRD 的 API 结构
kubectl explain webapp.spec
kubectl explain webapp.spec.image控制器开发(Go + Kubebuilder)
项目初始化
# 安装 kubebuilder
go install sigs.k8s.io/kubebuilder/cmd/kubebuilder@latest
# 创建项目
kubebuilder init --domain example.com --repo mydomain/webapp-operator
# 创建 API(CRD + Controller)
kubebuilder create api --group apps --version v1 --kind WebApp
# 项目结构
webapp-operator/
├── api/v1/
│ ├── webapp_types.go # CRD 结构定义
│ ├── webapp_webhook.go # Admission Webhook(校验和默认值)
│ ├── groupversion_info.go
│ └── zz_generated.deepcopy.go
├── controllers/
│ └── webapp_controller.go # 控制器逻辑
├── config/
│ ├── crd/ # CRD YAML
│ ├── rbac/ # RBAC 配置
│ ├── manager/ # Operator 部署配置
│ └── samples/ # 示例 YAML
├── Dockerfile
├── Makefile
└── main.goCRD 类型定义
// api/v1/webapp_types.go
package v1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// WebAppSpec 定义期望状态
type WebAppSpec struct {
Image string `json:"image"`
Replicas int32 `json:"replicas"`
DBRef string `json:"dbRef,omitempty"`
Resources *ContainerResources `json:"resources,omitempty"`
}
type ContainerResources struct {
CPU string `json:"cpu,omitempty"`
Memory string `json:"memory,omitempty"`
}
// WebAppStatus 定义实际状态
type WebAppStatus struct {
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
Ready bool `json:"ready,omitempty"`
Message string `json:"message,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.readyReplicas
// +kubebuilder:printcolumn:name="Ready",type="string",jsonpath=".status.ready"
// +kubebuilder:printcolumn:name="Replicas",type="integer",jsonpath=".spec.replicas"
// +kubebuilder:printcolumn:name="Age",type="date",jsonpath=".metadata.creationTimestamp"
type WebApp struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec WebAppSpec `json:"spec"`
Status WebAppStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type WebAppList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []WebApp `json:"items"`
}控制器调谐逻辑
// controllers/webapp_controller.go
package controllers
import (
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
webappv1 "mydomain/webapp-operator/api/v1"
)
// WebAppReconciler 调谐 WebApp 资源
type WebAppReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=apps.example.com,resources=webapps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps.example.com,resources=webapps/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps.example.com,resources=webapps/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
func (r *WebAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 1. 获取 WebApp CR
var app webappv1.WebApp
if err := r.Get(ctx, req.NamespacedName, &app); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. 处理删除(Finalizer 逻辑)
if !app.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(&app, "webapp.example.com/cleanup") {
// 执行清理逻辑(如删除关联资源、备份等)
if err := r.handleCleanup(ctx, &app); err != nil {
return ctrl.Result{}, err
}
controllerutil.RemoveFinalizer(&app, "webapp.example.com/cleanup")
if err := r.Update(ctx, &app); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// 3. 添加 Finalizer
if !controllerutil.ContainsFinalizer(&app, "webapp.example.com/cleanup") {
controllerutil.AddFinalizer(&app, "webapp.example.com/cleanup")
if err := r.Update(ctx, &app); err != nil {
return ctrl.Result{}, err
}
}
// 4. 调谐 Deployment
if err := r.reconcileDeployment(ctx, &app); err != nil {
return ctrl.Result{}, err
}
// 5. 调谐 Service
if err := r.reconcileService(ctx, &app); err != nil {
return ctrl.Result{}, err
}
// 6. 更新 Status
return r.updateStatus(ctx, &app)
}
func (r *WebAppReconciler) reconcileDeployment(ctx context.Context, app *webappv1.WebApp) error {
// 构建期望的 Deployment
desired := &appsv1.Deployment{
ObjectMeta: ctrl.ObjectMeta{
Name: app.Name,
Namespace: app.Namespace,
Labels: map[string]string{
"app": app.Name,
"managed": "webapp-operator",
},
},
Spec: appsv1.DeploymentSpec{
Replicas: &app.Spec.Replicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": app.Name},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": app.Name},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "web",
Image: app.Spec.Image,
Ports: []corev1.ContainerPort{{
ContainerPort: 8080,
}},
Resources: buildResources(app.Spec.Resources),
}},
},
},
},
}
// 设置 Owner Reference(级联删除)
ctrl.SetControllerReference(app, desired, r.Scheme)
// Create or Update
var existing appsv1.Deployment
err := r.Get(ctx, client.ObjectKey{
Name: app.Name,
Namespace: app.Namespace,
}, &existing)
if errors.IsNotFound(err) {
return r.Create(ctx, desired)
} else if err != nil {
return err
}
// 更新已有 Deployment(只更新 Spec,保留 Status)
existing.Spec = desired.Spec
return r.Update(ctx, &existing)
}
func (r *WebAppReconciler) updateStatus(ctx context.Context, app *webappv1.WebApp) (ctrl.Result, error) {
var deploy appsv1.Deployment
if err := r.Get(ctx, client.ObjectKey{
Name: app.Name,
Namespace: app.Namespace,
}, &deploy); err != nil {
return ctrl.Result{}, err
}
// 更新状态
app.Status.ReadyReplicas = deploy.Status.ReadyReplicas
app.Status.Ready = deploy.Status.ReadyReplicas == *deploy.Spec.Replicas
if app.Status.Ready {
app.Status.Message = "Deployment is ready"
} else {
app.Status.Message = fmt.Sprintf("Waiting for %d/%d replicas",
deploy.Status.ReadyReplicas, *deploy.Spec.Replicas)
}
if err := r.Status().Update(ctx, app); err != nil {
return ctrl.Result{}, err
}
// 正常情况下返回空 Result,控制器会在资源变更时重新调谐
// 如果需要延迟重新调谐:
// return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
return ctrl.Result{}, nil
}Admission Webhook
Webhook 为 CRD 增加校验和默认值注入能力,在资源创建或更新时自动执行:
// api/v1/webapp_webhook.go
package v1
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)
var _ webhook.Defaulter = &WebApp{}
var _ webhook.Validator = &WebApp{}
// Default 实现 Defaulter 接口,注入默认值
func (r *WebApp) Default() {
// 设置默认副本数
if r.Spec.Replicas == 0 {
r.Spec.Replicas = 1
}
// 设置默认资源限制
if r.Spec.Resources == nil {
r.Spec.Resources = &ContainerResources{
CPU: "500m",
Memory: "256Mi",
}
}
}
// ValidateCreate 实现 Validator 接口,校验创建请求
func (r *WebApp) ValidateCreate() error {
if r.Spec.Image == "" {
return fmt.Errorf("image is required")
}
if r.Spec.Replicas < 1 {
return fmt.Errorf("replicas must be at least 1")
}
return nil
}
// ValidateUpdate 校验更新请求
func (r *WebApp) ValidateUpdate(old runtime.Object) error {
return r.ValidateCreate()
}
// ValidateDelete 校验删除请求
func (r *WebApp) ValidateDelete() error {
return nil
}Operator 部署
RBAC 配置
Operator 的 RBAC 必须遵循最小权限原则,只授予管理目标资源所需的权限:
# Kubebuilder 自动生成 RBAC 注解
# +kubebuilder:rbac:groups=apps.example.com,resources=webapps,verbs=get;list;watch;create;update;patch;delete
# +kubebuilder:rbac:groups=apps.example.com,resources=webapps/status,verbs=get;update;patch
# +kubebuilder:rbac:groups=apps.example.com,resources=webapps/finalizers,verbs=update
# +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
# +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch
# +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
# 生成的 ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: webapp-operator-role
rules:
- apiGroups: ["apps.example.com"]
resources: ["webapps", "webapps/status", "webapps/finalizers"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["core"]
resources: ["services", "pods"]
verbs: ["get", "list", "watch", "create", "update", "patch"]Operator Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-operator
namespace: operators
labels:
control-plane: webapp-operator
spec:
replicas: 2 # 多副本高可用
selector:
matchLabels:
control-plane: webapp-operator
template:
spec:
serviceAccountName: webapp-operator-sa
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: manager
image: registry.example.com/webapp-operator:v1.0.0
args:
- --leader-elect # Leader Election,多副本只有一个活跃
- --health-probe-bind-address=:8081
- --metrics-bind-address=:8080
ports:
- containerPort: 8080 # Metrics
- containerPort: 9443 # Webhook
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
resources:
limits:
cpu: "1"
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespaceLeader Election
当 Operator 部署多副本时,Leader Election 确保只有一个控制器实例在处理事件:
// main.go
func main() {
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: 9443,
LeaderElection: true, // 启用 Leader Election
LeaderElectionID: "webapp-operator.example.com", // Leader Election ID
LeaderElectionNamespace: "operators", // 选举范围
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
if err := ctrl.NewWebhookManagedBy(mgr).For(&webappv1.WebApp{}).Complete(); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "WebApp")
os.Exit(1)
}
if err := (&controllers.WebAppReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "WebApp")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}Operator 测试
单元测试(envtest)
Kubebuilder 集成了 envtest,可以在不需要真实 Kubernetes 集群的情况下测试控制器逻辑:
// controllers/webapp_controller_test.go
package controllers
import (
"context"
"testing"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
webappv1 "mydomain/webapp-operator/api/v1"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
func TestWebAppReconciler(t *testing.T) {
// envtest 启动一个临时的 API Server 和 etcd
// 测试逻辑:
// 1. 创建 WebApp CR
// 2. 调用 Reconcile
// 3. 验证 Deployment 是否被正确创建
req := reconcile.Request{
NamespacedName: types.NamespacedName{
Name: "test-webapp",
Namespace: "default",
},
}
app := &webappv1.WebApp{
ObjectMeta: metav1.ObjectMeta{
Name: "test-webapp",
Namespace: "default",
},
Spec: webappv1.WebAppSpec{
Image: "nginx:alpine",
Replicas: 3,
},
}
// 创建 CR
err := k8sClient.Create(context.Background(), app)
if err != nil {
t.Fatalf("Failed to create WebApp: %v", err)
}
// 调用 Reconcile
_, err = reconciler.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile failed: %v", err)
}
// 验证 Deployment
var deploy appsv1.Deployment
err = k8sClient.Get(context.Background(), req.NamespacedName, &deploy)
if err != nil {
t.Fatalf("Deployment not found: %v", err)
}
if *deploy.Spec.Replicas != 3 {
t.Errorf("Expected 3 replicas, got %d", *deploy.Spec.Replicas)
}
}端到端测试
# 安装 CRD 和 Operator 到测试集群
make install
make deploy IMG=registry.example.com/webapp-operator:test
# 创建测试资源
kubectl apply -f config/samples/apps_v1_webapp.yaml
# 验证 Operator 行为
kubectl get webapps -A
kubectl get deployments -A | grep test-webapp
# 清理
kubectl delete -f config/samples/apps_v1_webapp.yaml
make undeploy成熟的开源 Operator
在决定自研 Operator 之前,先评估是否已有成熟的开源方案:
| Operator | 管理的应用 | 成熟度 |
|---|---|---|
| Prometheus Operator | Prometheus + AlertManager + Grafana | L5 |
| Cert-Manager | TLS 证书自动管理 | L5 |
| PostgreSQL Operator (Zalando) | PostgreSQL HA 集群 | L4 |
| MySQL Operator (Oracle) | MySQL InnoDB Cluster | L4 |
| Redis Operator (OPs) | Redis Sentinel/Cluster | L3 |
| Kafka Operator (Strimzi) | Apache Kafka 集群 | L5 |
| Elasticsearch Operator (ECK) | Elasticsearch + Kibana | L4 |
| Jaeger Operator | Jaeger 分布式追踪 | L3 |
| Argo CD Operator | GitOps 持续部署 | L4 |
| Loki Operator | Grafana Loki 日志系统 | L3 |
OLM(Operator Lifecycle Manager)
OLM 是 Kubernetes 上管理和分发 Operator 的标准框架:
# OperatorGroup 定义 Operator 的工作范围
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
name: my-operators
namespace: operators
spec:
targetNamespaces:
- production
- staging
---
# Subscription 订阅 Operator(自动安装和升级)
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: webapp-operator
namespace: operators
spec:
channel: stable # 订阅的频道
name: webapp-operator # Operator 名称
source: operatorhub # Catalog Source
sourceNamespace: olm
installPlanApproval: Automatic # 自动批准升级优点
缺点
总结
Operator 是 Kubernetes 生态中管理有状态服务和复杂中间件的最佳实践。开发 Operator 前应评估必要性:如果 Helm Chart 能满足需求就不必引入 Operator。开发时应遵循最小权限原则,并充分测试控制器的幂等性和错误恢复能力。
决策指南:
- 无状态服务:使用 Helm Chart + Argo CD
- 简单有状态服务:使用 Helm Chart + 手动运维脚本
- 复杂有状态服务:评估开源 Operator,如不满足再考虑自研
- 平台级能力封装:自研 Operator,将基础设施供给能力封装为 K8s 原生 API
关键知识点
- 控制器的核心是 Reconcile 循环:期望状态 vs 实际状态,持续调谐直到一致
- CRD 定义 Schema,Controller 实现逻辑,两者独立部署和升级
- ctrl.SetControllerReference 设置 Owner Reference,实现级联删除
- Leader Election 确保 Operator 多副本中只有一个活跃控制器
- Finalizer 用于在资源删除前执行清理逻辑
- Admission Webhook 用于校验和默认值注入
项目落地视角
- 从使用成熟的开源 Operator 开始(如 Prometheus Operator、Cert-Manager),再考虑自研
- 自研 Operator 时先用 Kubebuilder 脚手架生成项目结构,降低起步复杂度
- Operator 的 RBAC 权限必须最小化,只授予管理目标资源所需的权限
- 为 Operator 建立完善的 CI/CD 流程,包括单元测试、集成测试和镜像构建
常见误区
- 把所有运维逻辑都塞进 Operator,导致单体控制器难以维护
- Reconcile 函数中包含有状态逻辑,导致幂等性被破坏
- 忽略 Status 子资源的更新,用户无法通过 kubectl describe 观察到实际状态
- 不使用 Leader Election,多副本 Operator 同时调谐导致竞争
- 忘记处理 Finalizer,导致资源卡在 Terminating 状态
进阶路线
- 学习 Operator SDK 的 Helm/Ansible 模式,用低代码方式快速构建 Operator
- 掌握 K8s Admission Webhook,为 CRD 增加校验和默认值注入能力
- 了解 OLM(Operator Lifecycle Manager)和 Kruise 等高级工作负载管理方案
- 学习 eBPF 在 Operator 中的应用(如 Cilium 的网络策略管理)
适用场景
- 数据库集群(MySQL、PostgreSQL、Redis)在 K8s 中的自动化运维
- 复杂中间件(Kafka、Elasticsearch)需要定制化的生命周期管理
- 平台团队需要将基础设施供给能力封装为 K8s 原生 API
- 需要持续监控和自动修复的应用管理
落地建议
- 为 Operator 建立完善的单元测试和集成测试(envtest),确保控制器逻辑正确
- 使用 Leader Election 部署多副本 Operator,避免单点故障
- 将 Operator 镜像发布到私有 Registry,并纳入 CI/CD 自动化构建流程
- 建立 Operator 的版本管理和升级策略,确保向后兼容
排错清单
- CRD 创建失败:检查 CRD Schema 定义和 kubectl explain 输出
- 控制器不调谐:检查 RBAC 权限、Controller 事件日志和 Informer 缓存同步状态
- 资源无法删除(卡在 Terminating):检查 Finalizer 逻辑是否正确清理
- Reconcile 循环报错:查看 Operator Pod 日志,检查错误处理逻辑
- Webhook 调用失败:检查证书配置和 Webhook Service 的可达性
复盘问题
- 当前团队是否有自研 Operator?开发和维护成本是否在可控范围内?
- Operator 的测试覆盖率是否足够?是否有端到端测试覆盖完整的生命周期?
- Operator 升级是否影响已创建的 CRD 实例?是否有兼容性保障?
- Operator 的 Leader Election 是否正常工作?主从切换是否有数据丢失?
