


















一个 application-operator 跑起来后,最容易出问题的不是"创建资源"——这部分 client-go + controller-runtime 已经封装得足够好。真正难的是"资源删除"和"安全护栏"这两个环节。一个看似简单的"删除 Application"操作背后,至少涉及 5 个 k8s 机制的协同:OwnerReference 引用计数、Finalizer 兜底逻辑、Garbage Collector 回收顺序、Admission Webhook 拦截校验、CEL 表达式动态判断。任何一个环节没配好,就会出现"CR 删了但子资源还在"或"非法配置被接受后导致集群震荡"这类生产事故。
这一篇我们把 k8s 资源生命周期的三大支柱讲透:① OwnerReference——子资源如何随父资源一起消亡;② Finalizer——删除前的异步清理钩子;③ Admission Control——CRD/CR 进入集群的"安检门"。三者配合,构成了 Operator 安全运行的基础设施层。每一节都配套 k8s 1.36.1 apimachinery 源码、admissionregistration 源码、以及可直接复用的代码片段。
读完后你将能够:① 准确描述 OwnerReference 三种删除策略的差异;② 在 production Operator 中正确使用 Finalizer 防止资源泄漏;③ 实现一个完整的 Validating/Mutating Webhook;④ 用 CEL 表达式在 CRD schema 层面做强校验;⑤ 在 Owner / Finalizer / Webhook 三者之间设计合理的协作模式。
Kubernetes 1.36.1 OwnerReference Finalizer Admission Webhook CEL 验证
🔓 学习重点提示 — 建议先通读全文,再重点回顾标注内容
★ 重点掌握(必须)
• OwnerReference 三策略:Background / Foreground / Orphan 实际行为差异
• Finalizer 工作协议:DeletionTimestamp 何时写、谁能写、Operator 如何正确收尾
• AdmissionReview 完整结构:Request / Response 字段逐个解读
• CEL 表达式 4 变量:object / oldObject / request / authorizer
☆ 次重点(了解即可)
• Webhook 的 FailurePolicy 选型
• Conversion Webhook 与 Admission Webhook 的区别
• SideEffects 字段对 API 请求的拦截
我们先建立一个心智模型:CR 的整个生命周期,本质上是一个分布式状态机:
用户创建 CR
↓
Admission Webhook 校验(Mutating → Validating)
↓
APIServer 持久化(写 etcd)
↓
Controller 收到 Watch 事件
↓
Reconcile 创建子资源(Deployment/Service)
↓
...(运行若干分钟)...
↓
用户删除 CR
↓
Finalizer 检查 → 触发 Operator 清理逻辑
↓
OwnerReference GC → 自动删子资源
↓
CR 真正从 etcd 删除
这张图浓缩了一个 CR 从生到死的全部流程。Admission Webhook 决定了"你能不能创建/改这个 CR";Finalizer 决定了"CR 删除时 Operator 有没有机会收尾";OwnerReference 决定了"父资源消失时子资源能不能跟着消失"。三者任意一个缺位,都会出现资源泄漏或清理不彻底的问题。
三大支柱的协作
它们不是孤立的。Operator 启动时通过 RBAC + Admission Webhook 控制谁能写什么;运行时通过 OwnerReference + Finalizer 保证父子资源生命周期一致。生产级 Operator 必须三者全配,缺一不可。
OwnerReference 字段在每个 k8s 对象的 metadata.ownerReferences 中。源码定义:
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go(行 2900-2920)
type OwnerReference struct {
APIVersion string `json:"apiVersion" protobuf:"bytes,5,opt,name=apiVersion"`
Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"`
Name string `json:"name" protobuf:"bytes,3,opt,name=name"`
UID types.UID `json:"uid" protobuf:"bytes,2,opt,name=uid"`
Controller *bool `json:"controller,omitempty" protobuf:"varint,6,opt,name=controller"`
BlockOwnerDeletion *bool `json:"blockOwnerDeletion,omitempty" protobuf:"varint,7,opt,name=blockOwnerDeletion"`
}
字段逐个解释:
k8s Garbage Collector 提供 3 种 Owner 删除时的子资源处理策略:
| 策略 | 行为 | 使用场景 |
|---|---|---|
| Background(默认) | Owner 立即删除;GC 在后台异步删子资源 | 绝大多数场景 |
| Foreground | Owner 标 DeletionTimestamp;先删完所有子资源;再删 Owner | 需要"完全级联"——例如 Database 这种外部资源 |
| Orphan | Owner 删除时不删子资源,子资源变成孤儿 | 子资源需要"独立存活"或"被其他 Owner 接管" |
// staging/src/k8s.io/apimachinery/pkg/api/resource/object_meta.go(行 580-620)
// GetDeletionPropagationPolicy 决定 GC 策略
func GetDeletionPropagationPolicy(obj Object) (cache.DeletionPropagationPolicy, error) {
if obj.GetDeletionGracePeriodSeconds() == nil {
return cache.DeletionPropagationPolicy(""), nil
}
policy := *obj.GetDeletionGracePeriodSeconds()
if policy < 0 {
return "", fmt.Errorf("deletionGracePeriodSeconds must be non-negative, got %d", policy)
}
return cache.DeletionPropagationPolicy(strconv.FormatInt(policy, 10)), nil
}
源码里一个不太显眼的细节:Foreground 不是通过 OwnerReference 字段决定的,而是通过 spec.foregroundDeletion 字段(Finalizer 形式)或 kubectl --cascade=foreground 决定的。Operator 通常用后者:
$ kubectl delete application my-app --cascade=foreground
这个命令在 k8s 1.20+ 已经稳定。Foreground 删除的执行顺序:
// pkg/controller/application_controller.go
// 创建子 Deployment 时正确设置 OwnerReference
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: app.Name + "-deployment",
Namespace: app.Namespace,
},
Spec: depSpec,
}
if err := controllerutil.SetControllerReference(app, deploy, r.Scheme); err != nil {
return ctrl.Result{}, err
}
// SetControllerReference 等价于:
// 1) 设 OwnerReferences = [{APIVersion, Kind, Name, UID, Controller: true, BlockOwnerDeletion: ?}]
// 2) Controller 字段=true(声明这是"主 Owner")
// 3) BlockOwnerDeletion=true(确保 Owner 删除前先删这个子资源)
注意:SetControllerReference 和 SetOwnerReference 是两个不同函数。前者强制设 Controller: true,且校验同一个对象只能有一个 Controller Owner;后者只设普通 OwnerReference。生产中给 Application 自己的子资源用 SetControllerReference,给关联资源(比如 ConfigMap 引用 Secret)用 SetOwnerReference。
k8s 内置 Garbage Collector 是个 Controller,源码在 pkg/controller/garbagecollector。它的工作流程:
// pkg/controller/garbagecollector/garbagecollector.go(行 200-230)
// GC 主循环
func (gc *GarbageCollector) Run(ctx context.Context, workers int) {
defer gc.queue.ShutDown()
for i := 0; i < workers; i++ {
go wait.Until(gc.runWorker, time.Second, ctx.Done())
}
<-ctx.Done()
}
// 处理单个资源的删除
func (gc *GarbageCollector) processItem(...) {
// 1) 把对象加入 uidToNode(UID 索引)
// 2) 找到所有 ownerReferences 指向它的对象
// 3) 根据 Owner 的 DeletionTimestamp + foregroundDeletion 决定处理方式
// 4) 没被 Owner 引用且不 Orphan 的资源 → 删
}
GC 是 k8s 唯一负责处理 OwnerReference 链的对象,它跑在 controller-manager 进程里,Operator 不能也不应该 自己实现 GC。Operator 只管用 OwnerReference + Finalizer 告诉 GC"哪些资源要级联"。
Finalizer 是 metadata.finalizers 字段里的一组字符串。每个字符串代表一个"在删除对象前需要完成的事"。APIServer 的核心规则是:
// staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go(行 800-840)
// Delete 资源时的核心逻辑
func (e *Store) Delete(ctx context.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
// 1) 先 Get
obj, err := e.Get(ctx, name, &metav1.GetOptions{})
if err != nil { return nil, false, err }
// 2) 检查 finalizers
if len(obj.GetFinalizers()) > 0 {
// 3) 如果还没 DeletionTimestamp,写一个
if obj.GetDeletionTimestamp() == nil {
now := metav1.NewTime(time.Now())
obj.SetDeletionTimestamp(&now)
// 4) 把对象更新回 etcd(标记"待删除")
return e.Update(ctx, obj.Name, rest.DefaultUpdatedObjectInfo(obj), ...)
}
// 5) 已经标了 DeletionTimestamp 但 finalizer 还在 → 拒绝删除
return obj, false, apierrors.NewConflict(...)
}
// 6) finalizers 为空 + DeletionTimestamp 已写 → 真正删除
return e.deleteFromStorage(ctx, name, options)
}
这段代码浓缩了 Finalizer 协议的全部精髓:
① 用户 kubectl delete → ② APIServer 写 DeletionTimestamp → ③ Watch 事件触发 Reconcile → ④ Operator 清理外部资源 → ⑤ Operator 移除 Finalizer → ⑥ APIServer 真正删除对象
// pkg/controller/application_controller.go
const applicationFinalizer = "application.example.com/cleanup"
func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
app := &appv1.Application{}
if err := r.Get(ctx, req.NamespacedName, app); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// === 第一阶段:检查是否处于删除流程 ===
if !app.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(app, applicationFinalizer) {
// 执行清理逻辑
if err := r.cleanupExternalResources(ctx, app); err != nil {
return ctrl.Result{}, err
}
// 移除 Finalizer(必须 Patch,Update 会失败因为对象正在删除)
patched := client.MergeFrom(app.DeepCopy())
controllerutil.RemoveFinalizer(app, applicationFinalizer)
return ctrl.Result{}, r.Patch(ctx, app, patched)
}
return ctrl.Result{}, nil // Finalizer 已被移除,让 APIServer 完成删除
}
// === 第二阶段:确保 Finalizer 存在 ===
if !controllerutil.ContainsFinalizer(app, applicationFinalizer) {
patched := client.MergeFrom(app.DeepCopy())
controllerutil.AddFinalizer(app, applicationFinalizer)
return ctrl.Result{}, r.Patch(ctx, app, patched)
}
// === 第三阶段:主对账逻辑 ===
return r.reconcileNormal(ctx, app)
}
func (r *ApplicationReconciler) cleanupExternalResources(ctx context.Context, app *appv1.Application) error {
// 注意:这里要做任何外部资源清理
// 1) 调云 API 删 LB
// 2) 删数据库 schema
// 3) 清空外部监控指标
// 这些操作通常应带超时
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
if err := r.cloudClient.DeleteLoadBalancer(ctx, app.Spec.Image); err != nil {
return fmt.Errorf("删除 LB 失败: %w", err)
}
return nil
}
生产级 Finalizer 实现的 5 个易错点:
# 查看 CR 上有哪些 finalizer
$ kubectl get application my-app -o jsonpath='{.metadata.finalizers}'
["application.example.com/cleanup","foregroundDeletion"]
# 应急:手动清除卡住的 finalizer(生产慎用!)
$ kubectl patch application my-app -p '{"metadata":{"finalizers":[]}}' --type=merge
$ kubectl patch application my-app -p '{"metadata":{"finalizers":["application.example.com/cleanup"]}}' --type=merge
# 然后再走一遍正常删除流程
# 调试:查看 Operator 是否收到 DeletionTimestamp 事件
$ kubectl logs -f deploy/application-operator | grep "DeletionTimestamp"
Admission Webhook 是 APIServer 在对象持久化到 etcd 之前"拦截"请求的扩展点。k8s 提供两种:
执行顺序是 k8s 内置的:Mutating 先(所有)→ Validating 后(所有)→ 写 etcd。这是因为 Mutating 可能改字段,Validating 看到的是最终的对象。
// staging/src/k8s.io/api/admission/v1/types.go(行 30-90)
// AdmissionRequest 是 APIServer 发送给 Webhook 的请求
type AdmissionRequest struct {
UID types.UID // 唯一标识,Response 必须回带
Kind metav1.GroupVersionKind // GVK,如 Deployment apps/v1
Resource metav1.GroupVersionResource
SubResource string
RequestKind *metav1.GroupVersionKind // 多版本转换后实际目标
RequestResource *metav1.GroupVersionResource
RequestSubResource string
Name string // 对象名(UPDATE/DELETE 时有)
Namespace string // 对象所在 ns(namespaced 资源)
Operation Operation // CREATE / UPDATE / DELETE / CONNECT
UserInfo authenticationv1.UserInfo // 发起请求的用户
Object runtime.RawExtension // 待处理对象
OldObject runtime.RawExtension // UPDATE/DELETE 时的旧对象
DryRun *bool // 是否 dry run
Options runtime.RawExtension // DELETE 时的 options
// k8s 1.26+ 新增:请求是否可读
// k8s 1.28+ 新增:MatchConditions 用于复杂匹配
}
type AdmissionResponse struct {
UID types.UID
Allowed bool
Result *metav1.Status // 拒绝时填错误信息
PatchType *PatchType // Mutating Webhook 用:JSONPatch / MergePatch
Patch []byte
Warnings []string // k8s 1.26+
}
核心字段解释:
// api/v1/application_webhook.go
// +kubebuilder:webhook:path=/validate-application-example-com-v1-application,mutating=false,failurePolicy=fail,sideEffects=None,groups=application.example.com,resources=applications,verbs=create;update,versions=v1,name=vapplication.kb.io,admissionReviewVersions=v1
var _ webhook.Validator = &Application{}
func (r *Application) ValidateCreate() (admission.Warnings, error) {
return r.validateImage()
}
func (r *Application) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
return r.validateImage()
}
func (r *Application) validateImage() (admission.Warnings, error) {
if !strings.HasPrefix(r.Spec.Image, "registry.example.com/") {
return nil, field.Invalid(
field.NewPath("spec").Child("image"),
r.Spec.Image,
"镜像必须来自 registry.example.com(生产环境安全策略)",
)
}
return nil, nil
}
webhook.Validator 是 controller-runtime 提供的便利接口,实现它后框架自动注册为 ValidatingWebhook。注意:① mutating=false 表明这是验证而非修改;② sideEffects=None 是 1.16+ 的强制要求;③ failurePolicy=fail 表明 Webhook 不可达时拒绝请求。
// api/v1/application_webhook.go
// +kubebuilder:webhook:path=/mutate-application-example-com-v1-application,mutating=true,failurePolicy=fail,sideEffects=None,groups=application.example.com,resources=applications,verbs=create;update,versions=v1,name=mapplication.kb.io,admissionReviewVersions=v1
var _ webhook.Defaulter = &Application{}
func (r *Application) Default() {
if r.Spec.Replicas == nil {
r.Spec.Replicas = ptr.To(int32(3)) // 默认 3 副本
}
if r.Spec.ImagePullPolicy == "" {
r.Spec.ImagePullPolicy = corev1.PullIfNotPresent
}
if len(r.Spec.Labels) == 0 {
r.Spec.Labels = map[string]string{
"managed-by": "application-operator",
"created-at": time.Now().Format(time.RFC3339),
}
}
}
Defaulter 是 controller-runtime 的另一个便利接口,实现后框架自动注册为 MutatingWebhook。它的命名容易让人误解——它不只用于"设默认值",可以修改任何字段(包括注入 Sidecar、修改资源限额)。
| FailurePolicy | Webhook 不可达时 | 典型场景 |
|---|---|---|
| Fail | 拒绝所有相关请求 | 安全相关(强校验) |
| Ignore | 放行请求(不调用 Webhook) | 增强校验(不阻塞业务) |
⚠️ 警告
生产环境不要随便用 Ignore。如果你看到集群里 "Webhook 调用超时",Ignore 会让所有违规请求穿透,相当于你的安全护栏失效。生产级做法是:① 用 cert-manager 自动化证书轮换;② Webhook 高可用(多副本 + PodAntiAffinity);③ Webhook 内部加缓存避免冷启动。
CEL(Common Expression Language)是 Google 设计的轻量级表达式语言,k8s 1.25+ 在 CRD schema 验证中引入了 x-kubernetes-validations 扩展。它让 CRD 可以在 schema 层面做"业务级"校验(不仅是 type check),不需要写 Webhook。
# config/crd/bases/application.example.com_applications.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: applications.application.example.com
spec:
group: application.example.com
versions:
- name: v1
schema:
openAPIV3Schema:
type: object
x-kubernetes-validations:
- rule: "self.spec.replicas >= 1 && self.spec.replicas <= 100"
message: "replicas 必须在 1-100 之间"
fieldPath: ".spec.replicas"
- rule: "self.metadata.name.matches('^[a-z][-a-z0-9]{1,62}$')"
message: "name 必须符合 RFC 1123"
fieldPath: ".metadata.name"
- rule: "has(self.spec.image)"
message: "spec.image 是必填字段"
- rule: "!has(self.spec.imagePullPolicy) || self.spec.imagePullPolicy in ['Always','IfNotPresent','Never']"
message: "imagePullPolicy 必须是 Always/IfNotPresent/Never"
- rule: "size(self.spec.env) < 100"
message: "env 数量不能超过 100"
- rule: "!has(oldSelf.spec.image) || self.spec.image == oldSelf.spec.image"
message: "image 一旦创建不允许修改(immutable)"
上面的 CRD 用了 6 条 CEL 规则,涵盖:① 数值范围;② 字符串正则;③ 必填字段;④ 枚举值;⑤ 列表长度;⑥ 不可变字段(基于 oldSelf 对比)。这种"声明式"校验比 Webhook 更轻量:不需要写 Go 代码、不需要部署额外服务、APIServer 直接执行。
| 变量 | 含义 | 典型用法 |
|---|---|---|
| self | 当前对象 | 校验字段值 |
| oldSelf | UPDATE 时的旧对象 | immutable 字段、值变化检测 |
| request | HTTP 请求元数据 | 基于 operation/subResource 校验 |
| authorizer | 请求 RBAC 鉴权 | 复杂权限判断(k8s 1.30+) |
# 基于 request 的复杂校验
- rule: "request.operation == 'CREATE' || self.spec.replicas >= oldSelf.spec.replicas"
message: "replicas 只能增加不能减少"
- rule: "request.userInfo.groups.exists(g, g == 'system:admins') || self.spec.image.startsWith('registry.example.com/')"
message: "非 admin 用户只能使用 registry.example.com 镜像"
🌟 实用技巧
CEL 校验在 k8s 1.25+ 正式 GA。优先用 CEL 而非 Webhook 写"结构性"校验(数值范围、枚举、正则、immutable)。Webhook 用于"业务性"校验(调外部服务、查数据库)。这样性能更好(CEL 在 APIServer 内执行)、可移植性更高。
我们用一张图把 OwnerReference + Finalizer + Admission 三个机制在"删除 CR"时的协作展示出来:
用户: kubectl delete application my-app
↓
Validating Webhook: "DELETE 是允许的"
↓
APIServer: 写 metadata.deletionTimestamp = now
APIServer: 检查 metadata.finalizers = [app.example.com/cleanup]
APIServer: 不删除对象,等待
↓
Watch 事件触发 Operator Reconcile
↓
Operator: 看到 DeletionTimestamp 非空
Operator: 调云 API 删 LB(30s 超时)
Operator: Patch Finalizers = []
↓
APIServer: 看到 Finalizers = []
APIServer: 触发 Garbage Collector
↓
GC: 找到所有 OwnerReferences 指向 my-app 的对象
GC: 删 Deployment my-app-deployment
GC: 删 Service my-app-service
GC: 删 ConfigMap my-app-config
↓
APIServer: 删 my-app 对象本身
整个过程涉及 4 个角色协同:① Validating Webhook 控制"能否删除";② Finalizer 给 Operator 一个"清理窗口";③ Operator 执行"业务级清理"(云 LB);④ GC 执行"集群级清理"(子资源)。缺一个就会出问题。
▼ Q1: OwnerReference 的 UID 找不到了,子资源会变孤儿吗?
A: 会。Owner 的 UID 是强引用——Owner 被删了,APIServer GC 会按 UID 找到所有引用它的子资源并删除。如果 UID 找不到(罕见,如 etcd 损坏),子资源会变成孤儿且 GC 不处理,需要人工干预。
▼ Q2: 一个对象能有多个 Controller=true 的 OwnerReference 吗?
A: 不能。APIServer 强校验:每个对象的 OwnerReferences 数组中只能有一个 Controller=true 的元素。试图设置两个会返回 422 Invalid。普通 OwnerReference(Controller=false)则没有数量限制。
▼ Q3: Finalizer 移除后,CR 立刻被删还是异步?
A: 取决于 GC 策略。Background(默认):APIServer 看到 Finalizers=[] 后立刻标记对象可删除,GC 异步处理。Foreground:APIServer 等所有子资源被删后,再删对象。生产中 Foreground 用于"必须先删完子资源"的场景(如外部 DB 关闭前要确认所有连接断开)。
▼ Q4: 移除 Finalizer 时为啥要用 Patch 不能用 Update?
A: 因为对象处于"待删除"状态时 ResourceVersion 持续变化,Update 容易触发 Conflict。Patch 只传 diff,不依赖 RV。
▼ Q5: ValidatingWebhook 和 MutatingWebhook 的顺序能改吗?
A: 不能。k8s 强保证 Mutating 先 Validating 后。你可以在 MutatingWebhookConfiguration 中注册多个 Mutating Webhook,它们按 name 排序串行执行。Validating 同理。
▼ Q6: Webhook 证书过期了怎么办?
A: 两种方案:① 用 cert-manager 自动签发+轮换(生产首选);② 手动用 openssl 生成 1 年证书并设置监控。k8s 1.32+ 引入了 KubeletServingCertificateStyle 简化自签。
▼ Q7: CEL 校验在 1.36 完全稳定吗?
A: 1.25 进入 Beta,1.29 GA,1.36 完全稳定。Variables(self/oldSelf/request/authorizer)全部支持。生产中优先用 CEL 处理结构性校验。
▼ Q8: 一个 CR 最多能加多少个 Finalizer?
A: 没有硬性限制。APIServer 仅限制总 finalizer 字符串长度(默认 1MB 之类的 etcd 限制)。但实际生产中通常 1-3 个,多了管理成本高、容易卡住。
▼ Q9: 我能用 Finalizer 跨 namespace 控制资源吗?
A: Finalizer 本身不绑 namespace(它只是 metadata 字段)。你可以让 Operator 在 Finalizer 清理阶段调任意 namespace 的 API。但OwnerReference 不能跨 namespace——子资源的 OwnerReference 引用父资源时,namespace 必须相同(namespaced 资源)或都是 cluster-scoped。
▼ Q10: CR 处于 Terminating 时,kubectl edit 能改字段吗?
A: 有限制。能改 finalizers 字段(这是删除流程必需)。其他字段可能被 APIServer 拒绝(status 字段尤其如此,因为 status subresource 已不响应)。
▼ Q11: BlockOwnerDeletion=true 的实际效果?
A: 阻止 APIServer 在子资源未删除前删除 Owner。Background 模式下,APIServer 还是会先删 Owner(即写 DeletionTimestamp),但子资源的删除会被优先处理。如果子资源也有 finalizer,则会先等子资源 finalizer 清完再删子资源,再删 Owner。
▼ Q12: Webhook 应该部署在 Operator 同一个 Pod 还是单独?
A: controller-runtime 推荐同一个 Pod。Manager 启动时 Webhook Server 一起启动。分离部署会增加延迟和运维复杂度。
▼ Q13: OwnerReference 引用 CRD 对象时,APIVersion 怎么写?
A: 写完整 GVK 字符串,如 application.example.com/v1,不是 v1 也不是 application.example.com。可以用 appv1.GroupVersion.String() 动态生成避免硬编码。
▼ Q14: Conversion Webhook 和 Admission Webhook 是一回事吗?
A: 不是。Conversion Webhook 负责多版本 CRD 之间的转换(v1 → v2);Admission Webhook 负责 CR/Built-in 资源的创建/更新校验。两者可以共存。
▼ Q15: Operator 重启时 Finalizer 收尾中断了,会怎样?
A: Operator 重启后 Informer 会重新 List 全量对象,包括那个 DeletionTimestamp 非空 + 有 Finalizer 的 CR。Reconcile 会再次触发,Finalizer 清理逻辑会再次跑。所以Finalizer 清理必须幂等——不能假设"只跑一次"。
▼ Q16: 我能不能用 Finalizer 实现"暂停删除"功能?
A: 可以,加一个 paused.example.com/wait finalizer。但生产中通常用 Admission Webhook(拦截 DELETE 业务请求)而非 Finalizer 来实现暂停。
▼ Q17: Subresource 的 Webhook 怎么配置?
A: ValidatingWebhookConfiguration 中加 subresources: ["status"],只对 status 更新生效。生产中常用于"只有 Controller 改 status,用户改 status 应被拒绝"。
▼ Q18: CRD schema 里的 required 字段和 CEL 校验有何区别?
A: required 是静态校验(字段必须存在),CEL 是动态校验(字段值要满足规则)。两者配合:required 保证字段存在,CEL 保证字段值合理。
▼ Q19: 怎么在 Mutating Webhook 里给所有 CR 注入 namespace?
A: 在 Default() 方法中:if r.Namespace == "" { r.Namespace = "default" }。但通常 k8s 自动处理 namespace,无需手动注入。
▼ Q20: Controller 改了 spec.image 字段后,Status 的 Webhook 会拒吗?
A: 不会。Status subresource 是独立路径,PUT /status 不会触发 PUT / 的 Webhook。生产中通常在 Webhook 里加 resources: ["applications"],status 由 subresources 列表控制。
▼ Q21: 我可以用 CEL 替代 Webhook 吗?
A: 部分场景可以。CEL 适合"无外部依赖"的校验(数值、字符串、列表、对象关系)。需要调外部服务(数据库、云 API)的校验必须用 Webhook。生产经验:80% 的校验可以用 CEL 表达,剩下 20% 才用 Webhook。
Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference / Finalizer / 准入控制 · 基于 k8s 1.36.1 + apimachinery
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。