Helm CEL 插件:用声明式表达式强化Kubernetes配置验证
1. 项目概述:Helm CEL 插件,为你的 Helm Chart 注入表达力
在 Kubernetes 生态里,Helm 是当之无愧的包管理标准。我们写 Chart,定义
values.yaml
,然后通过
helm install
或
helm upgrade
将应用部署到集群。但不知道你有没有遇到过这样的场景:一个复杂的 Chart,
values.yaml
文件里几十个甚至上百个配置项,你小心翼翼地修改了几个参数,执行
helm upgrade
,结果因为某个值类型不对、范围不对,或者依赖关系没满足,导致整个发布失败,甚至回滚。更头疼的是,Helm 自带的
values.schema.json
(JSON Schema)虽然能做一些基础校验,但它的表达能力有限,写起来也相当繁琐,尤其是涉及到跨字段的逻辑校验时,简直让人抓狂。
这就是
idsulik/helm-cel
这个项目要解决的问题。它是一个 Helm 插件,用
Common Expression Language (CEL)
来替代或增强 JSON Schema,为你的 Helm Chart 提供强大、灵活且极具表达力的值验证能力。简单来说,它让你能用一种接近编程语言的、声明式的方式来定义你的配置规则。比如,你可以轻松写出这样的规则:“如果
service.type
是
NodePort
,那么
nodePort
必须在 30000-32767 之间,否则
nodePort
字段必须为空”。这种条件逻辑,用原生的 JSON Schema 实现起来非常别扭,而用 CEL 则是一行表达式的事。
我是在为一个内部中间件 Chart 设计复杂的配置验证时发现这个工具的。当时我们要求某些配置项必须成对出现,某些数值范围依赖于另一个配置项的值,用 JSON Schema 写出来的验证逻辑又长又难维护。直到用了
helm-cel
,整个配置验证的清晰度和可维护性都上了一个台阶。接下来,我就结合自己的使用经验,带你深入了解一下这个工具,从设计思路到实战避坑,希望能帮你把 Helm Chart 的配置管理提升到一个新的水平。
2. 核心设计思路:为什么是 CEL?
在深入使用之前,我们得先搞清楚,为什么这个插件选择 CEL 作为验证语言,而不是其他选择,比如 Rego(Open Policy Agent 用的)、或者直接写 Go 插件。
2.1 CEL 是什么?它解决了什么问题?
CEL 的全称是 Common Expression Language,最初由 Google 设计,用于在其安全策略和配置验证场景中提供一种安全、可移植、高性能的表达式语言。它的核心设计目标有几个:
- 安全性 :CEL 表达式是在一个沙箱环境中求值的,它无法访问文件系统、网络或进行任何有副作用的操作。这对于验证来自不可信来源(如用户输入)的数据至关重要。
- 确定性 :相同的输入和表达式,总是产生相同的输出,没有随机性。
- 高性能 :CEL 表达式通常会被编译成可执行的中间代码,执行速度很快。
- 强类型 :CEL 是静态类型的,支持整数、浮点数、字符串、布尔值、列表、映射等基本类型,并且有清晰的类型检查规则。
在 Kubernetes 领域,CEL 已经被广泛采纳。最著名的例子就是 Kubernetes 1.25 引入的
Validating Admission Policy
,它允许你使用 CEL 来编写准入控制策略,而不再仅仅依赖 Webhook。这意味着
helm-cel
的设计理念与 Kubernetes 社区的发展方向是一致的。你用 CEL 为 Helm Chart 写的验证规则,其语法和思想可以很自然地迁移到集群侧的准入策略中,形成配置管理的“左移”(在部署前验证)和“右移”(在准入时验证)的统一。
2.2 与 JSON Schema 的对比:从“是什么”到“应该怎样”
Helm 原生的
values.schema.json
主要回答的问题是:“这个配置项
是什么
类型?” 它是一个结构描述器。
- 优点 :标准、简单,对于基础类型和格式校验(如字符串格式、枚举值)很有效。
-
缺点
:表达能力弱。很难(或非常冗长)表达“
应该怎样
”的逻辑。例如:
- “字段 A 和字段 B 不能同时存在。”
-
“当字段 X 为
true时,字段 Y 必须被设置且大于 10。” - “这个列表里的所有元素都必须满足某个条件。”
helm-cel
的
values.cel.yaml
则专注于回答:“这些配置值
应该怎样
才有效?” 它是一个逻辑断言器。
-
优点
:极强的逻辑表达能力。你可以使用
&&(与)、||(或)、!(非)等逻辑运算符,以及丰富的标准库函数(字符串处理、列表操作、类型检查等),构建复杂的业务规则。 -
缺点
:不擅长描述纯粹的数据结构。它通常需要和
values.schema.json(或 Chart 的默认结构)配合使用,一个定义骨架,一个定义血肉规则。
实操心得:混合使用策略 在我的项目中,我通常两者都用。
values.schema.json用来保证最基本的类型安全和必填字段,比如image.repository必须是字符串。而所有复杂的业务逻辑校验,比如资源配额合理性、端口冲突检查、环境特定的约束,全部交给values.cel.yaml。这样分工明确,各司其职。
2.3 插件架构浅析
helm-cel
本身是一个用 Go 编写的标准 Helm 插件。安装后,它会向 Helm CLI 注册一个
cel
子命令。当你运行
helm cel validate
时,插件会做以下几件事:
-
加载与合并
:加载指定的
values.yaml文件(支持多个,按顺序合并),形成一个完整的配置值对象。 -
加载规则
:加载指定的
values.cel.yaml文件(同样支持多个),解析其中的rules和expressions。 -
编译与求值
:使用 Go 的 CEL 库(
google/cel-go)将每条规则的expr字符串编译成可执行程序。然后,将上一步得到的配置值对象作为输入,对每条规则进行求值。 -
输出结果
:根据求值结果(
true或false)和规则的severity,生成成功、警告或错误信息,并以相应的退出码结束。
这个过程是纯函数式的,没有副作用,非常适合集成到 CI/CD 流水线中。
3. 从安装到上手:快速构建验证防线
3.1 多种安装方式及其适用场景
项目提供了几种安装方式,选择哪种取决于你的使用环境。
1. 标准安装(推荐给所有 Helm 用户) 这是最直接的方式,使用 Helm 自带的插件管理器。
helm plugin install https://github.com/idsulik/helm-cel
安装完成后,直接运行
helm cel --help
验证是否成功。这种方式将插件安装在你的本地
$HELM_PLUGINS
目录下,与你的 Helm 环境完全集成。
2. Docker 方式(适合 CI/CD 或隔离环境) 如果你在 Docker 容器内运行 CI 任务,或者不想污染本地环境,Docker 镜像是很好的选择。
# 拉取最新镜像
docker pull idsulik/helm-cel:latest
# 或指定版本,生产环境推荐用固定版本
docker pull idsulik/helm-cel:2.1.2
使用方式略有不同,你需要将你的 Chart 目录挂载到容器内:
docker run --rm -v $(pwd)/my-chart:/charts idsulik/helm-cel:latest validate /charts
这种方式确保了验证环境的一致性,非常适合在 Jenkins、GitLab CI 等流水线中使用。
3. 从源码构建(适合开发者或需要定制) 如果你想了解内部机制、调试问题或参与贡献,可以从源码构建。
git clone https://github.com/idsulik/helm-cel
cd helm-cel
# 确保已安装 Go 1.22+
make install
make install
会编译并将插件安装到你的 Helm 插件目录。这对于测试新功能或修复特定问题非常有用。
注意事项:网络问题 在国内环境,直接从 GitHub 安装插件或拉取 Docker 镜像可能会遇到网络延迟。对于 Docker 方式,可以考虑先将镜像推送到内部的镜像仓库。对于
helm plugin install,如果失败,可以尝试先git clone仓库到本地,然后使用helm plugin install /local/path/to/helm-cel进行本地安装。
3.2 创建你的第一个验证规则
假设我们有一个最简单的 Chart,用于部署一个 Web 服务,它的
values.yaml
如下:
replicaCount: 2
image:
repository: nginx
tag: "1.21"
service:
type: ClusterIP
port: 80
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
我们想添加一些验证规则:
-
replicaCount必须至少为 1。 -
image.tag不能是latest(避免浮动标签导致的不确定性)。 -
service.port必须在 1-65535 之间。 -
如果设置了资源限制(
limits),那么请求(requests)也必须设置,且limits必须大于等于requests。
在 Chart 根目录下创建
values.cel.yaml
文件:
rules:
# 规则 1: 副本数校验
- expr: "values.replicaCount >= 1"
desc: "replicaCount must be at least 1"
# 规则 2: 镜像标签校验
- expr: 'values.image.tag != "latest"'
desc: "image.tag cannot be 'latest' for production stability"
# 规则 3: 服务端口校验
- expr: "values.service.port >= 1 && values.service.port <= 65535"
desc: "service.port must be a valid port number"
# 规则 4: 资源请求与限制的关联校验
- expr: >
!has(values.resources.limits) ||
(
has(values.resources.requests) &&
has(values.resources.limits.memory) &&
has(values.resources.limits.cpu) &&
// 注意:这里需要解析字符串如 "128Mi",实际项目中可能需要更复杂的逻辑
// 此处简化为例,假设它们已是可比较的数字
values.resources.limits.memory >= values.resources.requests.memory &&
values.resources.limits.cpu >= values.resources.requests.cpu
)
desc: "If resources.limits are set, then resources.requests must also be set and limits must be >= requests"
注意 :规则 4 的表达式比较复杂,使用了
>YAML 块标量符号来书写多行表达式,并使用了has()函数来检查字段是否存在。在实际中,比较"128Mi"这样的字符串需要先转换,我们稍后在高级用法里会讲到。
现在,运行验证:
helm cel validate .
如果一切配置正确,你会看到绿色的
✅ Values validation successful!
输出。
3.3 基础命令详解
helm cel
有两个核心子命令:
validate
和
generate
。
validate
命令
:这是最常用的命令,用于验证配置。
-
--values-file, -v: 指定要验证的 values 文件。默认是values.yaml。你可以指定多个,用逗分隔或多次使用-v标志。 文件顺序很重要 ,后面的文件会覆盖前面的同名配置,这与helm install -f file1.yaml -f file2.yaml的行为一致。 -
--rules-file, -r: 指定规则文件。默认是values.cel.yaml。同样支持多个文件,所有规则会被合并,但表达式名必须唯一。 -
--output, -o: 输出格式。text(默认,人类可读)、json或yaml(机器可读,用于 CI)。
一个复杂的验证命令可能长这样:
helm cel validate ./my-chart \
-v values.yaml \
-v environments/prod/overrides.yaml \
-r values.cel.yaml \
-r rules/network.cel.yaml \
-r rules/security.cel.yaml \
-o json
这个命令会合并
values.yaml
和
overrides.yaml
得到最终值,然后应用三个规则文件中的所有规则,并以 JSON 格式输出结果。
generate
命令
:这是一个辅助命令,可以根据现有的
values.yaml
文件结构,自动生成一个基础的
values.cel.yaml
规则骨架。这对于快速开始或为一个遗留 Chart 添加基础验证很有用。
helm cel generate ./my-chart --force
--force
会覆盖已存在的
values.cel.yaml
文件。生成的内容主要是基于字段存在性的检查(
has(values.xxx)
),你需要在此基础上补充具体的业务逻辑规则。
4. 规则编写实战:从基础到高级模式
掌握了基本命令后,我们来深入探讨如何编写强大而清晰的 CEL 验证规则。这是
helm-cel
的核心价值所在。
4.1 规则文件结构与组织
一个规则文件的基本结构如下:
# 可选的表达式定义区,用于定义可复用的子表达式
expressions:
isValidPort: "values.port >= 1 && values.port <= 65535"
isValidHostname: 'values.hostname.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$")'
# 核心规则区
rules:
- expr: "has(values.requiredField)" # 使用原生表达式
desc: "requiredField must be set"
severity: error # 可选,默认是 error
- expr: "${isValidPort}" # 引用自定义表达式
desc: "Port must be valid"
- expr: 'values.type == "external" ? ${isValidHostname} : true' # 条件逻辑中使用
desc: "Hostname must be valid if service is external"
规则组织的最佳实践
:不要把所有规则都堆在一个
values.cel.yaml
文件里。随着 Chart 变复杂,规则会变得难以管理。建议按功能模块拆分:
my-chart/
├── Chart.yaml
├── values.yaml
└── cel/
├── 00-required.cel.yaml # 全局必填字段校验
├── 10-resources.cel.yaml # 资源配额校验
├── 20-network.cel.yaml # 网络、服务、Ingress校验
├── 30-storage.cel.yaml # 存储卷校验
└── 99-custom.cel.yaml # 业务特定逻辑校验
然后在验证时通过
-r cel/*.cel.yaml
来加载所有规则。注意文件名前缀数字可以帮助控制加载顺序(虽然规则本身没有顺序依赖,但这样更清晰)。
4.2 常用 CEL 函数与技巧
CEL 提供了丰富的标准函数库。以下是在 Helm Chart 验证中最常用的一些:
1. 存在性检查 (
has()
)
这是最基础的函数,用于检查一个字段是否存在于 values 映射中。
- expr: "has(values.image.repository)"
desc: "Docker image repository is required"
检查嵌套字段:
- expr: "has(values.image) && has(values.image.repository)"
desc: "Image repository is required"
# 更简洁的写法(如果 values.image 可能不存在,会安全地返回 false)
- expr: "has(values.image.repository)"
desc: "Image repository is required"
2. 类型检查 (
type()
)
确保字段是预期的类型。
- expr: "type(values.replicaCount) == int"
desc: "replicaCount must be an integer"
- expr: "type(values.tolerations) == list"
desc: "tolerations must be a list"
- expr: 'type(values.nodeSelector) == map'
desc: "nodeSelector must be a map/dictionary"
3. 字符串操作 (
matches()
,
contains()
,
startsWith()
)
正则表达式是验证字符串格式的利器。
# 验证镜像标签格式(类似 v1.2.3)
- expr: 'values.image.tag.matches("^v?[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9]+)?$")'
desc: "Image tag must follow semantic versioning pattern"
# 验证 Kubernetes 标签值
- expr: 'values.appLabel.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") && size(values.appLabel) <= 63'
desc: "appLabel must be a valid Kubernetes label value"
# 检查是否包含子字符串
- expr: '!values.image.repository.contains("localhost/")'
desc: "Image repository should not point to localhost in production"
4. 列表与映射操作 (
size()
,
all()
,
exists()
,
map()
)
处理数组和对象结构。
# 检查列表长度
- expr: "size(values.extraEnv) <= 20"
desc: "Cannot have more than 20 extra environment variables"
# 检查列表中所有元素是否满足条件 (all)
- expr: "values.containers.all(c, has(c.image))"
desc: "All containers must have an image specified"
# 检查列表中是否存在满足条件的元素 (exists)
- expr: 'values.ingress.hosts.exists(h, h.host.matches("^.*\\.example\\.com$"))'
desc: "At least one ingress host must be under example.com domain"
# 对列表元素进行转换和检查 (map)
- expr: "values.ports.map(p, p.containerPort).all(port, port >= 30000 && port <= 32767)"
desc: "All containerPorts in NodePort range must be between 30000 and 32767"
5. 数字与比较运算 基本的数学和逻辑运算。
- expr: "values.replicaCount >= 1 && values.replicaCount <= 10"
desc: "replicaCount must be between 1 and 10"
# 百分比检查
- expr: "values.autoscaling.targetCPUUtilizationPercentage >= 50 && values.autoscaling.targetCPUUtilizationPercentage <= 90"
desc: "CPU utilization target must be between 50% and 90%"
# 利用三元运算符进行条件逻辑
- expr: 'values.service.type == "NodePort" ? values.service.nodePort >= 30000 && values.service.nodePort <= 32767 : true'
desc: "NodePort must be in range 30000-32767 when service type is NodePort"
4.3 高级验证模式示例
让我们看几个更贴近真实场景的复杂例子。
示例1:资源请求与限制的关联性及格式校验
Kubernetes 资源请求和限制的格式如
“128Mi”
、
“0.5”
,需要解析。我们可以写一个辅助表达式来转换。
expressions:
# 将 Kubernetes 内存字符串(如 "128Mi", "2Gi")转换为以 Mi 为单位的整数
memoryToMi: |
(mem) ->
mem.endsWith('Mi') ? int(mem.replace('Mi', '')) :
mem.endsWith('Gi') ? int(mem.replace('Gi', '')) * 1024 :
mem.endsWith('Ki') ? int(mem.replace('Ki', '')) / 1024 :
/*** 这里可以扩展更多单位 ***/
-1 # 无效格式返回 -1
rules:
- expr: >
!has(values.resources) ||
!has(values.resources.limits) ||
(
has(values.resources.requests) &&
has(values.resources.limits.memory) &&
has(values.resources.limits.cpu) &&
has(values.resources.requests.memory) &&
has(values.resources.requests.cpu) &&
${memoryToMi}(values.resources.limits.memory) >= ${memoryToMi}(values.resources.requests.memory) &&
// 注意:CPU 的解析更复杂("100m" -> 0.1),此处简化,假设已是数字或可解析字符串
double(values.resources.limits.cpu.replace('m', '')) / 1000.0 >= double(values.resources.requests.cpu.replace('m', '')) / 1000.0
)
desc: "If limits are set, requests must also be set and limits must be >= requests"
注意 :上面的
memoryToMi表达式是一个 lambda 函数,但请注意,当前版本的 CEL 可能对复杂 lambda 的支持有限,且helm-cel的表达式引用通常用于布尔表达式片段。对于这种复杂的转换,更稳妥的做法是在规则外部预处理,或者使用多个规则分步校验格式和大小。这里主要是展示思路。
示例2:Ingress 主机与 TLS 证书的配对检查
rules:
# 规则1: 如果指定了 TLS,那么每个 TLS 条目必须有对应的 hosts
- expr: >
!has(values.ingress) ||
!has(values.ingress.tls) ||
size(values.ingress.tls) == 0 ||
values.ingress.tls.all(tlsEntry,
has(tlsEntry.hosts) &&
size(tlsEntry.hosts) > 0 &&
tlsEntry.hosts.all(host, host != "")
)
desc: "Each TLS configuration must have a non-empty list of hosts"
# 规则2: 如果启用了 TLS,且 hosts 列表不为空,那么每个 host 都应该在 TLS 配置中找到(或使用通配符匹配)
# 这是一个简化版,实际可能需要更复杂的匹配逻辑
- expr: >
!has(values.ingress) ||
!has(values.ingress.hosts) ||
size(values.ingress.hosts) == 0 ||
!has(values.ingress.tls) ||
size(values.ingress.tls) == 0 ||
values.ingress.hosts.all(hostObj,
values.ingress.tls.exists(tlsEntry,
tlsEntry.hosts.exists(tlsHost, tlsHost == hostObj.host || tlsHost == "*.example.com") # 示例通配符
)
)
desc: "All ingress hosts should be covered by a TLS certificate"
severity: warning # 可能只是警告,因为可能有默认证书
示例3:亲和性与反亲和性的约束
rules:
# 检查 podAntiAffinity 的 requiredDuringSchedulingIgnoredDuringExecution 不能同时指定 labelSelector 和 topologyKey 为空
- expr: >
!has(values.affinity) ||
!has(values.affinity.podAntiAffinity) ||
!has(values.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution) ||
values.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution.all(req,
has(req.topologyKey) && req.topologyKey != "" &&
has(req.labelSelector) &&
has(req.labelSelector.matchExpressions)
)
desc: "Pod anti-affinity required rules must have both topologyKey and labelSelector.matchExpressions"
4.4 错误信息与严重级别
每条规则都可以指定一个
severity
:
-
error(默认):验证失败将导致整个验证过程失败,退出码为 1。用于必须遵守的规则。 -
warning:验证失败会显示警告信息,但验证结果仍为成功(有警告),退出码为 2。用于推荐性、最佳实践或可能有问题但非阻塞的规则。
合理使用
warning
非常重要。例如,你可以将“生产环境不应使用
latest
标签”设置为
warning
,这样开发环境可以快速测试,而 CI 流水线会发出提醒但不会阻断部署。
rules:
- expr: 'values.image.tag == "latest"'
desc: "Using 'latest' tag is not recommended for production"
severity: warning
5. 集成到 CI/CD 流水线:让验证成为部署守门员
helm-cel
的真正威力在于将其集成到自动化流程中。它清晰的退出码(0成功,1错误,2警告)和结构化输出(JSON/YAML)使其成为 CI/CD 流水线的理想选择。
5.1 基础 Git Hooks 集成
在团队开发中,可以在 Git 的
pre-commit
或
pre-push
钩子中加入验证,确保提交到仓库的 Chart 配置都是有效的。
一个简单的
.pre-commit-config.yaml
配置示例:
repos:
- repo: local
hooks:
- id: helm-cel-validate
name: Validate Helm Chart values with CEL
entry: bash -c 'for chart in $(find . -name "Chart.yaml" -type f | xargs dirname); do echo "Validating $chart"; helm cel validate "$chart" || exit 1; done'
language: system
files: '^.*/values\.yaml$|^.*/values\.cel\.yaml$'
pass_filenames: false
这个钩子会在你提交任何
values.yaml
或
values.cel.yaml
文件时,递归地查找并验证项目中的所有 Helm Chart。
5.2 GitHub Actions 工作流示例
在 GitHub Actions 中,你可以创建一个专门的验证工作流,或者将其作为测试工作流的一部分。
name: Validate Helm Charts
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: 'latest'
- name: Install helm-cel plugin
run: |
helm plugin install https://github.com/idsulik/helm-cel
- name: Find and validate all Helm charts
run: |
# 查找所有 Chart.yaml 文件
find . -name 'Chart.yaml' -type f | while read chart_file; do
chart_dir=$(dirname "$chart_file")
echo "🔍 Validating chart in: $chart_dir"
# 运行验证,捕获输出和退出码
if output=$(helm cel validate "$chart_dir" -o json 2>&1); then
exit_code=$?
echo "$output" | jq -r '.result.errors[]? | "❌ ERROR: \(.description) (path: \(.path))"' >&2
echo "$output" | jq -r '.result.warnings[]? | "⚠️ WARNING: \(.description) (path: \(.path))"' >&2
if [[ $exit_code -eq 1 ]]; then
echo "❌ Validation failed with errors for $chart_dir"
exit 1
elif [[ $exit_code -eq 2 ]]; then
echo "⚠️ Validation passed with warnings for $chart_dir"
# 可以选择让 warnings 也导致失败,取决于团队策略
# exit 1
else
echo "✅ Validation successful for $chart_dir"
fi
else
echo "❌ Failed to run validation for $chart_dir"
exit 1
fi
done
这个工作流会检查仓库中的所有 Chart,并以 JSON 格式解析输出,分别处理错误和警告。你可以根据团队策略决定是否让警告(
exit code 2
)也导致工作流失败。
5.3 GitLab CI 流水线示例
对于 GitLab CI,配置也类似:
stages:
- validate
helm-cel-validation:
stage: validate
image: alpine/helm:latest
before_script:
- apk add --no-cache git
- helm plugin install https://github.com/idsulik/helm-cel
script:
- |
find . -name 'Chart.yaml' | while read chart; do
chart_dir=$(dirname "$chart")
echo "Validating $chart_dir"
helm cel validate "$chart_dir" || {
# 如果 exit code 是 1 (错误),则失败
if [ $? -eq 1 ]; then
echo "Validation failed with ERRORS for $chart_dir"
exit 1
fi
# 如果 exit code 是 2 (警告),
更多推荐
所有评论(0)