发布时间:2022年3月24日
当要制作一个新的 chart 时,我们可以使用 helm create {chart-name}
命令来初始化一个标准的 chart 模板,其目录结构如下:
my-chart
├── charts
├── Chart.yaml
├── templates
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── NOTES.txt
│ ├── serviceaccount.yaml
│ ├── service.yaml
│ └── tests
│ └── test-connection.yaml
└── values.yaml
templates/
目录为应用所需资源清单的模板文件,将用于第三阶段与确定的 values
结合确定最终的 YAML
values.yaml
默认的 values
值,在第二阶段与通过 -f / --set / --set-string / --set-file
等选项传入的信息结合组成最终的 values
Chart.yaml
文件包含了该 chart 的描述,包含了 chart 的 name
(chart名称)、apiVersion
(api版本)、appVersion
(应用版本)、version
(chart版本)、description
(chart描述)。当我们 chart 目录下使用 helm package .
命令打包 chart 时,生成的 chart 包将被命名为 {name}-{version}.tgz
charts/
包含其他的chart(称之为 子chart),一般不做使用
Helm 提供模板语言实际上是由 Go 的模板语言,当我们通过 helm version 查看安装的版本时,会同时打印出目前版本中内置的 Go 语言版本,如下图:
在使用这种模板语言时,有两点最基础的内容需要我们注意:
{{ }}
内YAML
格式文件,需要严格控制缩进下面我们对 helm 的模板语言的几个特点进行逐个讲解:
template 中变量必须用 {{ }}
进行包括,第一部分中列举过 helm 中经常使用到的内置变量名,我们可以在模板中直接引用,参照下面的例子:
# values.yaml
imageName: nginx
imageTag: latest
replicas: 2
targetPort: 80
label: nginx
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
labels:
app: {{ .Release.Name }}-nginx-deploy
spec:
replcas: {{ .Values.replicas }}
selector:
matchLabels:
app: {{ .Values.label }}
template:
metadata:
app: {{ .Values.label }}
spec:
containers:
- name: {{ .Release.Name }}-nginx-container
image: {{ .Values.imageName }}:{{ .Values.imageTag }}
ports:
- name: HTTP
protocol: TCP
containerPort: {{ .Values.targetPort }}
通过 helm template
命令测试结果如下
与 unix / linux 一样,template 中通过支持管道概念,可以将前一个语句的结果传递给后一个管道进行处理,通过多个管道的调用,可以实现一系列我们想要的转换。
helm template 中的管道函数来源有两个:
全部的方法可以在 helm 官方文档中找到,地址为:Helm | 模板函数列表
常用到的一些管道参照下表,如需其他方法请自行查找文档:
管道 | 示例 | 作用 |
---|---|---|
quote | {{ .Release.Name | quote }} |
给变量加上双引号 |
nindent | {{ .Values.replicas | nindent 4 }} |
配置该行的缩进,以每行最左侧为基准,与父配置缩进数量无关 |
toYaml | {{ .Values.nginx-spec | toYaml | nindent 4 }} |
将变量值转换为 YAML 格式,通常与 nindent 一起使用 |
trimSuffix | {{ .Value.name | trimSuffix "*" }} |
从变量末尾删除指定字符 |
repeat | {{ .Value.name | repeat 3 - }} |
重复字符三次 |
contains | {{ .Values.name | contains "test" }} |
判断第二个字符串是否包含第一个字符串 |
default | {{ .Values.name | default "nginx" }} |
配置缺省值,当变量不存在时使用默认值 |
replace | {{ .Values.name | replace "nginx" "ng" }} |
字符替换,将 nginx 替换为 ng |
camelcase | {{ .Values.name | camelcase }} |
将变量转换为驼峰法则 |
imageName: nginx
imageTag: latest
replicas: 2
label: nginx
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name | camelcase | quote }}
labels:
app: "{{ .Release.Name | replace "-nginx" ""}}-nginx-deploy"
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: {{ .Values.label | repeat 2 }}
template:
metadata:
app: {{ .Values.label | repeat 2 }}
spec:
containers:
- name: "{{ .Release.Name | replace "-nginx" "" }}-nginx-container"
image: {{ .Values.imageName }}:{{ .Values.imageTag }}
ports:
- name: HTTP
protocol: TCP
containerPort: {{ .Values.targetPort | default 80 }}
运行 helm template
后结果如下图
template 中的管道本质上就是一个函数,只是在实际应用中我们习惯将不需要与第三方数据进行交互的函数在管道中使用,比如:{{ .Values.name | quote }}
也可以用 {{ quote .Values.name }}
来实现,但我们更偏向于用第一种方式即管道来处理。
但涉及到与其他数据对比的场景,如字符串包含、大小对比等场景,我们一般使用函数来处理。在使用函数进行数据处理时,我们常常与分支流程 if / else if / else
结合使用。
下面就列举一些最基础的方法,更多的内容可以根据上面提供的地址具体查找使用方法:
函数 | 示例 | 作用 |
---|---|---|
and | {{ and condition-1 condition-2 }} |
判断 condition1 与 condition2 是否都为真 |
or | {{ or condition-1 condition-2 }} |
判断 condition1 与 condition2 是否最少有一个为真 |
not | {{ not condition }} |
返回条件 condition 的相反值 |
eq | {{ eq value1 value2 }} |
判断 value1 与 value2 相等 |
ne | {{ ne value1 value2 }} |
判断 value1 与 value2 不相等 |
lt | {{ ne value1 value2 }} |
判断 value1 小于 value2 |
le | {{ le value1 value2 }} |
判断 value1 小于等于 value2 |
gt | {{ gt value1 value2 }} |
判断 value1 大于 value2 |
ge | {{ ge value1 value2 }} |
判断 value1 大于等于 value2 |
printf | {{ printf "$s/$s:$s" value1 value2 value3 }} |
将变量按照指定形式输出 |
dict | {{ dict "key-one" "value-one" "key-two" "value-two" }} |
创建一个新的字典,value 的值支持引用变量 key-one: value-one key-two: value-two-key1: value-two-value1 value-two-key2: value-two-value2 |
在 template 中,分支判断很简单,只需要在 {{ }}
中结合 if / else if / else
关键字,并在结束时使用 {{ end }}
进行闭合即可
如果值为如下情况,则会被判断为 false。
nil
(空或 null)map
,slice
,tuple
,dict
,array
)apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name | camelcase | quote }}
labels:
app: "{{ .Release.Name | replace "-nginx" ""}}-nginx-deploy"
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: {{ if contains "branch" .Release.Name }}"branch-show-label"{{ else }}"normal-label"{{ end }}
template:
metadata:
app: {{ if contains "branch" .Release.Name }}"branch-show-label"{{ else }}"normal-label"{{ end }}
spec:
containers:
- name: "{{ .Release.Name | replace "-nginx" "" }}-nginx-container"
image: {{ .Values.imageName }}:{{ .Values.imageTag }}
ports:
- name: HTTP
protocol: TCP
containerPort: {{ .Values.targetPort | default 80 }}
执行结果为
在使用条件判断时,如果我们的判断处于新起一行时,一定要严格控制空格数量,可以看下面的例子
spec:
selector:
matchLabels:
{{ if contains "branch" .Release.Name }}
app: "branch-show-label"
{{ else }}
app: "normal-label"
{{ end }}
template:
metadata:
{{ if contains "branch" .Release.Name }}
app: "branch-show-label"
{{ else }}
app: "normal-label"
{{ end }}
输出结果为:
spec:
replicas: 2
selector:
matchLabels:
app: "branch-show-label"
template:
metadata:
app: "branch-show-label"
spec:
从上面我们可以看到输出的结果中,不但缩进数量不正确而且上下还各多了空行
在第二部分YAML语法中我们提到了这个场景,当遇到这个问题可以使用 -
语法来剪掉多余的空行。
在上述语法中,我们可以在 {{` 后跟上一个 `-` 即 `{{-` 来帮助我们剪去左侧的空格,也可以使用 `-}}
标识删除右侧的空格
- 在 template 中,换行符也被认为是空格
- 请注意 - 与其他内容之间需要存在一个空格,否则将被认为是一个减法操作
- 一般只需要删除左侧的换行,即该行与上一行的多余换行,而右侧的换行空格不需要删除,一旦删除后会该行与下行内容之间就不会存在换行
按照上面的信息,我们可以将 YAML 修改为下面内容,并再次创建模板查看结果
spec:
selector:
matchLabels:
{{- if contains "branch" .Release.Name }}
app: "branch-show-label"
{{- else }}
app: "normal-label"
{{- end }}
template:
metadata:
{{- if contains "branch" .Release.Name }}
app: "branch-show-label"
{{- else }}
app: "normal-label"
{{- end }}
metadata:
name: "BranchChart"
labels:
app: "branch-chart-nginx-deploy"
spec:
replicas: 2
selector:
matchLabels:
app: "branch-show-label"
template:
metadata:
app: "branch-show-label"
spec:
执行结果截图
在前面的例子中,我们使用的 values.yaml
中,values 配置都只有一级,在实际应用中我们可能会嵌套很多层的 values
配置,如果每次使用一个内层变量都要从最外层一直引用到内层,会增加错误引用的风险,因此 template 提供了 with
关键字,用于修改变量变量的引用范围。
在实际应用中,我们习惯于在 values.yaml
文件中,按照资源清单中的配置结构设计我们的 values,如下面实例一样,我们将 front 所需要的 values 按照 deployment 类型资源清单字段进行设置,会产生多层级的配置。
front:
name: front-nginx-deploy
label: front-nginx
image:
name: nginx
tag: latest
containerPort: 80
replicas: 2
如果不使用 with
关键字时,我们通常这样进行配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.front.name | quote }}
labels:
app: {{ .Values.front.name | quote }}
spec:
replicas: {{ .Values.front.replicas }}
selector:
matchLabels:
app: {{ .Values.front.label }}
template:
metadata:
app: {{ .Values.front.label }}
spec:
containers:
- name: {{ .Values.front.image.name }}
image: {{ .Values.front.image.name }}:{{ .Values.front.image.tag}}
ports:
- name: HTTP
protocol: TCP
containerPort: {{ .Values.front.image.containerPort | default 80 }}
可以从上面看到,如果我们一直使用在默认根作用域内读取变量,可能整体 YAML 文件中的变量名称变成的非常长,从而增加错误引用的风险。
在这种情况下,我们通常使用 with
关键字来切换当前的变量域,具体可以看下面的例子
注意:作用域一旦修改,在修改后的作用域内无法再返回其上层作用域
apiVersion: apps/v1
kind: Deployment
{{- with .Values.front }}
metadata:
name: {{ .name | quote }}
labels:
app: {{ .name | quote }}
spec:
replicas: {{ .replicas }}
selector:
matchLabels:
app: {{ .label }}
template:
metadata:
app: {{ .label }}
spec:
containers:
{{- with .image }}
- name: {{ .name }}
image: {{ .name }}:{{ .tag}}
ports:
- name: HTTP
protocol: TCP
containerPort: {{ .Values.front.image.containerPort | default 80 }}
{{- end}}
{{- end}}
在这个例子中,我们共使用了两次 with
关键字来修改变量的作用域:
第一次,我们将作用域从根作用域切换到了 .Values.front
,此时 .
这个变量就代表着 .Values.front
,因此在后续我们取用 front.name / front.label
等变量时,直接使用 .name / .label
即可。
第二次,我们在 .Values.front
作用域内,通过 with .image
,将作用域切换到了 .Values.front.image
下,此时 .
代表着 .Values.front.image
,因此后续要取镜像名称及镜像版本直接使用 .name / .tag
即可。
在两个作用域内,我们都使用了 name / tag
变量,但是因为他们所处的作用域不同他们的值也不会互相产生影响。
在 template 中,默认的作用域为根作用域
$
,我们之前使用的.Values / .Release
等变量,其实都是根作用于下的变量,全拼为$.Values / $.Release
。
因此,我们可以认为,在模板的起始位置,helm 帮我们做了{{- with $ }}
的处理。
除了在前面,我们提到了一旦修改了作用域,在修改后的作用域范围内将无法访问其上层作用域的变量外,还有一点值得注意,在任何作用域内,我们都可以通过$
来访问根作用域的变量。
因此,如果需要在修改了作用域的情况下需要使用父级及以上层级的数据,可以通过访问根作用域来实现。
通过执行 helm template
后,输出结果为:
apiVersion: apps/v1
kind: Deployment
metadata:
name: "front-nginx-deploy"
labels:
app: "front-nginx-deploy"
spec:
replicas: 2
selector:
matchLabels:
app: front-nginx
template:
metadata:
app: front-nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- name: HTTP
protocol: TCP
containerPort: 80
执行结果
在部分场景下,我们可能有循环 values 中某个配置的需求,如多个 Tag、多个 Image,在模板中我们通过 range
关键字实现循环需求,具体参照下面的例子
front:
name: front-nginx-deploy
labels:
app: front
type: front-nginx
containers:
- name: nginx
tag: latest
containerPort: 80
- name: tomcat
tag: latest
containerPort: 8080
replicas: 2
在上面的 values 中我们配置了两个集合类型的数据,一个是 Map 类型 labels
,一个是 Sequence 类型的 containers
在 Deployment 的声明中,我们可以这样进行循环使用
apiVersion: apps/v1
kind: Deployment
{{- with .Values.front }}
metadata:
name: {{ .name | quote }}
labels:
app: {{ .name | quote }}
spec:
replicas: {{ .replicas }}
selector:
matchLabels:
{{- range .labels }}
{{ . }}
{{- end}}
template:
metadata:
{{- range .labels }}
{{ toYaml . }}
{{- end}}
spec:
containers:
{{- range .containers }}
- name: {{ .name }}
image: {{ .name }}:{{ .tag}}
ports:
- name: HTTP
protocol: TCP
containerPort: {{ .containerPort | default 80 }}
{{- end}}
{{- end}}
在上面的 Deployment.yaml
中,我们分别循环了 labels
以及 containers
两个集合类型数据
在循环 labels
变量时,因为 range
在循环一个集合类型数组时,.
变量指向的是每一个 key
对应的 value
值,所以 metadata
中只会输出 front
与 front-nginx
在循环 containers
时,因为每一个 container
存在下级配置,因此我们可以通过 .name / .tag / .container
其对应的值。
执行结果
循环是一个很实用的功能,比如在配置 ConfigMap
类型的资源清单时,我们可以使用循环很轻松的创建一个数组出来,如下
apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
data:
mappings: |-
{{- range .Values.cm.mappings }}
- {{ . }}
{{- end }}
在第二部分 YAML 语法中,我们提到过
|-
,该符号常用于标记表示一个多行字符串。
在模板的配置中,在小部分场景下,我们可能有自定义变量的需求,比如有一个经常会用到的变量,需要将其取值路径进行简化,我们就可以通过将其定义为一个自定义变量的方式来方便我们的使用。
在实际使用中,我们通过 $name := value-path
的方式来实现一个自定义变量。
下面还是以之前的配置做一个介绍
front:
name: front-nginx-deploy
label: front-nginx
image:
name: nginx
tag: latest
containerPort: 80
replicas: 2
在下面的 Deployment 的声明中,我们可以将常用的 front.name / front.label
等拿出来定义为变量来简化使用。
自定义变量也存在作用域的区别,在子域中定义的变量无法在子域的上层进行调用。
在不使用with
的情况下,定义的变量均定义在根域下。
apiVersion: apps/v1
kind: Deployment
metadata:
{{- $frontname := .Values.front.name | quote -}}
name: {{ $frontname }}
labels:
app: {{ $frontname }}
spec:
replicas: {{ .Values.front.replicas }}
{{- $containerLabel := .Values.front.label | quote -}}
selector:
matchLabels:
app: {{ $containerLabel }}
template:
metadata:
app: {{ $containerLabel }}
执行结果
除了用于高频率数据复用外,自定义变量还有一个特性是在 当前域上层定义的变量在当前域中是可以访问的,在 with
模块我们说过,某一个域的子域无法访问其上层域的变量值,对此我们提出了可以使用全局的根域 $
来拿到上层域的数据。
现在除了从根域取外,我们可以在上级域中将下级域中要用的数据设置为变量,这样也可以实现对上层域数据的访问。
在循环场景下我们可以使用 {{- range $key, $val := .Values.key }}
来在循环中拿到循环数据的索引和值,索引数据分为以下两种
apple
格式,循环中拿到的 $key
为 color、taste
mappings
格式,循环中拿到的 $key
为 0、1、2
cm:
apple:
color: red
taste: sweet
mappings:
- a
- b
- c
在 ConfigMap 的声明中,我们可以这样进行循环使用
apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
data:
apple:
{{- range $key, $val := .Values.cm.apple }}
{{ $key }}: {{ $val }}
{{- end}}
mappings:
{{- range $key, $val := .Values.cm.mappings }}
{{ $key }}: {{ $val }}
{{- end}}
执行结果