前面我们介绍了 Helm Chart 的一些基本概念和使用,接下来我们重点介绍下 Chart 模板的编写。模板会渲染成 Kubernetes 的资源清单文件,下面我们将来学习下模板的结构,如何使用它们,如何编写 Go 模板以及如何调试。
对象从模板引擎传递到模板中,在代码中可以传递对象。也有一种方法可以在模板宏创建新的对象,比如 tuple
函数。对象可以很简单,也可以包含其他对象或函数,例如,Release 对象就包含几个对象(比如 Release.Name),Files 对象就包含几个函数。
前面提到过我们可以在模板中使用 {{ .Release.Name }}
获取 release 的名称,Release 是我们可以在模板中访问的几个顶级对象之一:
Release
:该对象描述了 release 本身的相关信息,它内部有几个对象:
Release.Name
:release 名称Release.Namespace
:release 安装到的命名空间Release.IsUpgrade
:如果当前操作是升级或回滚,则该值为 trueRelease.IsInstall
:如果当前操作是安装,则将其设置为 trueRelease.Revision
:release 的 revision 版本号,在安装的时候,值为1,每次升级或回滚都会增加Reelase.Service
:渲染当前模板的服务,在 Helm 上,实际上该值始终为 HelmValues
:从 values.yaml
文件和用户提供的 values 文件传递到模板的 Values 值,默认情况下,Values 是空的。Chart
:获取 Chart.yaml
文件的内容,该文件中的任何数据都可以访问,例如 {{ .Chart.Name }}-{{ .Chart.Version}}
可以渲染成 mychart-0.1.0
,该对象下面可用的字段前面我们已经提到过了。Files
:可以访问 chart 中的所有非特殊文件,虽然无法使用它来访问模板文件,但是可以来访问 chart 中的其他文件。
Files.Get
:用于根据名称获取文件(比如 .Files.Get config.ini
)Files.GetBytes
:用于以 bytes 数组而不是字符串的形式来获取文件内容的函数,这对于类似于图片之类的东西很有用Files.Glob
:用于返回名称于给定的 shell glob 模式匹配的文件列表Files.Lines
:可以逐行读取文件的函数,对于遍历文件中的每行内容很有用Files.AsSecrets
:将文件内容以 Base64 编码的字符串返回的函数Files.AsConfig
:将文件正文作为 YAML 字典返回的函数Capabilities
:提供了获取有关 Kubernetes 集群支持功能的信息的对象
Capabilities.APIVersions
:支持的版本集合Capabilities.APIVersions.Has $version
:判断一个版本(比如 batch/v1
)或资源(比如 apps/v1/Deployment
)是否可用Capabilities.Kube.Version
:Kubernetes 的版本Capabilities.Kube
:是 Kubernetes 版本的缩写Capabilities.Kube.Major
:Kubernetes 主版本Capabilities.Kube.Minor
:Kubernetes 的次版本Template
:包含当前正在执行的模板的相关信息
Name
:当前模板的命名空间文件路径(比如 mychart/templates/mytemplate.yaml
)BaePath
:当前 chart 的模板目录的命名空间路径(比如 mychart/templates
)需要注意的是内置的对象始终是以大写字母开头的,这也是符合 Go 的命名约定的。创建自己的名称的时候,可以自由使用适合你团队的约定,一些团队,比如 Kubernetes Charts 团队,选择仅使用首字母小写,以区分本地名称和内置名称,这里我们也会遵循该约定。
前面我们介绍了 Helm 模板提供的内置对象,其中就有一个内置对象 Values
,该对象提供对传递到 chart 中的 values 值的访问,其内容主要有4个来源:
values.yaml
文件values.yaml
文件-f
参数传递给 helm install
或 helm upgrade
的 values 值文件(例如 helm install -f myvals.yaml ./mychart
)--set
传递的各个参数(例如 helm install --set foo=bar ./mychart
)values.yaml
文件是默认值,可以被父 chart 的 values.yaml
文件覆盖,而后者又可以由用户提供的 values 值文件覆盖,而该文件又可以被 --set
参数覆盖。
演示:chart模板编写-2022.4.5(测试成功) |
1️⃣ 创建charts包
➜ ~ helm create mychart
Creating mychart
➜ ~ tree mychart
mychart
├── charts
├── Chart.yaml
├── templates
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── NOTES.txt
│ ├── serviceaccount.yaml
│ ├── service.yaml
│ └── tests
│ └── test-connection.yaml
└── values.yaml
3 directories, 10 files
我们清空values.yaml
文件内容,删除templates目录
下内容:
2️⃣ 创建模板文件
常规yaml文件如下:
在mychart/templates目录
下创建ConfigMap.yaml文件
:
apiVersion: v1
kind: ConfigMap
metadata:
name: myconfigmap
data:
test: helm-chart
然后直接使用helm template mychart1 ./mychart
命令直接渲染,我们看下结果:
3️⃣ 编辑模板文件
mychart/values.yaml
文件然后编辑 ConfigMap
模板。删除 values.yaml
中的默认设置后,我们将只设置一个参数:favoriteDrink: coffee
ConfigMap.yaml文件
:apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favoriteDrink }}
可以看到在最后一行我们将 favoriteDrink
作为 Values
的属性进行访问:{{ .Values.favoriteDrink }}
。
#➜ helm install --generate-name --dry-run --debug ./mychart
➜ helm install --generate-name --dry-run ./mychart #注意这里的--dry-run参数:simulate an install
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hg/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hg/.kube/config
NAME: mychart-1649168183
LAST DEPLOYED: Tue Apr 5 22:16:24 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649168183-configmap
data:
myvalue: "Hello World"
drink: coffee
⚠️ 注意:这里如果使用template命令
的话,同时使用--generate-name
选项,会没有效果,建议直接使用--dry-run
选项;
因为--dry-run
是模拟安装,而template命令
只渲染,可能会有点问题!
4️⃣ 使用--set
参数来覆盖默认vaulues值
values.yaml
文件中将 favoriteDrink 设置为了 coffee,所以这就是模板中显示的值,我们可以通过在调用 helm install
的过程中添加 --set
参数来覆盖它:#查看下values.yaml文件内容
favoriteDrink: coffee
#使用`--set` 参数来覆盖默认vaulues值
➜ helm install --generate-name --set favoriteDrink=xyy --dry-run ./mychart
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hg/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hg/.kube/config
NAME: mychart-1649205876
LAST DEPLOYED: Wed Apr 6 08:44:37 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649205876-configmap
data:
myvalue: "Hello World"
drink: xyy
因为 --set
的优先级高于默认的 values.yaml
文件,所以我们的模板会生成 drink: slurm
。
5️⃣ Values 值文件也可以包含更多结构化的内容
values.yaml
文件中创建一个 favorite 的部分,然后在其中添加几个 keys:favorite:
drink: coffee
food: pizza
ConfigMap.yanl
:apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink }}
food: {{ .Values.favorite.food }}
➜ local-vscode helm install --generate-name --set favoriteDrink=xyy --dry-run ./mychart
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hg/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hg/.kube/config
NAME: mychart-1649206046
LAST DEPLOYED: Wed Apr 6 08:47:27 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649206046-configmap
data:
myvalue: "Hello World"
drink: coffee
food: pizza
虽然我们可以通过这种方式来构造数据,但是还是建议你将 values 树保持更浅,这样在使用的时候更加简单。当我们考虑为子 chart 分配 values 值的时候,我们就可以看到如何使用树形结构来命名 values 值了。
测试结束。
如果你需要从默认值中删除 key,则可以将该 key 的值覆盖为 null,在这种情况下,Helm 将从覆盖的 values 中删除该 key。例如,在 Drupal chart 中配置一个 liveness 探针:
livenessProbe:
httpGet:
path: /user/login
port: http
initialDelaySeconds: 120
如果你想使用 --set livenessProbe.exec.command=[cat, docroot/CHANGELOG.txt]
将 livenessProbe 的处理程序覆盖为 exec
而不是 httpGet
,则 Helm 会将默认键和覆盖键合并在一起,如下所示:
livenessProbe:
httpGet:
path: /user/login
port: http
exec:
command:
- cat
- docroot/CHANGELOG.txt
initialDelaySeconds: 120
但是,这样却有一个问题,因为你不能声明多个 livenessProbe 处理程序,为了解决这个问题,你可以让 Helm 通过将 livenessProbe.httpGet
设置为 null 来删除它:
➜ helm install stable/drupal --set image=my-registry/drupal:0.1.0 --set livenessProbe.exec.command=[cat, docroot/CHANGELOG.txt] --set livenessProbe.httpGet=null
到这里我们已经了解到了几个内置对象,并利用它们将信息注入到了模板中,现在我们来看看模板引擎的另外方面:函数和管道。
现在我们已经了解了如何将信息加入到模板中,但是这些信息都是直接原样的放置过去的,有时候,我们希望以一种对我们更有用的方式来转换提供的数据。
下面让我们从一个最佳实践开始:将 .Values
对象中的字符串注入模板时,我们应该引用这些字符串,我们可以通过在 template 指令中调用 quote
函数来实现,比如:
演示:测试quote函数-2022.4.5(测试成功) |
编辑ConfigMap.yaml
:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ quote .Values.favorite.drink }}
food: {{ .Values.favorite.food }}
模板函数遵循的语法规则是 functionName arg1 arg2...
,在上面的代码片段中,quote .Values.favorite.drink
会调用 quote
函数并传递一个单个参数。
测试
➜ local-vscode helm install --generate-name --dry-run ./mychart
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hg/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hg/.kube/config
NAME: mychart-1649221072
LAST DEPLOYED: Wed Apr 6 12:57:53 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649221072-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: pizza
可以看到
quote
函数就是给后面加上一对"
符号。
测试结束。
Helm模板语言
Helm 有60多种可用的函数,其中一些是由 Go 模板语言本身定义的,其他大多数都是 Sprig 模板库提供的,接下来我们会通过部分示例来逐步介绍其中的一些功能函数。
⚠️ Helm 模板
当我们谈论
Helm 模板语言
的时候,就好像是特定于 Helm 一样,但实际上它是 Go 模板语言加上一些额外的函数以及各种封装程序的组合,以将某些对象暴露给模板。当我们需要学习模板的时候,Go 模板上有许多资源会对我们有所帮助的。
模板语言有一个强大的功能就是**管道(Pipeline)**概念,管道利用 UNIX 的概念,将一系列模板命令链接在一起,一起对外提供服务,换句话说,管道是按顺序完成多项工作的有效方式。
演示:测试管道-2022.4.5(测试成功) |
1️⃣ 我们来使用管道重写上面的示例模板:编辑ConfigMap.yaml
文件
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | quote }}
food: {{ .Values.favorite.food | quote }}
2️⃣ 在这里我们没有调用 quote ARGUMENT
函数,而是颠倒了下顺序,我们使用管道符(|)将参数发送给函数:.Values.favorite.drink | quote
,使用管道,我们可以将多个功能链接在一起:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | quote }}
food: {{ .Values.favorite.food | upper | quote }}
管道顺序
反转顺序是模板中常见的做法,我们会看到
.val | quote
比quote .val
用法更多,虽然两种方法都是可以的。
3️⃣ 最后,模板渲染后,会产生如下所示的结果:
➜ local-vscode helm install --generate-name --dry-run ./mychart
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hg/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hg/.kube/config
NAME: mychart-1649225993
LAST DEPLOYED: Wed Apr 6 14:19:54 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649225993-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
我们可以看到 values 中的 pizza
值已经被转换成了 "PIZZA"
。当这样传递参数的时候,第一个求值结果(.Values.favorite.drink
)会作为一个参数发送给函数。
4️⃣ 我们可以修改上面的 drink
示例,用一个带有两个参数的函数进行说明:repeat COUNT STRING
。
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | repeat 5 | quote }}
food: {{ .Values.favorite.food | upper | quote }}
repeat
函数将重复字符串给定的次数,渲染后我们可以得到如下的输出结果:
➜ local-vscode helm install --generate-name --dry-run ./mychart
……
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649226152-configmap
data:
myvalue: "Hello World"
drink: "coffeecoffeecoffeecoffeecoffee"
food: "PIZZA"
⚠️ 这里需要注意下:这个repeat 5
要放在前面吗,不会会报错,放在后面的话,因为是不满足yaml格式的。
测试结束。
在模板中经常会使用到的一个函数是 default
函数:default DEFAULT_VALUE GIVEN_VALUE
,该函数允许你在模板内部指定默认值。
演示:测试default函数-2022.4.5(测试成功) |
1️⃣ 我们来修改上面示例中的模板:
#这里添加deafult "rice"
food: {{ .Values.favorite.food | default "rice" | upper | quote }}
#完整ConfigMap.yaml内容如下
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | repeat 5 | quote }}
food: {{ .Values.favorite.food | default "rice" | upper | quote}}
2️⃣ 正常运行,我们还是可以得到 values.yaml
文件中定义的 pizza:
➜ local-vscode helm install --generate-name --dry-run ./mychart
……
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649226682-configmap
data:
myvalue: "Hello World"
drink: "coffeecoffeecoffeecoffeecoffee"
food: "PIZZA"
3️⃣ 现在我们从 values.yaml
文件中移除 food 的定义:
favorite:
drink: coffee
# food: pizza
4️⃣ 现在我们重新运行 helm install --generate-name --dry-run --debug ./mychart
将渲染成如下的 YAML 文件:
➜ local-vscode helm install --generate-name --dry-run ./mychart
……
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649226830-configmap
data:
myvalue: "Hello World"
drink: "coffeecoffeecoffeecoffeecoffee"
food: "RICE"
5️⃣ ⚠️ 注意
在一个真实的 chart 模板中,所有的静态默认值都应位于 values.yaml
文件中,并且不应该重复使用 default
函数,但是,默认命令非常适合计算不能在 values.yaml
文件中声明的 values 值,例如:
food: {{ .Values.favorite.food | default (printf "%s-rice" (include "fullname" .)) }}
不过在有些地方,if
条件语句可能比 default 函数更合适,我们会在后面了解到。
模板函数和管道是将数据转换后然后将其插入到 YAML 文件中的一种强大方法,但是有的时候有必要添加一些模板逻辑,这些逻辑比仅仅插入字符串要复杂得多,下面我们将来了解模板语言中提供的控制流程。
测试结束。
另外需要注意的是在模板中,运算符(eq、ne、lt、gt、and、or 等等)均实现为函数;在管道中,运算符可以用括号()
进行分割。
接下来我们可以去了解控制流程条件语句、循环和作用域修饰符的使用。
控制流程为模板作者提供了控制模板生成流程的功能,Helm 的模板语言提供了以下一些流程控制:
if/else
条件语句with
指定一个作用域范围range
提供类似于 for each
这样的循环样式除此之外,还提供了一些声明和使用命名模板的操作:
define
在模板内部声明一个新的命名模板template
导入一个命名模板block
声明了一种特殊的可填充模板区域。这里我们先来了解 if
、with
、range
语句的使用,其他将在后面的命名模板
部分介绍。
首先我们先来了解下有条件地在模板中包含一个文本区域,就是 if/else
,这个条件判断的基本结构如下所示:
{{ if PIPELINE }} #PIPLINE可以认为是一个表达式!
# Do something
{{ else if OTHER PIPELINE }}
# Do something else
{{ else }}
# Default case
{{ end }}
#注意:if和else一定是成对存在的。
可以看到我们这里判断的是管道而不是一个 values 值,这是因为控制结构可以执行整个管道,而不仅仅是判断值。如果值为以下的一些内容,则将管道判断为 false:
在其他条件下,条件都为真。
示例:if/else测试-2022.4.6(测试成功) |
1️⃣ 现在我们在上面的示例模板 ConfigMap.yaml
中添加一个简单的条件,如果 drink 设置为 coffee,我们就添加另外一个设置:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}
{{ if eq .Values.favorite.drink "coffee" }}mug: true{{ end }}
我们把 values.yaml 文件内容设置成下面的样子:
favorite:
# drink: coffee
food: pizza
2️⃣ 由于我们注释掉了 drink: coffee
,所以渲染后输出不会包含 mug: true
的标志。
这里需要注意下:⚠️ (用于比较的不兼容类型)
但是如果我们把注释取消掉,则应该输出如下所示的内容:
#values.yaml
favorite:
drink: coffee
food: pizza
➜ helm install --generate-name --dry-run ./mychart
……
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649242370-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
mug: true
这是因为上面模板中我们添加了 if eq .Values.favorite.drink "coffee"
这样的条件判断,相当于是判断 .Values.favorite.drink
值是否等于 "coffee"
,如果相等则渲染 mug: true
。
3️⃣ 这里我们再很关注一下细节问题
#values.yaml
favorite:
drink: coffee
food: pizza
思考:这个模板里if流程控制语句
为什么不可以竖着写,而要写在一行上?不会不感觉可读性不好!
我们这里尝试这竖着写看下有什么效果:
竖着写,当模板被渲染之后,这里会多出2个空行!
那么,我们可以使用如下方式来解决这个问题:这里添加2个-
即可。
4️⃣ 接下来,我们再使用下if…… esle if ……else
,看下效果
配置文件如下:
#values.yaml
favorite:
drink: coffee
food: pizza
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}
{{ if eq .Values.favorite.drink "coffee" -}}
condiition: true
{{ else if eq .Values.favorite.drink "water" -}}
elsecondition: true
{{ else -}}
defaultcase: true
{{- end }}
我们渲染下,查看效果:
➜ local-vscode helm install --generate-name --dry-run --set favorite.drink=water ./mychart
……
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649243095-configmap
data:
myvalue: "Hello World"
drink: "water"
food: "PIZZA"
elsecondition: true
➜ local-vscode helm install --generate-name --dry-run --set favorite.drink=waterxxx ./mychart
……
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649243172-configmap
data:
myvalue: "Hello World"
drink: "waterxxx"
food: "PIZZA"
defaultcase: true
测试结束。
还有一个非常重要的功能点就是关于空格的控制,因为空格对于 YAML 文件非常重要的,不是说任意缩进就可以。
示例:空格控制测试-2022.4.7(测试成功) |
⚠️ 关于空格控制,其实上面那个示例有测试过了,这里我们再次做下实验。
1️⃣ 依然还是以前面的例子为例,我们来格式化下模板格式以更易于阅读:
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}
{{ if eq .Values.favorite.drink "coffee" }}
mug: true
{{ end }}
#values.yaml
favorite:
drink: coffee
food: pizza
现在我们的模板看上去更易于阅读了,但是我们通过模板引擎来渲染下,却会得到如下的错误信息:
➜ helm install --generate-name --dry-run ./mychart
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hg/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hg/.kube/config
Error: INSTALLATION FAILED: YAML parse error on mychart/templates/ConfigMap.yaml: error converting YAML to JSON: yaml: line 9: did not find expected key
这是因为我们在模板中添加了空格,生成了不正确的 YAML 文件:
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1575970308-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
mug: true
我们可以看到 mug: true
的缩进是有问题的,不符合 YAML 文件格式,现在我们将缩进去掉试看看:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}
{{ if eq .Values.favorite.drink "coffee" }}
mug: true
{{ end }}
重新渲染模板,然后可以发现已经可以正常通过了,但是渲染出来的 YAML 文件格式看上去还是有点奇怪:
# Source: mychart/templates/configmap.yaml
➜ helm install --generate-name --dry-run ./mychart
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hg/.kube/config
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649321939-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
mug: true
我们可以看到得到的 YAML 文件中多了一些空行,这是因为模板引擎渲染的时候它会删除 {{
和 }}
之间的内容,但是会完全保留其余的空格。我们知道在 YAML 文件中空格是有意义的,所以管理空格就变得非常重要了,不过 Helm 模板也提供了一些工具来帮助我们管理空格。
首先可以使用特殊字符修改模板声明的花括号语法,以告诉模板引擎去掉空格。{{-
添加了破折号和空格表示应将左边的空格移除,-}}
表示将右边的空格移除,另外也需要注意的是,换行符也是空格
。
空格
需要注意的时候要确保 -
和指令的其余部分之间要有空格,{{- 3 }}
表示删除左边的空格并打印3,但是 {{-3 }}
表示打印-3
。
2️⃣ 使用这个语法,我们可以修改上面的模板来移除多余的空行:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}
{{- if eq .Values.favorite.drink "coffee" }}
mug: true
{{- end }}
渲染后可以看到空行被移除掉了:
➜ helm install --generate-name --dry-run ./mychart
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1575972373-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
mug: true
3️⃣ 为了更加清楚地说明这个问题,我们用*
来代替将要删除的每个空格,行尾的*
表示被删除的换行符:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}*
**{{- if eq .Values.favorite.drink "coffee" }}
mug: true*
**{{- end }}
⚠️ 所以我们这里用 {{-
表示的就是删除本行开头的两个空格以及上一行的换行符,这样是不是就将空行都删除了啊。
4️⃣ 在使用移除空格的时候还需要小心,比如下面的操作:
food: {{ .Values.favorite.food | upper | quote }}
{{- if eq .Values.favorite.drink "coffee" -}}
mug: true
{{- end -}}
我们依然还是可以用 *
来代替空格进行分析,如下所示:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}*
**{{- if eq .Values.favorite.drink "coffee" -}}*
mug: true*
**{{- end -}}
第一个 {{-
会删除前面的空格和前面的换行符,然后后面的 -}}
会删除当前行的换行符,这样就会把 mug: true
移动到 food: "PIZZA"
后面去了,最终渲染过后就会变成:food: "PIZZA"mug: true
,因为在两侧都去掉换行符。
有关模板中空格控制的详细信息,可以查看 Go 模板官方文档介绍。
5️⃣ 缩进函数({{ indent 2 "mug: true" }}
)使用
⚠️ 不过有时候告诉模板系统如何缩进比起去控制模板指令的间距更加容易,所以,有时候你会发现缩进函数({{ indent 2 "mug: true" }}
)更有用。
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}
{{ indent 2 "indent: 2" }}
{{- if eq .Values.favorite.drink "coffee" }}
mug: true
{{- end }}
#values.yaml
favorite:
drink: coffee
food: pizza
我们渲染下,结果是报错的:
因为{{ indent 2 "indent: 2" }}
这个需要顶格写:
测试结束。
接下来需要了解的是 with
操作,它可以控制变量的作用域,然后重新用 .
调用就表示对当前作用域的引用。所以,.Values
是告诉模板引擎在当前作用域下内查找 Values 对象。
with
语句的语法和 if
语句比较类似:
{{ with PIPELINE }}
# 限制范围
{{ end }}
示例:使用with修改作用域-2022.4.7(测试成功) |
1️⃣ 范围可以更改,可以让你将当前范围 .
设置为特定的对象。例如,我们一直在使用 .Values.favorites
,让我们重写下模板文件 ConfigMap 来更改 .
的范围指向 .Values.favorites
:
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
{{- end }}
#values.yaml
favorite:
drink: coffee
food: pizza
我们这里将前面练习的 if
条件语句删除了,在模板中我们添加了一个 {{- with .Values.favorite }}
的语句,意思就是说在 with
语句的作用范围内可以用 .
表示 .Values.favorite
了,所以我们可以引用 .drink
和 .food
了,但是在 {{ end }}
之后就会重置为之前的作用域了。
渲染结果:
➜ helm install --generate-name --dry-run ./mychart
# Source: mychart/templates/ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649331891-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
2️⃣ 不过需要注意得是,在受限的作用域范围内,你无法从父级范围访问到其他对象,比如,下面得模板会失败:
{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
release: {{ .Release.Name }}
{{- end }}
因为 Release.Name
并不在 .
的限制范围内,所以会产生错误。但是,如果我们交换最后两行,则就可以正常工作了,因为 {{ end }}
之后会重置作用域。
{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
{{- end }}
release: {{ .Release.Name }}
测试结束。
下面我先来了解下 range
,然后我们再去了解下模板变量,它可以为上面得这个范围问题提供一种解决方案。
我们知道许多编程语言都支持使用 for
循环、foreach
循环或者类似功能机制进行循环迭代。在 Helm 得模板语言中,迭代集合得方法是使用 range
运算符。
示例:range循环操作-2022.4.7(测试成功) |
1️⃣ 比如首先我们在 values.yaml
文件中添加一份 pizza 馅料列表:
favorite:
drink: coffee
food: pizza
pizzaToppings:
- mushrooms
- cheese
- peppers
- onions
现在我们有了 pizzaToppings
列表(在模板中称为切片),我们可以来修改下模板将列表打印到 ConfigMap 中:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
{{- end }}
toppings: |-
{{- range .Values.pizzaToppings }}
- {{ . | title | quote }}
{{- end }}
我们仔细观察下模板中的 toppings:
列表,range
函数将遍历 Values.pizzaToppings
列表,我们看到里面使用了一个 .
,类似于上面我们用 with
设置范围一样,运算符也是这样的,每次循环,.
都会被设置为当前的 pizzaTopping
,也就是说第一次设置为mushrooms
,第二次迭代设置为cheese
,依次类推。
我们可以直接传递 .
这个值到管道上,所以我们这里 {{ . | title | quote }}
就相当于发送 .
给 title
(标题大小写函数)函数,然后发送给 quote
函数。
2️⃣ 我们渲染这个模板,会输出如下的内容:
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1575975849-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
toppings: |-
- "Mushrooms"
- "Cheese"
- "Peppers"
- "Onions"
在上面模板中,我们做了一些小小的特殊处理,toppings: |-
行表示声明一个多行字符串,所以其实我们的 toppings
列表不是一个 YAML 列表,而是一个比较大的字符串,这是因为 ConfigMap 中的数据由 key/value
对组成,所有 key 和 value 都是简单的字符串,要了解为什么是这样的,可以查看 Kubernetes ConfigMap 文档,不过这个细节对我们这里不重要。
⚠️ 需要注意下:这里如果不跟上|
的话,会报错的!(属于 yaml部分内容)
这里加不加-
号都是可以渲染成功的。
3️⃣ YAML部分内容
(1)多行字符串可以使用 |
保留换行符,也可以使用 >
折叠换行,如:
this: |
Foo
Bar
that: >
Foo
Bar
对应的意思就是:{this: 'Foo\nBar\n', that: 'Foo Bar\n'}
(2)+
表示保留文字块末尾的换行,-
表示删除字符串末尾的换行,如:
s1: |
Foo
s2: |+
Foo
s3: |-
Foo
对应的意思就是:{s1: 'Foo\n', s2: 'Foo\n\n\n', s3: 'Foo'}
4️⃣ 有时候,在模板中快速创建一个列表,然后遍历该列表很有用,Helm 模板具有简化该功能的函数:tuple
。元组是固定大小的列表集合,但是具有任意数据类型,下面是元组的大概使用方法:
sizes: |-
{{- range tuple "small" "medium" "large" }}
- {{ . }}
{{- end }}
上面的模板最终会被渲染成如下的 YAML:
sizes: |-
- small
- medium
- large
除了列表和元组之外,range
还可以用于遍历字典,我们在下一节介绍模板变量的时候再来了解这个用法吧。
有了函数、管道、对象以及控制结构,我们可以想象下大多数编程语言中更基本的思想之一:变量
。在模板中,变量的使用频率较低,但是,我们还是可以使用他们来简化代码,以及更好地使用 with
和 range
。
示例:变量测试-2022.4.7(测试成功) |
在前面的示例中,我们知道下面的模板渲染会出错:
{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
release: {{ .Release.Name }}
{{- end }}
因为 Release.Name
不在 with
语句块限制的范围之内,解决作用域问题的一种方法是将对象分配给在不考虑当前作用域情况下访问的变量。
1️⃣ 在 Helm 模板中,变量是对另外一个对象的命名引用。它遵循 $name
格式,变量使用特殊的赋值运算符进行赋值 :=
,我们可以修改上面的模板,为 Release.Name
声明一个变量:
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
{{- $relname := .Release.Name -}}
{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
release: {{ $relname }}
{{- end }}
#values.yaml
favorite:
drink: coffee
food: pizza
注意在 with
语句之前,我们先分配了 $relname := .Release.Name
,然后在 with
语句块中,$relname
变量仍然表示 release 的名称,我们渲染该模板,可以得到如下的正确结果:
➜ helm install --generate-name --dry-run ./mychart
# Source: mychart/templates/ConfigMap.yaml
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649378928-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
release: mychart-1649378928
⚠️ 注意:这个变量的位置可以放在最上面,是没问题的,但是不能放在with里面。
2️⃣ 变量在 range
循环里面非常有用,它们可以用于类似于列表的对象来捕获索引和 value 值:
toppings: |-
{{- range $index, $topping := .Values.pizzaToppings }}
{{ $index }}: {{ $topping }}
{{- end }}
注意 range
在前面,然后是变量,然后是赋值运算符,然后才是列表,这会将整数索引(从0开始)分配给 $index
,并将 value 值分配给 $topping
,上面的内容会被渲染成如下内容:
toppings: |-
0: mushrooms
1: cheese
2: peppers
3: onions
对于同时具有 key 和 value 的数据结构,我们也可以使用 range
来获得 key、value 的值,比如,我们可以像这样循环遍历 .Values.favorite
:
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
{{- end }}
#values.yaml
favorite:
drink: coffee
food: pizza
pizzaToppings:
- mushrooms
- cheese
- peppers
- onions
在第一次迭代中,$key
是 drink
,$val
是 coffee
,在第二次迭代中,$key
是 food
,$val
是 pizza
。运行上面的命令将生成下面的内容:
➜ helm install --generate-name --dry-run ./mychart
# Source: mychart/templates/ConfigMap.yaml
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649427327-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "pizza"
3️⃣ 一般来说变量不是全局的,它们的作用域是声明它们的块区域。之前,我们在模板的顶层分配了 $relname
,该变量将在整个模板的范围内,但是在我们上面的示例中,$key
和 $val
作用域只在 {{ range... }}{{ end }}
区域内。
但是,有一个始终是全局变量的 $
始终指向顶层根上下文,当我们在 range
循环内需要知道 chart 包的 release 名称的时候,该功能就非常有用了,比如下面的模板文件:
{{- range .Values.tlsSecrets }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .name }}
labels:
# helm 模板经常使用 `.`,但是这里是无效的,用 `$` 是可以生效的。
app.kubernetes.io/name: {{ template "fullname" $ }}
# 这里不能引用 `.Chart.Name`,但是可用使用 `$.Chart.Name`
helm.sh/chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}"
app.kubernetes.io/instance: "{{ $.Release.Name }}"
# 值来自于 Chart.yaml 文件中的 appVersion
app.kubernetes.io/version: "{{ $.Chart.AppVersion }}"
app.kubernetes.io/managed-by: "{{ $.Release.Service }}"
type: kubernetes.io/tls
data:
tls.crt: {{ .certificate }}
tls.key: {{ .key }}
---
{{- end }}
我们这里继续用上面案例进行测试:这里我们先使用变量方式
测试。
#ConfigMap.yaml
{{- $relname := .Release.Name }}
{{- $chartName := .Chart.Name}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
chartName: {{ $chartName }}
{{- end }}
#values.yaml
favorite:
drink: coffee
food: pizza
pizzaToppings:
- mushrooms
- cheese
- peppers
- onions
渲染后可以得到正确结果:
$ helm install --generate-name --dry-run ./mychart
---
# Source: mychart/templates/ConfigMap.yaml
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649431829-configmap
data:
myvalue: "Hello World"
drink: "coffee"
chartName: mychart
food: "pizza"
chartName: mychart
我们再次使用全局变量的 $
来进行测试:
#ConfigMap.yaml
{{- $relname := .Release.Name }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
chartName: {{ $.Chart.Name }}
{{- end }}
#values.yaml
favorite:
drink: coffee
food: pizza
pizzaToppings:
- mushrooms
- cheese
- peppers
- onions
同样渲染后可以得到正确结果:
$ helm install --generate-name --dry-run ./mychart
---
# Source: mychart/templates/ConfigMap.yaml
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649432089-configmap
data:
myvalue: "Hello World"
drink: "coffee"
chartName: mychart
food: "pizza"
chartName: mychart
测试结束。
到现在为止,我们只研究了在一个文件中声明的一个模板,但是,Helm 模板语言的强大功能之一是它能够声明多个模板并将其一起使用。我们将在下面的章节中来讨论这一点。
前面我们都是只操作的一个模板,现在我们来尝试使用多个模板文件。在本节中,我们可以了解到如何在一个文件中定义命名模板,然后在其他地方使用它们。命名模板(有时也叫子模板)只是在文件内部定义的有名称的模板。主要有两种创建方式以及几种不同的使用方式。
当使用命名模板的时候有几个重要细节:模板名称是全局的,如果声明两个具有相同名称的模板,则会使用最后被加载的模板。由于子 chart 中的模板是与顶级模板一起编译的,所以需要谨慎命名。
一种流行的命名约定是在每个定义的模板前添加 chart 名称:{{ define "mychart.labels" }}
,通过使用特定的 chart 名作为前缀,我们可以避免由于两个不同的 chart 实现了相同名称的模板而引起的冲突。
到目前为止,我们只使用了一个模板文件,但是 Helm 的模板语言允许我们创建命名的嵌入式模板,可以在其他位置进行访问。在编写这些模板之前,有一些值得一提的命名约定:
templates/
中的大多数文件都被视为 Kubernetes 资源清单文件(NOTES.txt 除外)_
开头命名的文件也不会被当做 Kubernetes 资源清单文件_
开头的这些文件其实就是 Helm 中的 partials
文件,所以其实我们完全可以将命名模板定义在这些 partials
文件中,默认就是 _helpers.tpl
文件,其实在前面我们创建的 mychart
包中也可以找到这个文件。
define
和 template
示例:define和template测试-2022.4.9(测试成功) |
define
关键字可以让我们在模板文件中创建命名模板,它的语法如下所示:
{{ define "MY.NAME" }}
# 模板内容区域
{{ end }}
1️⃣ 将命名模板放在模板文件里
比如我们可以定义一个模板来封装下 Kubernetes 的 labels 标签:
{{- define "mychart.labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
{{- end }}
现在我们可以将该模板嵌入到前面的 ConfigMap 模板中,然后将其包含在模板中:
{{- define "mychart.labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
{{- template "mychart.labels" }}
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
{{- end }}
当模板引擎读取这个文件的时候,它会存储 mychart.labels
的引用,直到该模板被调用,然后会内联渲染该模板。我们渲染这个模板可以都到如下所示的结果(记得先删掉默认生成的 _helpers.tpl
文件):
➜ helm install --generate-name --dry-run ./mychart
# Source: mychart/templates/ConfigMap.yaml
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649491252-configmap
labels:
generator: helm
date: 2022-04-09
data:
myvalue: "Hello World"
drink: "coffee"
chartName: mychart
food: "pizza"
chartName: mychart
2️⃣ 将命名模板放在_helpers.tpl里
一般来说,Helm 中约定将这些模板统一放到一个 partials 文件中,通常就是 _helpers.tpl
文件中,我们将上面的命名模板移动到该文件(templates/_helpers.tpl
)中去:
这里,我们先创建下这个_helpers.tpl
文件:
{{/* 生成基本的 Label 标签 */}}
{{- define "mychart.labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
{{- end }}
一般来说,我们也会用一个简单的块({{/*...*/}}
)来注释这个命名模板的作用。
现在虽然我们把命名模板放到了 _helpers.tpl
文件中,但是我们在 configmap.yaml
模板中还是可以访问,因为命名模板是全局的:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
{{- template "mychart.labels" }}
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
{{- end }}
因为上面我们提到过命名模板是全局的,我们可以再渲染下上面的模板可以得到正确的结果。
测试结束。
示例:设置模板范围-2022.4.9(测试成功) |
1️⃣ 上面我们定义的模板中,还没有使用到任何对象,只使用了函数,现在我们来修改下定义的命名模板,包含 chart 的名称和版本:
# _heplers.tpl
{{/* 生成基本的 Label 标签 */}}
{{- define "mychart.labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
chartName: {{ .Chart.Name }}
chartVersion: {{ .Chart.Version }}
releaseName: {{ .Release.Name }}
{{- end }}
现在我们来渲染下模板,会出现下面的错误:
➜ helm install --generate-name --dry-run ./mychart
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hg/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hg/.kube/config
Error: INSTALLATION FAILED: unable to build kubernetes objects from release manifest: error validating "": error validating data: [unknown object type "nil" in ConfigMap.metadata.labels.chartName, unknown object type "nil" in ConfigMap.metadata.labels.chartVersion, unknown object type "nil" in ConfigMap.metadata.labels.releaseName]
……
我们可以看到提示 labels.chart
为 nil
,这是因为我们使用的 .Chart.Name
不在定义的这个模板的作用域范围内,当渲染命名模板(使用 define
定义)的时候,它将接收模板调用传递的作用域。在我们这个示例中,我们是这样引用这个模板的:
{{- template "mychart.labels" }}
2️⃣ 没有传入任何作用域,所以在模板内我们无法访问 .
中的任何内容,当然要解决很简单,我们只需要把作用域范围传递给模板即可:
#ConfigMap.yaml
{{- $relname := .Release.Name }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
{{- template "mychart.labels" . }}
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
chartName: {{ $.Chart.Name }}
{{- end }}
我们这里在使用 template
调用模板的时候传递了 .
,我们可以很容易传递 .Values
或者 .Values.favorite
或者我们想要的任何范围,但是这里我们想要的是顶级作用域,所以我们传递的是 .
。
现在我们再来重新渲染我们的模板,可以得到如下所示的结果:
# Source: mychart/templates/ConfigMap.yaml
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649492314-configmap
labels:
generator: helm
date: 2022-04-09
chartName: mychart
chartVersion: 0.1.0
releaseName: mychart-1649492314
data:
myvalue: "Hello World"
drink: "coffee"
chartName: mychart
food: "pizza"
chartName: mychart
现在 {{ .Chart.Name }}
解析为了 mychart,而 {{ .Chart.Version }}
解析为了 0.1.0
。
测试结束。
示例:include函数-2022.4.9(测试成功) |
1️⃣ 假设我们定义了一个如下所示的简单模板:
# _helpers.tpl
{{/* 定义一个简单的chart app模板*/}}
{{- define "mychart.app" -}}
app_name: {{ .Chart.Name }}
app_version: "{{ .Chart.Version }}"
{{- end -}}
现在我们想把上面的内容插入到模板的 labels
部分,在 data
部分也想要这个内容:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
labels:
{{ template "mychart.app" . }}
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
{{- end }}
{{ template "mychart.app" . }}
但是我们直接渲染上面的模板还是会有错误:
➜ helm install --generate-name --dry-run ./mychart
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hg/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hg/.kube/config
Error: INSTALLATION FAILED: unable to build kubernetes objects from release manifest: error validating "": error validating data: ValidationError(ConfigMap): unknown field "app_version" in io.k8s.api.core.v1.ConfigMap
➜ local-vscode
因为 template
只是一个动作,而不是一个函数,所以无法将模板调用的输出传递给其他函数,只是内联插入,相当于渲染的结果是这样的:
apiVersion: v1
kind: ConfigMap
metadata:
name: measly-whippet-configmap
labels:
app_name: mychart
app_version: "0.1.0+1478129847"
data:
myvalue: "Hello World"
drink: "coffee"
food: "pizza"
app_name: mychart
app_version: "0.1.0+1478129847"
⚠️ 需要注意:
2️⃣ 很明显上面的 YAML 文件是不符合 ConfigMap 资源对象的格式要求的,所以报错了。为解决这个问题,Helm 提供了代替 template
的函数 include
,可以将模板的内容导入到当前的管道中,这样就可以在管道中传递给其他函数进行处理了,如下所示:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
labels:
{{ include "mychart.app" . | indent 4 }}
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
{{- end }}
{{ include "mychart.app" . | indent 2 }}
现在我们重新渲染就可以得到正确的结果了,这是因为我们用 include
函数得到模板内容后通过管道传给了后面的 indent
函数来保证了缩进:
Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1576036671-configmap
labels:
app_name: mychart
app_version: "0.1.0"
data:
myvalue: "Hello World"
drink: "coffee"
food: "pizza"
app_name: mychart
app_version: "0.1.0"
⚠️ 建议
在 Helm 模板中最好使用
include
而不是template
,这样可以更好地处理 YAML 文档的输出格式。有时候如果我们只想导入内容而不是模板,这个时候我们可以通过下面描述的
.Files
对象来访问文件实现。
测试结束。
在上一节中我们介绍了几种创建和访问命名模板的方法,这使得从另一个模板中导入一个模板变得很容易,但是有时候需要导入一个不是模板的文件并注入其内容,而不通过模板渲染器获得内容。
Helm 提供了一个 .Files
对象对文件的访问,但是在模板中使用这个对象之前,还有几个需要注意的事项值得一提:
可以在 Helm chart 中添加额外的文件,这些文件也会被打包,不过需要注意,由于 Kubernetes 对象的存储限制,Charts 必须小于 1M
由于一些安全原因,通过 .Files
对象无法访问某些文件
templates/
下面的文件.helmignore
排除的文件Chart 不会保留 UNIX 模式的信息,所以,当使用 .Files
对象时,文件级别的权限不会对文件的可用性产生影响。
示例:访问文件基本示例-2022.4.9(测试成功) |
1️⃣ 现在我们来编写一个模板,将3个文件读入到 ConfigMap
模板中,首先我们在 chart 中添加3个文件,将3个文件都直接放置在 mychart/
目录中。
config1.toml
:
message = Hello from config 1
config2.toml
:
message = Hello from config 2
config3.toml
:
message = Hello from config 3
2️⃣ 3个文件都是简单的 TOML 文件,我们知道这些文件的名称,所以我们可以使用 range
函数来遍历它们,并将其内容注入到 ConfigMap
中去。
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
{{- $files := .Files }}
{{- range tuple "config1.toml" "config2.toml" "config3.toml" }}
{{ . }}: |-
{{ $files.Get . }}
{{- end }}
这里我们声明了一个 $files
的变量来保存 .Files
对象的引用,还使用了 tuple
函数来循环文件列表,然后我们打印每个文件夹 {{ . }}: |-
,后面使用 {{ $files.Get . }}
获取文件内容。
现在我们渲染这个模板会产生包含3个文件内容的单个 ConfigMap:
# Source: mychart/templates/ConfigMap.yaml
#ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649497695-configmap
data:
config1.toml: |-
message = Hello from config 1
config2.toml: |-
message = Hello from config 2
config3.toml: |-
message = Hello from config 3
⚠️ 注意:
另外在处理文件的时候,对文件路径本身执行一些标准操作可能非常有用,为了解决这个问题,Helm 从 Go 的路径包中导入了许多功能供你使用,它们都可以使用与 Go 包中相同的相同名称来访问,但是首字母需要小写,比如 Base 需要变成 base,导入的函数有:- Base - Dir - Ext - IsAbs - Clean。
测试结束。
随着 chart 的增长,你可能需要更多地组织文件,因此 Helm 提供了 Files.Glob
的方法来帮助我们获取具有 glob
模式的文件。
.Glob
返回 Files
类型,所以你可以在返回的对象上调用任何 Files
方法。比如,我们的文件目录结构如下所示:
foo/:
foo.txt foo.yaml
bar/:
bar.go bar.conf baz.yaml
我们可以用 Glob
进行多种选择:
{{ range $path := .Files.Glob "**.yaml" }}
{{ $path }}: |
{{ .Files.Get $path }}
{{ end }}
或者
{{ range $path, $bytes := .Files.Glob "foo/*" }}
{{ $path }}: '{{ b64enc $bytes }}'
{{ end }}
想要将文件内容同时放入 ConfigMap 和 Secrets 中,以便在运行时安装到 Pod 中,这种需求很常见,为了解决这个问题,Helm 在 Files
类型上添加了一个实用的方法。
根据上面的目录结构,我们可以按照如下的方式进行处理:
apiVersion: v1
kind: ConfigMap
metadata:
name: conf
data:
{{ (.Files.Glob "foo/*").AsConfig | indent 2 }}
---
apiVersion: v1
kind: Secret
metadata:
name: very-secret
type: Opaque
data:
{{ (.Files.Glob "bar/*").AsSecrets | indent 2 }}
示例:编码-2022.4.9(测试成功) |
1️⃣ 我们也可以导入一个文件并用 base64
编码进行编码:
我们来创建一个templates/secret.yaml
模板文件:
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-secret
type: Opaque
data:
token: |-
{{ .Files.Get "config1.toml" | b64enc }}
2️⃣ 上面将采用我们上面的 config1.toml
文件并对其内容进行 base64
编码,渲染会得到如下所示的结果:
$ helm install --generate-name --dry-run ./mychart
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: Secret
metadata:
name: mychart-1576048287-secret
type: Opaque
data:
token: |-
bWVzc2FnZSA9IEhlbGxvIGZyb20gY29uZmlnIDEK
测试结束。
有时,需要访问模板中文件的每一行内容,Helm 也提供了方法的 Lines
方法,我们可以使用 range
函数遍历每行内容:
data:
some-file.txt: {{ range .Files.Lines "foo/bar.txt" }}
{{ . }}{{ end }}
在 Helm 安装的时候无法将文件传递到 chart 外部,所以,如果你要求用户提供数据的话,则必须使用 helm install -f
或者 helm install --set
来获取。
在本节中我们将来了解为 chart 用户提供说明的一个 NOTES.txt
文件,在 chart 安装或者升级结束时,Helm 可以为用户打印出一些有用的信息,使用模板也可以自定义这些信息。
要将安装说明添加到 chart 中,只需要创建一个 templates/NOTES.txt
文件,该文件纯文本的,但是可以像模板一样进行处理,并具有所有常规模板的功能和可用对象。
示例:NOTES.txt-2022.4.9(测试成功) |
1️⃣ 现在让我们来创建一个简单的 templates/NOTES.txt
文件:
Thank you for installing {{ .Chart.Name }}.
Your release is named {{ .Release.Name }}.
To learn more about the release, try:
$ helm status {{ .Release.Name }}
$ helm get {{ .Release.Name }}
2️⃣ 现在我们运行 helm install ./mychart
,我们就可以在底部看到这样的消息:
RESOURCES:
==> v1/Secret
NAME TYPE DATA AGE
rude-cardinal-secret Opaque 1 0s
==> v1/ConfigMap
NAME DATA AGE
rude-cardinal-configmap 3 0s
NOTES:
Thank you for installing mychart.
Your release is named rude-cardinal.
To learn more about the release, try:
$ helm status rude-cardinal
$ helm get rude-cardinal
用这种方式可以向用户提供一个有关如何使用其新安装的 chart 的详细信息,强烈建议创建 NOTES.txt
文件,虽然这不是必须的。
测试结束。
到现在为止,我们从单一模板,到多个模板文件,但是都仅仅是处理的一个 chart 包,但是 charts 可能具有一些依赖项,我们称为 subcharts(子 chart)
,接下来我们将创建一个子 chart。
同样在深入了解之前,我们需要了解下子 chart 相关的一些信息。
示例:Subcharts和Clobal Values-2022.4.9(测试成功) |
同样还是在之前操作的 mychart/
这个 chart 包中,我们来尝试添加一些新的子 chart:
➜ cd mychart/charts
➜ helm create mysubchart
Creating mysubchart
➜ rm -rf mysubchart/templates/*.*
➜ echo > mysubchart/values.yaml
和前面一样,我们删除了所有的基本模板,这样我们可以从头开始。
接下来我们为 mysubchart 这个子 chart 创建一个简单的模板和 values 值文件,mychart/charts/mysubchart
中已经有一个 values.yaml
文件了,在文件中添加下面的 values:
dessert: cake
下面我们再创建一个新的 ConfigMap 模板 mychart/charts/mysubchart/templates/configmap.yaml
:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-cfgmap2
data:
dessert: {{ .Values.dessert }}
因为每个子 chart 都是独立的 chart,所以我们可以单独测试 mysubchart
:
➜ helm install --generate-name --dry-run ./mychart/charts/mysubchart
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hg/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hg/.kube/config
NAME: mysubchart-1649500624
LAST DEPLOYED: Sat Apr 9 18:37:05 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mysubchart-1649500624-cfgmap2
data:
dessert: cake
我们原来的 chart - mychart 现在是 mysubchart 的父级 chart 了。由于 mychart 是父级,所以我们可以在 mychart 中指定配置,并将该配置发送到 mysubchart 中去,比如,我们可以这样修改 mychart/values.yaml
:
favorite:
drink: coffee
food: pizza
pizzaToppings:
- mushrooms
- cheese
- peppers
- onions
mysubchart:
dessert: ice cream
最后两行,mysubchart
部分中的所有指令都会被发送到 mysubchart
子 chart 中,所以,如果我们现在渲染模板,我们可以看到 mysubchart
的 ConfigMap 会被渲染成如下的内容:
➜ helm install --generate-name --dry-run ./mychart #注意:本次是直接渲染父charts目录的
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1649500768-cfgmap2
data:
dessert: ice cream
我们可以看到顶层的 values 值覆盖了子 chart 中的值。这里有一个细节需要注意,我们没有将 mychart/charts/mysubchart/templates/configmap.yaml
模板更改为指向 .Values.mysubchart.dessert
,因为从该模板的角度来看,该值仍然位于 .Values.dessert
,当模板引擎传递 values 值的时候,它会设置这个作用域,所以,对于 mysubchart
模板,.Values
中仅仅提供用于该子 chart 的值。
但是有时候如果我们确实希望某些值可以用于所有模板,这个时候就可以使用全局 chart values 值来完成了。
全局值是可以从任何 chart 或子 chart 中都可以访问的值,全局值需要显示的声明,不能将现有的非全局对象当作全局对象使用。
Values 数据类型具有一个名为 Values.global
的保留部分,可以在其中设置全局值,我们在 mychart/values.yaml
文件中添加一个全局值:
favorite:
drink: coffee
food: pizza
pizzaToppings:
- mushrooms
- cheese
- peppers
- onions
mysubchart:
dessert: ice cream
global:
salad: caesar
由于全局值的原因,在 mychart/templates/configmap.yaml
和 mysubchart/templates/configmap.yaml
下面都应该可以以 {{ .Values.global.salad }}
的形式来访问这个值。
mychart/templates/configmap.yaml
:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
salad: {{ .Values.global.salad }}
mysubchart/templates/configmap.yaml
:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-cfgmap2
data:
dessert: {{ .Values.dessert }}
salad: {{ .Values.global.salad }}
然后我们渲染这个模板,可以得到如下所示的内容:
---
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1576053485-cfgmap2
data:
dessert: ice cream
salad: caesar
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-1576053485-configmap
data:
salad: caesar
全局值对于传递这样的数据比较有用。
父级 chart 和子 chart 可以共享模板,任何 chart 中已定义的块都可以用于其他 chart。比如,我们可以定义一个简单的模板,如下所示:
{{- define "labels" }}from: mychart{{ end }}
前面我们提到过可以使用在模板中使用 include
和 template
,但是使用 include
的一个优点是可以动态引入模板的内容:
{{ include $mytemplate }}
一般情况下,我们不会去使用共享模板,容易冲突。
测试结束。
调试模板可能比较麻烦,因为渲染的模板会发送到 Kubernetes API server,而 API server 可能会因为格式以外的一些原因而拒绝 YAML 文件。
下面这些命令可以帮助你调试一些问题:
helm lint
是验证 chart 是否遵循最佳实践的首选工具
helm install --dry-run --debug
或者 helm template --debug
:前面我们已经使用了这个技巧,这个是让服务器渲染模板,然后返回生成的资源清单文件的好方法,而且不会真正的去安装这些资源
helm get manifest
:这是查看服务器上安装了哪些模板的好方法
当你的 YAML 文件无法解析的时候,但你想要查看生成的内容的时候,检索 YAML 的一种简单方法是注释掉模板中的问题部分,然后重新运行 helm install --dry-run --debug
:
apiVersion: v2
# some: problem section
# {{ .Values.foo | quote }}
上面的内容将呈现并返回完整的注释:
apiVersion: v2
# some: problem section
# "bar"
这提供了一种查看生成的内容的快速方法。
Helm 也提供了一种 Hook 机制,可以允许 chart 开发人员在 release 生命周期的某些时间点进行干预。比如,可以使用 hook 来进行下面的操作:
Hooks 的工作方式类似于普通的模板,但是他们具有特殊的注解,这些注解使 Helm 可以用不同的方式来使用他们。
在 Helm 中定义了如下一些可供我们使用的 Hooks:
pre-install
:在模板渲染后,kubernetes 创建任何资源之前执行post-install
:在所有 kubernetes 资源安装到集群后执行pre-delete
:在从 kubernetes 删除任何资源之前执行删除请求post-delete
:删除所有 release 的资源后执行pre-upgrade
:在模板渲染后,但在任何资源升级之前执行post-upgrade
:在所有资源升级后执行pre-rollback
:在模板渲染后,在任何资源回滚之前执行post-rollback
:在修改所有资源后执行回滚请求test
:在调用 Helm test
子命令的时候执行(可以查看测试文档)Hooks 允许开发人员在 release 的生命周期中的一些关键节点执行一些钩子函数,我们正常安装一个 chart 包的时候的生命周期如下所示:
helm install foo
foo
模板如果开发人员在 install
的生命周期中定义了两个 hook:pre-install
和post-install
,那么我们安装一个 chart 包的生命周期就会多一些步骤了:
helm install foo
crds/
目录下面的 CRDs 被安装foo
模板pre-install
hooks--wait
参数,Helm 库会等待所有资源都准备好,在这之前不会运行 post-install
hookpost-install
hook(加载 hook 资源)等待 hook 准备就绪,这是一个阻塞的操作,如果 hook 中声明的是一个 Job 资源,Helm 将等待 Job 成功完成,如果失败,则发布失败,在这个期间,Helm 客户端是处于暂停状态的。
对于所有其他类型,只要 kubernetes 将资源标记为加载(添加或更新),资源就被视为就绪状态,当一个 hook 声明了很多资源是,这些资源是被串行执行的。
另外需要注意的是 hook 创建的资源不会作为 release 的一部分进行跟踪和管理,一旦 Helm 验证了 hook 已经达到了就绪状态,它就不会去管它了。
所以,如果我们在 hook 中创建了资源,那么不能依赖 helm uninstall
去删除资源,因为 hook 创建的资源已经不受控制了,要销毁这些资源,你需要将 helm.sh/hook-delete-policy
这个 annotation 添加到 hook 模板文件中,或者设置 Job 资源的生存(TTL)字段。
示例:编写hook-2022.4.10(测试成功) |
Hooks 就是 Kubernetes 资源清单文件,在元数据部分带有一些特殊的注解,因为他们是模板文件,所以你可以使用普通模板所有的功能,包括读取 .Values
、.Release
和 .Template
。
1️⃣ 例如,在 templates/post-install-job.yaml
文件中声明一个 post-install 的 hook:
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ .Release.Name }}"
labels:
app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
annotations:
# 因为添加了这个 hook,所以我们这个资源被定义为了 hook
# 如果没有这行,则当前这个 Job 会被当成 release 的一部分内容。
"helm.sh/hook": post-install
"helm.sh/hook-weight": "-5"
#"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
metadata:
name: "{{ .Release.Name }}"
labels:
app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
restartPolicy: Never
containers:
- name: post-install-job
image: "alpine:3.3"
command: ["/bin/sleep","{{ default "10" .Values.sleepyTime }}"]
当前这个模板成为 hook 的原因就是添加这个注解:
annotations:
"helm.sh/hook": post-install
2️⃣ 给values.yaml添加如下内容
#values.yaml
……
sleepyTime: 5
3️⃣ 安装并测试
➜ local-vscode helm install test ./mychart
4️⃣ 一种资源也可以实现多个 hooks:
annotations:
"helm.sh/hook": post-install,post-upgrade
类似的,实现给定 hook 的资源数量也没有限制,比如可以将 secret 和一个 configmap 都声明为 pre-install
hook。
当子 chart 声明 hooks 的时候,也会对其进行调用,顶层的 chart 无法禁用子 chart 所声明的 hooks。可以为 hooks 定义权重,这将有助于确定 hooks 的执行顺序:
annotations:
"helm.sh/hook-weight": "5"
hook 权重可以是正数也可以是负数,但是必须用字符串表示,当 Helm 开始执行特定种类的 hooks 的时候,它将以升序的方式对这些 hooks 进行排序。
我们还可以定义确定何时删除相应 hook 资源的策略,hook 删除策略可以使用下面的注解进行定义:
annotations:
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
我们也可以选择一个或多个已定义的注解:
before-hook-creation
:运行一个新的 hook 之前删除前面的资源(默认)hook-succeeded
:hook 成功执行后删除资源hook-failed
:hook 如果执行失败则删除资源如果未指定任何 hook 删除策略注解,则默认情况下会使用 before-hook-creation
策略。
我的博客主旨:我希望每一个人拿着我的博客都可以做出实验现象,先把实验做出来,然后再结合理论知识更深层次去理解技术点,这样学习起来才有乐趣和动力。并且,我的博客内容步骤是很完整的,也分享源码和实验用到的软件,希望能和大家一起共同进步!
各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人免费帮您解决问题:
个人微信二维码:x2675263825 (舍得), qq:2675263825。
个人微信公众号:云原生架构师实战
个人博客地址:www.onlyonexl.cn
个人csdn
https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421
个人已开源干货
名称 | 链接 |
---|---|
实战:打造一款王者云笔记:typora+坚果云+阿里云oss & 定制宇宙中最美的typora主题皮肤 & 玩转vscode(永久开源) | https://www.jianguoyun.com/p/DXS6qiIQvPWVCRiS0qoE |
好了,关于helm模板开发就到这里了,感谢大家阅读,最后贴上我女神的photo,祝大家生活快乐,每天都过的有意义哦,我们下期见!