TT Lab
开始
学习 学习路径 课程

Helm Chart 的制作与发布

点号会变、行会粘连——模板事故的两个根源

在 TT Lab 中继续学习

一句话总结

模板事故大部分不是来自语法,而是来自两件事——with、range 会改变点所指向的对象,以及空白修剪会把换行抹掉。

为什么这会成为问题

Helm 模板里出的错通常是这个样子。

Error: YAML parse error on frontier/templates/portal.yaml:
error converting YAML to JSON: yaml: line 5: mapping values are not allowed in this context

这条消息是 YAML 解析器说的。它是在把模板全部渲染完之后试图把结果读成 YAML 时失败的,所以消息里写的行号是渲染结果的行号,不是模板文件的行号。因此无论怎么看模板第 5 行,都看不出问题。罪魁祸首是三行之上标签末尾的 -}} 两个字符,而这两个字符在错误里哪里都没出现。

从这里走出来的路只有一条。直接看坏掉的结果。 helm template --debug 会把无法解析为 YAML 的结果也原样输出。看输出,一眼就能看到 annotations: 之后注释块接在同一行上,接着 labels: 也粘在上一行的末尾。推测原因的事,变成了观察。

点在每个块里都会改变

with 和 range 不是便利语法,而是替换上下文的块。

{{- with .Values.app }}
data:
  team: {{ .team }}
  release: {{ .Release.Name }}
{{- end }}

在块内,. 不再是根,而是 .Values.app。所以能找到 .team,却没有 .Release。Helm 会这样说。

nil pointer evaluating interface {}.Name

这条消息也没有指出原因。它的意思是 .Release 是 nil,而人看来 .Release 总是存在的,所以不会去怀疑。解法是指向根的变量 $。$ 绑定在模板开始时的上下文上,在任何块内都不会改变。写成 {{ $.Release.Name }} 就行了。

range 也一样。像 range $i, $e := .Values.envs 这样接收变量,就可以安全地使用索引和元素,根的值用 $.Values... 取出。如果不接收变量,只用 .,一旦在里面又需要根的值,就卡住了。

indent 与 nindent,以及空白标记

{{- 会删除标签之前的空白和换行,-}} 会删除标签之后的空白和换行。大多数事故出在后面,因为下一行会整个粘到上一行上。

indent 与 nindent 的区别也在同一条轴上。

函数 作用 使用位置
indent 4 在每行前面加四个空格 已经换行的位置
nindent 4 先加换行再加四个空格 在键的正下方插入块时

在 annotations: 下面插入一个 map 的位置,几乎总是 nindent。在这里用 indent,块的第一行就会粘在 annotations: 所在的行上,生成 annotations: owner: sre 这样的东西,YAML 解析器会以“mapping values are not allowed in this context”拒绝它。

去掉引号,值的类型就变了

第三个陷阱是在渲染成功之后才爆的。

data:
  tag: {{ .Values.release.tag }}       # values 에는 "1.10"
  enabled: {{ .Values.release.enabled }}  # values 에는 "no"

该代码块中的两处韩文注释说明:values 中的值分别是 "1.10" 和 "no"。

渲染结果是 tag: 1.10 和 enabled: no。引号消失了,所以这些值不再是字符串。使用 YAML 1.1 规则的解析器会把 no 读成假,被读成数字的 1.10 会丢掉末尾的 0,变成 1.1。ConfigMap 的 data 只接收字符串,所以 API 服务器会拒绝。

cannot unmarshal bool into Go struct field ConfigMap.data of type string

镜像标签、版本、电话号码、国家代码这类人按字符串处理的值,在模板中经过 quote 是基本做法。反过来,在必须是数字的位置(replicas、port)加上 quote,会在那边发生同类拒绝。

在现场相遇的样子

这三件事通常在部署流水线的不同位置被抓住。范围和空白事故在渲染时立刻被抓住,而引号事故会一直活到集群收到它为止。所以只运行 helm template 看“能渲染啊”就过去的话,就会在最晚的位置爆出来。很多团队在 CI 里加一行 helm template | kubectl apply --dry-run=server 就是这个原因——由真正的 API 服务器来检查 schema 和类型。

部署前拦住值本身的机制也要一起使用。required 在值缺失时、fail 在值不合理时,会让渲染停下。两者的消息都由人来写,所以在里面写上“该怎么改”,故障时读到的人就能立刻行动。而且 helm lint --strict 会把默认 lint 只当作警告的东西(比如含大写字母的对象名称)变成失败。亲眼看一次同一个 Chart 用 helm lint 以 0 结束、用 --strict 以 1 结束,就会清楚 CI 里该挂哪一个。

下一项实验要做什么

亲手制造 with 内找不到 .Release 的错误,并用 $ 修好。让 range 接收变量,同时使用索引和根的值。故意写错空白修剪把渲染搞坏,用 --debug 读坏掉的结果,再用 nindent 修好。确认没有引号的值会被 API 服务器拒绝,最后用 required、fail、helm lint --strict 拦住同样的事故,使之到不了部署。