CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
模板与 values
这一课你将能做到
用 .Values、if/with/range、include+nindent 写出可渲染的 Deployment/Service 模板,关键行能解释为什么这么写
用 helm template(不需要集群)对比同一 Chart 在 dev/prod 两份 values 下的输出差异,说清 -f 与 --set 的覆盖顺序
遇到「模板渲染出无效 YAML」时,用 --debug 与缩进规则定位并修复
为什么要学
Chart 的灵魂是 Go template + YAML :Helm 在安装/升级前把默认 values、各层 -f 文件、--set 参数合并成一份 .Values,再驱动模板渲染出最终清单交给 API。写错缩进、漏引号、条件分支吐出半截映射,都会变成「看起来像 Helm 的错,其实是 YAML 坏了」。本课练的是 可读、可调试 的模板——够用且清晰,而不是堆砌过滤器炫技。学完后你应能不看答案写出 backend 的 Deployment/Service,并用 helm template 自检。
先建立图像
values.yaml(默认) + values-dev.yaml(-f) + --set(可选)
\ | /
▼ ▼ ▼
合并后的 .Values(越靠后优先级越高)
+
.Release / .Chart / .Capabilities / .Files
│
▼
templates/*.yaml(Go template 引擎)
│
▼
纯 YAML 清单 → 交给 API,或仅 helm template 预览
内置对象 人话 常用字段 .Values合并后的旋钮表 .Values.image.tag、.Values.replicaCount.Release这次安装实例 .Release.Name、.Release.Namespace.Chart包元数据 .Chart.Name、.Chart.Version.Capabilities集群能力探测 .Capabilities.KubeVersion(进阶)
人话:模板里写的是「填空题」;helm template 是交卷前自己先誊一遍答案,不用集群就能检查。
跟着做
步骤 1:准备三份 values
values.yaml(Chart 默认):
replicaCount: 1
image:
repository: registry.example/coderoadmap-backend
tag: "1.0.0"
service:
port: 8080
db:
host: mysql.coderoadmap-dev.svc
passwordSecretName: backend-db-secret
# 空对象 = 可选块;模板里 with 条件渲染
resources: {}
replicaCount: 1
image:
tag: "1.0.0-dev"
replicaCount: 2
image:
tag: "1.0.0"
db:
host: mysql.prod.internal
resources:
limits:
cpu: "500m"
memory: 512Mi
步骤 2:写 Deployment 模板 templates/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
# helpers 统一命名,避免手写拼错
name: {{ include "coderoadmap-backend.fullname" . }}
labels:
{{- include "coderoadmap-backend.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "coderoadmap-backend.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "coderoadmap-backend.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: api
# 仓库与 tag 分字段;升级通常只改 tag
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
env:
- name: SPRING_DATA_REDIS_HOST
value: redis.coderoadmap.svc
- name: DB_HOST
value: {{ .Values.db.host | quote }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.db.passwordSecretName }}
key: password
{{- with .Values.resources }}
# with:仅当 resources 非空时才渲染整个 resources 块
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
步骤 3:补 Service 模板 apiVersion: v1
kind: Service
metadata:
name: {{ include "coderoadmap-backend.fullname" . }}
labels:
{{- include "coderoadmap-backend.labels" . | nindent 4 }}
spec:
type: ClusterIP
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.port }}
protocol: TCP
selector:
{{- include "coderoadmap-backend.selectorLabels" . | nindent 4 }}
确认 selector 与 Pod 标签来自 同一个 selectorLabels helper——不一致 Service 就选不中 Pod。
步骤 4:写 helpers 最小集 {{- define "coderoadmap-backend.fullname" -}}
{{- printf "%s-backend" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "coderoadmap-backend.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- define "coderoadmap-backend.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
注意 include 必须传上下文 .,否则 helper 里访问不到 .Release/.Chart。
步骤 5:渲染并对比环境 helm template myrel . -f values-dev.yaml > dev.yaml
helm template myrel . -f values-prod.yaml > prod.yaml
打开对比:dev 副本 1、tag 1.0.0-dev、无 resources 块;prod 副本 2、有 resources 限额、db.host 指向内网。再验证覆盖顺序:
helm template myrel . -f values-prod.yaml --set image.tag=9.9.9 | grep "image:"
# 输出 9.9.9 —— --set 优先级高于 -f 文件
结论写进笔记:优先级 默认 values < 先传的 -f < 后传的 -f < --set ;多层 -f 时后者覆盖前者。
步骤 6:故意制造错误再修复 把 resources 块的缩进打乱(或删掉 nindent 前的 -),然后:
helm template myrel . -f values-prod.yaml --debug
读报错信息定位行号 → 修复 → 重新渲染通过。体会口诀:「渲染报错先怀疑 YAML 结构与空白,再怀疑函数与数据。」
讲清楚要点
因为渲染结果必须是合法 YAML,所以空白控制与模板逻辑同等重要 ——{{- 与 -}} 吃掉紧邻的空白/换行;indent/nindent 把 helper 输出按层级缩进(nindent 先换行再缩进,几乎总是比 indent 更安全)。
因为字符串里可能含 :、{ 等特殊字符,所以镜像名、env 值、注解值要用引号包起来 ——image: "{{ ... }}"、| quote 都是为此;不加引号时 YAML 会把冒号解析成映射导致结构损坏。
因为多环境只是 values 不同,所以禁止在模板里写死 if eq .Release.Name "prod" 这类环境魔数 ——Release 名是命名问题不是配置项;环境差异一律显式放进 values,模板保持环境无关。
因为 helm template 不碰集群,所以 CI 与本地评审应先 template/lint ——在「装进集群」之前就能发现绝大多数结构错误;它发现不了的只有运行时问题(镜像不存在、探针路径错误)。
因为 helpers 统一命名与标签,所以改资源名只改 define 一处 ——fullname/labels/selectorLabels 三件套是骨架里的标准件。
因为 --set 表达复杂结构很别扭(逗号分隔、索引语法),且优先级最高,所以只用于临时试验 ——长期配置以 values 文件为准,GitOps 场景更是只信文件。
你可能会懵的地方
{{ }} 和 {{- -}} 差在哪?
功能相同;加 - 会剥掉紧邻的空白与换行。YAML 对缩进和空行敏感,不用 - 经常渲染出「幽灵空行」导致结构错位,所以实操里几乎总是写 {{- ... -}}。
if / with / range 怎么选?
if 判断真假(空字符串、0、nil、空 map 都为假);with 把 . 切换到指定对象(块内直接 .cpu 而不是 .Values.resources.cpu),对象为空则整块跳过;range 遍历列表或 map。资源可选块的标准写法就是 {{- with .Values.resources }} ... {{- end }}。
为什么到处是 nindent?
include 返回的是不带缩进的字符串,直接放进 YAML 会破坏层级。nindent 4 = 先输出一个换行再缩进 4 格,让标签块精确落位到 labels: 下面两级。
toYaml 是干嘛的?
把 values 里的对象(如 resources: {limits: {...}})原样序列化成 YAML 文本。用户在 values 里怎么写结构,模板就怎么输出——比逐字段手拼灵活得多,是透传任意结构(resources、nodeSelector、tolerations)的标准手法。
模板里能写多复杂的逻辑?
引擎能力很强,但工程共识是 模板越薄越好 :分支与默认值尽量设计进 values 结构里;模板过厚=难 lint、难评审、难在 GitOps 里 diff。看到三层嵌套 if 的 Chart 先怀疑设计。
错法对照 错法 问题 正解 image: {{ .Values.image.repository }}:{{ .Values.image.tag }} 不加引号tag 含特殊字符或以数字开头时 YAML 解析歧义 整串加引号 image: "{{ ... }}" Service selector 手写、Pod 标签用 helper 两处易漂移,选不中 Pod 两边引用同一 selectorLabels 用 .Release.Name 判断环境写死分支 难测试、难复用 values 显式参数 条件分支只包住值不包住 key 输出半截 key 导致非法 YAML 整个键值块放进 if/with 只 --set 调通、从不固化 values 文件 同事与 CI 无法复现 结果写回 -f 文件 helper 里漏传上下文 . helper 内取不到 .Release include "x" . 始终传点
卡点急救
现象:error converting YAML to JSON 或渲染产物缩进错乱 ——九成是空白问题:检查该行是否该用 {{-、helper 输出是否用了 nindent、块缩进格数是否与 YAML 层级一致;用 helm template --debug 看 Meta 行报的文件与行号。
现象:env 值渲染成 map[...] 或直接报错 ——你把对象当字符串用了;用 toYaml+nindent 整块透传,或 | quote 处理标量,并检查 values 里该键的真实类型。
现象:同一个 Chart dev 渲染正常、prod 报错 ——差异几乎必在 prod values 的结构(多了 resources、缩进错了);helm template myrel . -f values-prod.yaml --debug 单独渲染 prod 定位,别在模板里瞎找。
面试常问
Helm 模板的内置对象有哪些?平时最常用哪几个?
.Values(合并后的用户配置)、.Release(安装实例名/命名空间/修订号)、.Chart(包元数据)、.Capabilities(集群 API 版本探测)、.Files(包内文件访问)、.Template(当前模板信息)。日常 90% 是前三者:镜像与副本读 .Values,命名与标签用 .Release+.Chart。
values 的合并优先级顺序?
从低到高:Chart 默认 values.yaml → 父 Chart(子 Chart 场景)→ 按顺序传入的每个 -f 文件(后者覆盖前者)→ --set/--set-string/--values 等命令行参数。--set 优先级最高但表达复杂结构困难,生产以 values 文件为准。
如何在不连接集群的情况下验证 Chart?
helm lint(语法与最佳实践)+ helm template --debug(渲染预览,可叠加各环境 -f 对比输出差异)+ helm install --dry-run --debug(连集群校验但不落资源,含 admission 校验)。CI 流水线中至少跑前两个。
自测三问
.Values、.Release、.Chart 各提供什么?升级镜像 tag 应改哪一层的哪个值?
为什么 CI 里必须跑 helm template/helm lint?它们发现不了哪类问题?
写出「仅当 .Values.resources 非空才渲染 resources 块」的模板写法,并解释为什么要用 with 而不是 if。
小结一句话 values 驱动 Go template 生成合法 YAML:空白与引号决定合法性,helpers 决定一致性,helm template 多环境对比是渲染正确性的第一道闸——多环境一套 Chart 从这里开始成立。
延伸