Kubernetes Operator 测试分层:envtest、kuttl 与故障注入

说明:测试命令与故障场景只用于说明分层方法。运行前请确认测试集群隔离、资源配额和清理步骤。

在基于 Kubernetes 建设云原生平台时,许多团队都会开发自定义控制器(Operator)或自动化运维 Operator 脚本。但在实践中,常常会出现这样的奇特现象:Go 语言的单元测试覆盖率高达 80% 以上,可一旦部署到生产环境,面对复杂的集群网络抖动、Node 节点驱逐或是 Webhook 延迟时,Operator 却频繁陷入死锁或状态非预期漂移。

问题根源在于:K8s 控制器是高度依赖声明式 API 和异步收敛(Reconcile)的系统,单纯依赖 Mock 对象的单元测试,根本无法模拟真实的 API Server 行为与并发状态更新。

必须为 K8s 运维与控制器开发建立涵盖单元测试、集成测试与端到端(E2E)测试的分层测试策略。

graph TD
    subgraph "Kubernetes 控制器与 SRE 脚本分层测试金字塔"
        UT["单元测试 (Unit Level - envtest)<br/>• 验证 Reconcile 算法与状态机<br/>• 本地启动控制面二进制 (etcd+apiserver)<br/>• 毫秒级执行,不依赖 Docker"]
        IT["集成测试 (Integration Level - Kind + kuttl)<br/>• 真实 K8s API + Webhook 拦截<br/>• 验证 CRD Schema 与 RBAC 权限<br/>• 声明式 YAML 步骤断言"]
        E2E["端到端与混沌测试 (E2E & Chaos Level)<br/>• Chaos Mesh 注入节点驱逐/网络丢包<br/>• 验证大并发下的控制器幂等性<br/>• 测算状态收敛时间 (MTTR)"]
    end
    UT --> IT --> E2E

单元测试层:基于 envtest 的控制循环断言

在单元测试阶段,不要使用手写 Mock 的方式替代 client-go。Kebebuilder 提供的 envtest 可以在本地直接拉起真实的 etcdkube-apiserver 二进制文件,无需启动 Docker 即可进行真实的 API 交互测试。

基于 Controller-Runtime 与 envtest 的测试示例

package controllers

import (
	"context"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	appv1alpha1 "my.domain/api/v1alpha1"
)

var _ = Describe("CronApp Controller", func() {
	const (
		CronAppName      = "test-cronapp"
		CronAppNamespace = "default"
		timeout          = time.Second * 10
		interval         = time.Millisecond * 250
	)

	BeforeEach(func() {
		// 测试前环境清理逻辑
	})

	AfterEach(func() {
		// 清理创建的 CRD 实例
	})

	Context("当创建 CronApp 自定义资源时", func() {
		It("应该自动创建对应的 Deployment 和 Service", func() {
			ctx := context.Background()
			cronApp := &appv1alpha1.CronApp{
				ObjectMeta: metav1.ObjectMeta{
					Name:      CronAppName,
					Namespace: CronAppNamespace,
				},
				Spec: appv1alpha1.CronAppSpec{
					Replicas: 3,
					Image:    "nginx:1.25",
				},
			}
			Expect(k8sClient.Create(ctx, cronApp)).To(Succeed())

			cronAppLookupKey := types.NamespacedName{Name: CronAppName, Namespace: CronAppNamespace}
			createdCronApp := &appv1alpha1.CronApp{}

			// 验证 CRD 状态被正确更新
			Eventually(func() bool {
				err := k8sClient.Get(ctx, cronAppLookupKey, createdCronApp)
				return err == nil
			}, timeout, interval).Should(BeTrue())

			// 验证 Reconcile 逻辑是否正确拉起了底层 Pod
			podList := &corev1.PodList{}
			Eventually(func() int {
				_ = k8sClient.List(ctx, podList)
				return len(podList.Items)
			}, timeout, interval).Should(Equal(3))
		})
	})
})

集成测试层:Kind 与 kuttl 的声明式校验

单元测试无法测试 Webhook(如 Mutating / Validating Webhook)以及复杂的 Pod 调度逻辑。必须在 CI 流水线中通过 Kind (Kubernetes in Docker) 启动轻量级单节点集群,并结合 kuttl 运行声明式集成测试。

sequenceDiagram
    autonumber
    participant CI as CI Pipeline Runner
    participant Kind as Kind Cluster (Docker)
    participant Kuttl as Kuttl Test Runner
    participant Operator as Operator 待测容器

    CI->>Kind: kind create cluster --config kind-config.yaml
    CI->>Kind: 部署 CRD + Webhook 镜像
    CI->>Operator: 启动 Controller Pod
    CI->>Kuttl: 执行 kubectl kuttl test ./tests/e2e/
    Kuttl->>Kind: 00-assert.yaml: 应用测试 CRD
    Kuttl->>Kind: 01-assert.yaml: 校验系统最终收敛 YAML 状态
    Kind-->>Kuttl: 返回 Status Matching 结果
    Kuttl-->>CI: 报告声明式集成测试成功

kuttl 测试步骤 YAML 定义

创建 tests/e2e/cronapp-test/00-create.yaml

apiVersion: app.domain/v1alpha1
kind: CronApp
metadata:
  name: e2e-sample
  namespace: default
spec:
  replicas: 2
  image: nginx:alpine

创建断言 tests/e2e/cronapp-test/00-assert.yaml(kuttl 将持续轮询直到状态完全匹配):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: e2e-sample
  namespace: default
spec:
  replicas: 2
status:
  readyReplicas: 2

端到端与混沌测试:验证生产环境幂等性

生产环境中最致命的问题莫过于并发冲突。当多个 Node 同时死机,控制器需要在短时间内处理数百个 Pod 的重调度与状态收敛,此时必须引入 Chaos Mesh 验证** Reconcile 幂等性**。

混沌测试注入命令与实战

# 1. 启动本地测试集群
kind create cluster --name operator-test-cluster --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF

# 2. 安装 setup-envtest 并导出环境变量供单元测试使用
go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
eval $(setup-envtest use 1.30.0 -p env)

# 3. 运行 envtest 单元测试
go test -v ./controllers/...

# 4. 使用 kuttl 执行声明式集成测试
kubectl kuttl test ./tests/e2e/ --config kuttl-test.yaml

# 5. 在测试集群中模拟 Worker 节点网络中断,观察 Operator 的重试收敛能力
docker stop operator-test-cluster-worker
kubectl get pods -w

生产级测试防线的三条硬准则

  1. 绝对不在单元测试中使用 Dummy Client:必须使用 envtest 跑在真实的 kube-apiserver 逻辑上,才能捕捉 ResourceVersion 冲突错误(Conflict / Optimistic Lock Error)。
  2. 控制器逻辑必须保证幂等:无论 Reconcile 函数因为网络重试被触发 1 次还是 100 次,集群产生的最终状态必须完全一致。
  3. 将 Chaos 测试作为发布门禁:在 major 版本上线前,必须进行至少 10 分钟的 API Server 高延迟与 Node 随机杀死测试。

更多推荐