模板引擎 — 造出字符串,再装成 YAML
一句话总结
Helm 并不编辑 YAML。它使用 Go 模板生成字符串,全部生成完毕后,才把结果解析为 YAML。
为什么需要了解这些
不理解这句话,可能会浪费一整天。明明加入了 resources 块,渲染结果中却整块消失;或者只填入一个值,解析器就报错。原因几乎总是缩进。对于模板引擎,YAML 只是普通文本,错开两个空格的块不是错误,而是一个含义不同的文档。
因此,熟练使用 Helm 并不在于记住大量函数,而在于始终意识到“字符串会以什么形状插入当前位置”。toYaml、nindent、default、required 四项足以覆盖八成实际工作,也正是因为这种思维方式。
工作原理
模板处理分为解析和执行两个阶段。解析阶段将模板文本转换为语法树,执行阶段则应用数据上下文(用一个点表示的对象),生成最终字符串。执行期间可用的内置对象包括:.Values(默认值与用户值的合并结果)、.Release(名称、命名空间、修订号)、.Chart(Chart.yaml 内容)、.Capabilities(集群支持的 API)、.Files、.Template。
核心函数可以这样分类。
| 函数 | 何时使用 | 陷阱 |
|---|---|---|
default |
值为空时指定替代值 | 把默认值设为 latest 会造成不可复现的发布 |
required |
缺少值时让渲染直接失败 | 错误消息必须说明“缺少什么”才有用 |
toYaml |
将整个映射或列表展开为字符串 | 单独使用时缩进不匹配 |
nindent |
先添加换行,再缩进 n 个空格 | indent 不添加换行,第一行会粘到前一个键后面 |
range |
展开列表或映射 | 遍历映射时键会排序,结果具有确定性 |
include |
调用具名模板 | template 会直接输出结果,无法接入管道 |
template 与 include 的区别看似细小,却至关重要。template 会把渲染结果直接输出到当前位置,因此无法在后面添加管道。include 则将结果作为字符串返回,所以可以继续使用 | nindent 4 等后处理。标签块等需要缩进的位置都使用 include,原因就在这里。
值的来源也有明确优先级。由弱到强依次为:Chart 的 values.yaml → 通过 -f 指定的值文件(从左到右)→ --set。还要记住一点:映射会深度合并,列表则会被整体替换。 如果把环境变量设计成列表,就无法通过一个值文件只修改其中一项。因此,经常需要覆盖的值更适合设计成映射。
最后还有一项惯例。ConfigMap 内容改变后,Pod 仍会保持不变,因为 Pod 规约没有变化,也就不会触发滚动更新。因此,应在 Pod 模板注解中以 checksum/config 保存配置文件的哈希。内容变化会导致哈希变化,哈希变化又会改变 Pod 规约,从而自然触发滚动更新。
生产现场中的常见情况
第一,消失的块。 如果 resources 或 nodeSelector 完全没有出现在渲染结果中,可能是值为空,也可能是缩进错误。单个字段出错通常会报错,但整个块缩进错位时,它可能悄悄附着到其他位置或直接消失。此时,肉眼检查 helm template 输出是唯一的诊断方法。
第二,lookup 的陷阱。 用于查询集群的 lookup 函数,在 helm template 中始终返回空结果。这是本地运行正常的条件语句在实际安装时表现不同的典型原因。
第三,可复现性。 在模板中使用 now 或随机函数,会让每次渲染结果都不同,使每次发布都被识别为发生变更。使用 GitOps 工具时,会导致同步永远无法完成。
模板悄无声息出错的位置
Helm 模板是生成字符串的工具,因此只要语法正确,即使含义错误也能通过。 应固定检查以下四种常见问题。
缩进错位。 toYaml 的结果不带缩进,因此要用
nindent 对齐。indent 与 nindent 的区别在于是否在前面添加换行。
resources:
{{- toYaml .Values.resources | nindent 2 }}
空值与不存在的值不同。 如果 .Values.foo 不存在,就会得到 <no value>,
而这个字符串会原样进入 YAML。应使用 required 阻止渲染,或通过 default 填充。
image: {{ required "image.repository 가 필요합니다" .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}
数字与字符串发生混淆。 YAML 会把 1.0 读取为数字,把 "1.0" 读取为字符串。
如果标签为 1.10,作为数字读取后会变成 1.1,这种事故确实发生过。标签、端口等必须是字符串的值应添加 quote。
tag: {{ .Values.image.tag | quote }}
if 与 with 的作用域不同。 with 会改变 .,因此在其内部不能直接使用
.Values 或 .Release。此时应通过 $ 引用顶层作用域。
{{- with .Values.ingress }}
host: {{ .host }}
release: {{ $.Release.Name }}
{{- end }}
**验证分为三个阶段:**语法、结果,以及与实际集群之间的差异。
helm lint .
helm template . -f values-prod.yaml | kubeconform -strict -
helm template . -f values-prod.yaml | kubectl diff -f -
helm template 不会查看集群,因此 lookup 函数会返回空值。
依赖该函数的模板,不能只凭渲染结果作出判断。
下一次实验要做什么
在 /root/helm/tpl/labhub-api Chart 中,用 default 填补空值,并通过 required 强制要求必填值,故意触发一次失败。使用 toYaml 和 nindent 整体传入资源块,通过 range 展开环境变量。同时应用两个值文件与 --set,确认最终由谁覆盖谁;最后完成包含配置哈希和安全上下文的完整渲染结果。