kube-score 与静态清单体检工具
一个团队在 Helm 源模板里补齐了 CPU 与内存 requests,代码评审也确认字段存在,部署到集群的 Deployment 却仍缺少 requests。真正原因是生产 values 关闭了那段模板,CI 只扫描了模板源码,没有扫描 helm template 的最终输出。静态体检的输入若不是将要交付的对象,绿色结果只证明检查了错误的材料。
另一次门禁升级后,几十个仓库同时报告缺少 NetworkPolicy 和 PodDisruptionBudget。开发者逐个给 Deployment 加忽略注解,流水线恢复绿色,但工具实际只收到单个 Deployment 文件,根本没有看到同 namespace 一起发布的 NetworkPolicy 与 PDB。上下文缺失被误判成配置缺失,随后又被永久豁免;一条本来可解释的静态建议,变成了无法复核的组织盲区。
静态清单体检究竟证明了什么
kube-score 是无状态 Kubernetes 对象静态分析器。它读取 YAML 对象,依据内置检查给出可靠性和安全建议;它不部署 Operator,不创建 CRD,也不需要集群 RBAC。输入可以来自静态 YAML、helm template、kustomize build,也可以来自 kubectl get 导出的对象流。最后一种方式中的集群权限属于调用 kubectl 的身份,不是 kube-score 在集群里运行的控制器权限。
它能证明的是:在固定 kube-score 版本、目标 Kubernetes 版本、输入对象集合和忽略策略下,这批渲染对象触发了哪些静态检查,以及进程返回了什么退出码。它不能证明镜像没有漏洞、节点与控制平面符合基准、对象已被 API Server 接受、Pod 能成功调度、probe 真能反映业务健康,也不能观察运行时攻击。
这条边界让 kube-score 特别适合 PR 和 CI:运行快、无集群写权限、输出容易归档。但同样因为没有控制器和持久状态,输入完整性、工具版本和外部证据保存全部由流水线负责。把它称为“集群安全扫描”会夸大能力,把它只当格式检查又低估了跨对象配置问题。
固定 v1.20.0 并验证二进制来源
这里使用 kube-score v1.20.0 作为稳定 release 基线。官方仓库仍有维护活动,但稳定 release 节奏较慢;采用前应查看 kube-score Releases 与官方 README,确认目标平台、目标 Kubernetes API 和组织所需输出格式。二进制支持 Linux、macOS、Windows,也可从 Docker、Homebrew 或 Krew 使用;从源码构建需要 Go 1.21 及以上。
本地开发机可从 GitHub Release 下载匹配操作系统和架构的归档,核对 release 提供的摘要后放入受控工具目录。CI 更适合使用内部镜像或固定 digest 的官方镜像,避免运行时下载浮动版本。安装完成先记录版本和帮助输出:
kube-score version
kube-score score --help
sha256sum "$(command -v kube-score)" > artifacts/kube-score.binary.sha256若使用 Homebrew 或 Krew,包管理器只解决安装与更新,不替团队锁定行为。把 v1.20.0 写进开发环境声明、CI 镜像标签和升级 PR;卸载时也要删除这些引用,而不只是删除个人电脑上的二进制。
目标 Kubernetes 版本是检查输入
--kubernetes-version 会改变检查判断,而 kube-score 的默认目标仍是 v1.18。不显式传入目标版本,意味着现代集群可能按旧 API 假设被分析。这个字段不是展示标签,而是结果可重复性的组成部分。
目标版本应来自部署环境的受控元数据,例如环境仓库中的 Kubernetes minor 基线,而不是让每个仓库自由填写。多环境跨多个 minor 时,对每个受支持目标分别运行,或选择最低受支持 minor 作为兼容门槛并对最高 minor 做 API 迁移检查。不能拿开发集群的 kubectl version 动态结果替代生产目标,因为流水线执行地点不等于部署目的地。
TARGET_KUBERNETES_VERSION='v1.36'
kube-score score \
--kubernetes-version "$TARGET_KUBERNETES_VERSION" \
--output-format ci \
artifacts/rendered.yaml这里的 v1.36 是实验变量,使用者必须替换成自己的受支持目标。证据中保留这个值;若升级集群 minor,要先用新旧目标版本对 fixture 和代表性项目双跑,查看新增、消失与 severity 变化,再决定是否提升门禁。
渲染完整发布单元而不是扫描模板碎片
Helm 模板、values、Kustomize base/overlay、生成器和 post-renderer 共同决定最终对象。kube-score 不会执行模板逻辑;把 Go template 源码直接交给它既可能解析失败,也可能错过条件分支。正确入口是固定渲染工具版本和环境参数,产出一个可审计的 YAML 流,再扫描这个产物。
set -euo pipefail
mkdir -p artifacts
helm template payments ./deploy/chart \
--namespace payments \
--values deploy/values-ci.yaml \
--include-crds > artifacts/rendered.yaml
kube-score score \
--kubernetes-version v1.36 \
--output-format sarif \
artifacts/rendered.yaml > artifacts/kube-score.sarifKustomize 项目对应地执行固定版本的 kustomize build overlays/production。若 Helm 后面还有 post-renderer、GitOps 变换或平台注入,静态门禁应尽量靠近真正提交给 API Server 的最后一个可重复产物。admission mutation 仍可能继续改变对象,所以静态结果不能替代 server-side dry-run 与集群准入证据。
输入摘要应与输出一起保存。把所有相关对象放进同一 YAML 流,尤其是同 namespace 一起部署的 Deployment、Service、NetworkPolicy 和 PodDisruptionBudget。只扫描 Deployment 时,跨对象检查看不到保护对象;把不同环境或不同 namespace 的对象无差别拼接,又可能制造并不存在的关联。
关键字段如何改变检查结论
资源 requests/limits、readiness、liveness、容器 securityContext、Pod securityContext、NetworkPolicy、PDB 和镜像引用是常见输入,但每个字段都有业务语义。requests 参与调度和资源保障,limits 改变 CPU 节流与 OOM 风险;readiness 决定是否接收 Service 流量,liveness 会触发重启;runAsNonRoot 是运行身份约束,不能靠它猜测镜像内用户是否真的兼容。
NetworkPolicy 与 PDB 是独立对象,必须通过 namespace 与 selector 关联工作负载。selector 写错时,“文件里有对象”并不代表工作负载受到保护。静态工具可以提醒缺失或明显问题,却不能发送真实请求证明网络已隔离,也不能制造节点维护证明 PDB 满足可用性目标。
kube-score/ignore 和 kube-score/enable annotation 会改变单个对象的检查集合。默认允许对象自带 ignore,意味着仓库作者可在资源上绕过门禁;--disable-ignore-checks-annotations 可禁止这种绕过。是否允许 annotation 不是风格偏好,而是权限模型:若应用团队可以自行批准风险,就应明确记录;若风险接受必须由平台或安全 owner 审批,CI 应禁止对象内 ignore,改用受保护的中央配置或审查流程。
正向 fixture 要验证可接受的工程语义
先建立一个完整发布单元作为正向 fixture。它包含 Deployment、Service、NetworkPolicy 与 PDB;容器提供资源约束、独立 readiness/liveness 和非 root 安全上下文。镜像使用专门测试制品的 digest,probe 路径由测试服务真实实现。下面只展示核心结构,仓库 fixture 应提供完整可渲染文件:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: payments
spec:
replicas: 3
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
securityContext:
runAsNonRoot: true
containers:
- name: api
image: registry.example.invalid/api@sha256:REPLACE_WITH_TEST_DIGEST
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
readinessProbe:
httpGet: { path: /ready, port: 8080 }
livenessProbe:
httpGet: { path: /live, port: 8080 }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
namespace: payments
spec:
minAvailable: 2
selector:
matchLabels: { app: api }对正向 fixture 运行固定版本命令,预期没有门禁级错误且退出 0。如果仍有 warning,不应为了“全绿”盲目加 ignore;逐条确认是 fixture 缺失、工具启发式限制还是确实需要改进。测试目标不是追求满分,而是建立团队认可且可解释的基线。
反向实验要稳定暴露失败机制
从正向 fixture 复制反例,删除 requests/limits、probe、PDB 与 NetworkPolicy,把 runAsNonRoot 改为 false。保持对象名、namespace、镜像和目标 Kubernetes 版本不变。默认情况下,出现 critical error 时 kube-score 应退出 1;加 --exit-one-on-warning 后 warning 也会让进程退出 1。
set +e
kube-score score \
--kubernetes-version v1.36 \
--output-format json \
fixtures/negative.yaml > artifacts/negative.json
status=$?
set -e
test "$status" -eq 1 || {
printf 'expected kube-score exit 1, got %s\n' "$status" >&2
exit 2
}预期证据是非零退出、对象身份、稳定 check ID 和对应消息。不要把终端颜色或自然语言全文作为唯一断言,因为版本升级可能调整文案。接着给反例添加某项 kube-score/ignore annotation,验证默认行为会隐藏对应检查;再加 --disable-ignore-checks-annotations,同一检查应重新出现。这个实验同时验证了发现机制与绕过控制。
再做一个上下文反例:仅传 Deployment 时记录结果,然后把匹配的 NetworkPolicy 与 PDB 一起传入,比较跨对象发现是否变化。如果结果没有按预期变化,检查 namespace、selector 和 kube-score 支持的对象类型;不要用额外 ignore 掩盖输入组装错误。
CI 用退出码控制动作,用结构化报告保留原因
退出码适合控制流水线,SARIF、JSON、CI 或 JUnit 类输出适合保留原因。CI job 应显式启用 pipefail,否则 kube-score | tee report.txt 可能返回 tee 的成功状态,掩盖前一个进程失败。报告上传步骤即使在检查失败时也要执行,但不能把上传成功改写成扫描成功。
一个可审计 job 按顺序记录渲染工具版本、kube-score 版本、目标 Kubernetes 版本、输入摘要、结构化结果和退出码。结果关联 commit 与环境基线,PR 页面只展示摘要,原始报告进入受控制品存储。规则是:扫描程序异常、输入解析失败和发现配置问题要分型;不能把所有非零都翻译为“安全不合格”。
门禁策略可从 critical error 开始,warning 先观察。--exit-one-on-warning 只有在 warning 已完成误报盘点、owner 明确、修复容量可承受时才启用。批量提升 severity 前先对代表性仓库重放,否则一个工具参数就可能阻断整个组织的交付队列。
项目例外必须能到期和复核
例外记录至少绑定对象身份、check ID、输入 revision、原因、风险、owner、到期条件和替代控制。仅写“该检查不适用”无法帮助下一位维护者判断是否仍成立。对象 annotation 适合让例外跟随资源,但也把批准权交给能修改仓库的人;中央例外适合更严格的治理,却需要稳定的对象键和审查流程。
升级 kube-score 后,检查 ID 或判断逻辑可能变化。例外复核应把旧 check ID 映射到新结果,而不是发现“旧 ID 不再命中”就认定风险消失。删除工作负载时同步删除例外;迁移 namespace 或 chart 名称时更新对象键,防止旧豁免意外匹配新服务。
报告包含命名空间、镜像、资源预算、端口、健康路径和安全上下文,可能暴露内部拓扑与容量信息。SARIF 上传到代码平台前核对仓库可见性和读权限,日志不要打印私有 Registry 凭证、完整 Secret 或包含敏感 values 的渲染文件。kube-score 自身不需要 Secret 值;渲染流程若必须取密,应在输出前以 ExternalSecret/Secret 引用替代明文。
Popeye 只补充运行中集群视角
Popeye 与 kube-score 都提供建议型体检,但输入和维护模型不同。kube-score 面向渲染对象,特别适合 PR 门禁;Popeye 使用 kubeconfig 读取运行中集群,也可作为 one-off 或 CronJob 执行,并能结合部分资源利用率信息生成 console、JSON、HTML 或 Prometheus 指标。它官方声明只读,但仍需要对目标资源的 get/list 权限。
Popeye v0.22.1 是可参考的稳定 release,项目仓库未归档,但 release 节奏比活跃主线更慢。采用时要固定版本、裁剪 ClusterRole、保存原始输出,并设置“目标 Kubernetes API 不再兼容、关键缺陷长期无 release、维护 owner 无法承担”时的退出条件。官方 CronJob 示例使用 --force-exit-zero,这适合定时报告持续产出,不适合作为“没有发现问题”的门禁证据;强制零退出必须与报告内容解析分开。
Popeye 不是 kube-score 的无缝替代。它看到 live object 和部分指标,却不能回到 PR 阶段阻止错误模板;kube-score 能检查待交付清单,却不知道对象在集群中的真实利用率和状态。需要 live 巡检时可把 Popeye 作为补充,核心静态门禁仍由 kube-score 的渲染输入和退出码负责。项目状态与用法从 Popeye 官方仓库 和 v0.22.1 Release 复核。
容量和成本主要落在流水线与证据存储
kube-score 没有集群内控制器,因此没有 Deployment 副本、Leader Election 或 etcd 报告容量问题。它的故障域是 CI Runner、渲染工具、制品存储和代码平台集成。对象数量、YAML 大小、检查集合、并行 PR 数和报告格式决定 CPU、内存、执行时间与存储成本。
测量应使用小、中、大三类真实发布单元,记录渲染时间、扫描时间、峰值内存、报告大小和并行队列等待。不要用单个 Deployment 的毫秒级结果推断包含数千对象的 monorepo。大型仓库可按真正独立的发布单元并行,但不能为了提速把需要跨对象关联的同 namespace 对象拆散。
高可用由 CI 平台承担:Runner 失败可重试,工具镜像从可用 Registry 拉取,报告存储有保留与恢复策略。重试必须固定同一 commit、同一输入摘要和同一工具 digest;否则第二次绿色无法解释第一次失败。外部 Registry 或 GitHub Release 不应成为每次 PR 的实时单点,经过验证的二进制与镜像应进入组织缓存。
双版本升级比“替换二进制”更重要
升级先读取 release notes,把候选版本与当前版本并行运行在 fixture、代表性仓库和历史失败样本上。比较检查 ID、severity、退出码、解析失败与报告 schema;新增发现要分成真实缺陷、目标 Kubernetes 版本变化、启发式变化和输入差异。只有 owner 与修复路径明确后,才切换必需检查。
回滚很直接:恢复旧二进制或镜像 digest、旧 CI 参数和匹配的基线结果。报告 schema 若已被下游消费,还要验证旧版本输出能被解析。由于 kube-score 无持久状态,回滚不涉及数据库,但这不等于零成本;组织级规则变化可能已经产生大量 annotation 例外和关闭的 PR,这些治理副作用需要清理。
卸载时删除本机包、Krew/Homebrew 安装、CI 镜像引用、缓存、workflow job、状态检查要求和报告上传集成。先取消分支保护中的必需 check,再移除 job,避免所有 PR 因等待一个永远不会上报的状态而阻塞。保留期满后删除 SARIF/JSON 制品与缓存,并核对机器人 Token 或上传凭证已经撤销。
若迁移到另一个静态工具,先用同一批渲染输入双跑,建立检查语义映射;总问题数相同不代表规则等价。退出完成的判据是:没有流水线继续调用旧版本,没有分支保护等待旧状态,没有遗留可写凭证,也没有无人负责的永久 ignore。
