范围与故障模式 — 开启策略那天的顺序
一句话总结
策略的风险不在于规则的内容,而在于范围和故障模式,所以启用策略那天的顺序永远是:窄范围 → 警告 → 扩大 → 拦截。
为什么需要它
把策略引入事故的记录汇总起来看,因为规则写错而出的事故很少。大部分是两种情况:挂得太宽,或者没有决定故障时的行为。
为什么挂得宽是危险的,看一下成本的性质就知道了。匹配任何资源的一个策略,其成本不是策略执行一次所花的时间,而是附加在进入 API 服务器的所有写入请求上的税。集群里控制器发出的请求,比人发出的请求多得多。租约、事件、EndpointSlice、Pod 状态更新,不停地流动。一个通配符策略,会把这一切都多拦住一次。
第二个更可怕。如果把策略设置成连 kube-system 也要看,而策略引擎又死掉了,那么在 failurePolicy: Fail 之下,集群会无法自行恢复。因为想要恢复引擎的部署,也要经过那个策略。这是一个循环。
工作原理
范围从三个层次来收窄。
| 层 | 决定什么 | 例子 |
|---|---|---|
| 资源 | 查看哪些组、版本、类型、动词 | 只看 apps/v1 deployments 的 CREATE、UPDATE |
| 命名空间 | 适用于哪些命名空间 | 用 namespaceSelector 只选带有特定标签的 |
| 请求 | 在其中再看哪些请求 | objectSelector、matchConditions 的 CEL 条件 |
这三个层次也是降低成本的顺序。在资源层被过滤掉的请求,API 服务器连策略都不会想起。命名空间层在其次,请求层在最后。所以用通配符接收、再用 CEL 条件过滤的策略,等于在三层中最贵的位置工作。结果相同,成本却更大。
排除 kube-system 不是出于喜好,而是留出逃生舱。策略引擎所在的命名空间也一样。很多组织的运行手册里写着“删除 Webhook 配置来让集群复活”的流程,如果一开始就把它们排除在范围之外,这个流程就用得更少了。
故障模式——failurePolicy 所交换的东西。
| 值 | 策略服务器无法响应时 | 此时失去什么 |
|---|---|---|
Fail |
拒绝请求 | 可用性。引擎死掉,该范围的写入就会停止 |
Ignore |
放行请求 | 保障。引擎死掉期间,请求无需检查就进来了 |
两者都不是正确答案。即使在同一个集群里,每条规则适合的值也不同。守护安全边界的规则用 Fail,卫生规则(标签标准、推荐设置)用 Ignore。而且越是选了 Fail 的规则,缩小范围就越重要。因为范围窄的话,引擎即使死掉,停摆的也只是那个范围。范围和故障模式不是分别选择的值,而是成对选择的值。
timeoutSeconds 是第三个旋钮。它是等待 Webhook 响应的时间,设得长,发生故障时 API 服务器就会被拖住那么久。设得短,缓慢的响应会被当作失败,转交给 failurePolicy 处理。所以超时不是“设得宽裕”,而是设成“比这条规则正常时花费的时间稍长一点”。
内置策略与 Webhook 策略的故障模式不同。这个差异会改变设计的选择。
- VAP:在 API 服务器内部评估。没有网络,所以没有超时,也不会因为引擎 Pod 死掉而停摆。故障只会以“表达式求值错误”这一狭窄的形式出现。
failurePolicy处理的就是这种情况。 - PSA:是内置的准入插件,同样没有外部依赖。启用错误时的故障,是“这个命名空间里 Pod 起不来”这种局部的形式。
- Webhook 引擎:依赖外部 Pod、网络和证书。故障模式最宽,所以
failurePolicy和范围设计就越发重要。
所以实际工作中的布局,通常是这样分的:不需要引擎就能表达的验证,下沉给 VAP 和 PSA,以缩小故障面,只把确实必要的留在 Webhook 上。
启用策略那天的顺序。
1) 좁은 범위로 시작한다 (한 네임스페이스, 한 종류, 한 동사)
2) 경고·감사로만 켜고 며칠 센다
3) 걸린 것을 고치거나 좁은 예외로 남긴다
4) 범위를 한 단계 넓히고 2..3 을 반복한다
5) 마지막에 차단으로 올린다 (정책 본문이 아니라 켜는 쪽만 고친다)
6) 되돌리는 절차를 미리 적어 둔다
很多团队会把第 4 项和第 5 项的顺序颠倒。从窄范围直接提升为拦截,然后再扩大范围。这样一来,扩大范围的那天就成了事故的那天——新加入的命名空间没有经过观测,就直接遇上了拦截。不在同一天做扩大范围和提升为拦截,是这个顺序的关键。
在现场相遇的样子
第一,通配符策略的延迟税。因为一个匹配所有资源的 Webhook,API 服务器延迟分布的尾部整体抬高,这样的案例很常见。原因之所以难以查明,是因为策略本身是快的——慢的不是某一个请求,而是全部请求。
第二,拦住了自己的策略。没有把引擎命名空间排除在范围之外,结果重新部署引擎的请求,在询问已经死掉的引擎时被拦住了。让它复活的唯一办法只有删除 Webhook 配置,而在寻找知道这个流程的人的期间,故障被拉长了。
第三,用 Ignore 启用后就忘了。为了避免事故而全部用 Ignore 启用,那么引擎死掉的几个小时里,无需检查就进来的东西,没有人知道。选了 Ignore 的话,引擎可用性告警就必须成为策略的一部分。
第四,集中在一个节点上的引擎。即使有三个副本,如果在同一个节点上,那个节点一掉线就会一起消失。在 Fail 之下,这就等于写入停止。拓扑分布和 PodDisruptionBudget 不是引入策略的可选项,而是必备品。
第五,在这个环境中能看到的。在 kwok 集群中,可以创建地址不可达的 Webhook 配置,实际看到 Fail 与 Ignore 的差别。同一个请求,在一边被拦住,在另一边通过。
参考文档
- 动态准入控制(Webhook 配置): https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
- 准入控制器列表:https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/
- ValidatingAdmissionPolicy: https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/
- Pod Security Admission: https://kubernetes.io/docs/concepts/security/pod-security-admission/
下一项实验要做什么
故意让策略出故障,并观察这个故障。创建地址不可达的 Webhook 配置,用同一个请求确认在 failurePolicy: Fail 下请求被拦住、在 Ignore 下通过,并看到用 namespaceSelector 把系统命名空间排除在范围之外后,逃生舱就出现了。分别在资源、命名空间、请求三个层次上逐步收窄范围,统计哪些请求经过了策略、哪些没有,最后走一遍窄范围 → 警告 → 扩大 → 拦截的顺序,并制作出回退的流程。