多读书多实践,勤思考善领悟

kubernetes包管理工具Helm之 二.Helm模板使用

本文于2093天之前发表,文中内容可能已经过时。

接上文: kubernetes包管理工具Helm之 一.Helm安装


内置函数和Values

定义一个chart包,了解 Helm 中模板的一些使用方法。

定义 chart

Helm 的 github 上面有一个比较完整的文档,建议大家好好阅读下该文档,这里我们来一起创建一个chart包。

一个 chart 包就是一个文件夹的集合,文件夹名称就是 chart 包的名称,比如创建一个 mychart 的 chart 包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ helm create mychart
Creating mychart
$ tree mychart/
mychart/
├── charts
├── Chart.yaml
├── templates
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── ingress.yaml
│ ├── NOTES.txt
│ └── service.yaml
└── values.yaml

2 directories, 7 files

chart 包的目录上节课我们就已经学习过了,这里我们再来仔细看看 templates 目录下面的文件:

  • NOTES.txt:chart 的 “帮助文本”。这会在用户运行 helm install 时显示给用户。
  • deployment.yaml:创建 Kubernetes deployment 的基本 manifest
  • service.yaml:为 deployment 创建 service 的基本 manifest
  • ingress.yaml: 创建 ingress 对象的资源清单文件
  • _helpers.tpl:放置模板助手的地方,可以在整个 chart 中重复使用

这里我们明白每一个文件是干嘛的就行,然后我们把 templates 目录下面所有文件全部删除掉,这里我们自己来创建模板文件:

1
$ rm -rf mychart/templates/*.*

创建模板

这里我们来创建一个非常简单的模板 ConfigMap,在 templates 目录下面新建一个configmap.yaml文件:

1
2
3
4
5
6
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-configmap
data:
myvalue: "Hello World"

实际上现在我们就有一个可安装的 chart 包了,通过helm install命令来进行安装:

1
2
3
4
5
6
7
8
9
10
$ helm install ./mychart/
NAME: ringed-lynx
LAST DEPLOYED: Fri Sep 7 22:59:22 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
mychart-configmap 1 0s

在上面的输出中,我们可以看到我们的 ConfigMap 资源对象已经创建了。然后使用如下命令我们可以看到实际的模板被渲染过后的资源文件:

1
2
3
4
5
6
7
8
9
10
$ helm get manifest ringed-lynx

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-configmap
data:
myvalue: "Hello World"

现在我们看到上面的 ConfigMap 文件是不是正是我们前面在模板文件中设计的,现在我们删除当前的release:

1
2
$ helm delete ringed-lynx
release "ringed-lynx" deleted

添加一个简单的模板

我们可以看到上面我们定义的 ConfigMap 的名字是固定的,但往往这并不是一种很好的做法,我们可以通过插入 release 的名称来生成资源的名称,比如这里 ConfigMap 的名称我们希望是:ringed-lynx-configmap,这就需要用到 Chart 的模板定义方法了。

Helm Chart 模板使用的是Go语言模板编写而成,并添加了Sprig中的50多个附件模板函数以及一些其他特殊的函

需要注意的是kubernetes资源对象的 labels 和 name 定义被限制 63个字符,所以需要注意名称的定义。

现在我们来重新定义下上面的 configmap.yaml 文件:

1
2
3
4
5
6
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: "Hello World"

我们将名称替换成了{{.Release.Name }}-configmap,其中包含在{{}}之中的就是模板指令,{{.Release.Name }} 将 release 的名称注入到模板中来,这样最终生成的 ConfigMap 名称就是以 release 的名称开头的了。这里的 Release 模板对象属于 Helm 内置的一种对象,还有其他很多内置的对象,稍后我们将接触到。

现在我们来重新安装我们的 Chart 包,注意观察 ConfigMap 资源对象的名称:

1
2
3
4
5
6
7
8
9
10
11
$ helm install ./mychart
helm install ./mychart/
NAME: quoting-zebra
LAST DEPLOYED: Fri Sep 7 23:20:12 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
quoting-zebra-configmap 1 0s

可以看到现在生成的名称变成了quoting-zebra-configmap,证明已经生效了,当然我们也可以使用命令helm get manifest quoting-zebra查看最终生成的清单文件的样子。

调试

我们用模板来生成资源文件的清单,但是如果我们想要调试就非常不方便了,不可能我们每次都去部署一个release实例来校验模板是否正确,所幸的时 Helm 为我们提供了--dry-run --debug这个可选参数,在执行helm install的时候带上这两个参数就可以把对应的 values 值和生成的最终的资源清单文件打印出来,而不会真正的去部署一个release实例,比如我们来调试上面创建的 chart 包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ helm install . --dry-run --debug ./mychart
[debug] Created tunnel using local port: '35286'

[debug] SERVER: "127.0.0.1:35286"

[debug] Original chart version: ""
[debug] CHART PATH: /root/course/kubeadm/helm/mychart

NAME: wrapping-bunny
REVISION: 1
RELEASED: Fri Sep 7 23:23:09 2018
CHART: mychart-0.1.0
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
...
HOOKS:
MANIFEST:

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: wrapping-bunny-configmap
data:
myvalue: "Hello World"

现在我们使用--dry-run就可以很容易地测试代码了,不需要每次都去安装一个 release 实例了,但是要注意的是这不能确保 Kubernetes 本身就一定会接受生成的模板,在调试完成后,还是需要去安装一个实际的 release 实例来进行验证的。

内置对象

刚刚我们使用{{.Release.Name}}将 release 的名称插入到模板中。这里的 Release 就是 Helm 的内置对象,下面是一些常用的内置对象,在需要的时候直接使用就可以:

  • Release:这个对象描述了 release 本身。它里面有几个对象:
    • Release.Name:release 名称
    • Release.Time:release 的时间
    • Release.Namespace:release 的 namespace(如果清单未覆盖)
    • Release.Service:release 服务的名称(始终是 Tiller)。
    • Release.Revision:此 release 的修订版本号,从1开始累加。
    • Release.IsUpgrade:如果当前操作是升级或回滚,则将其设置为 true。
    • Release.IsInstall:如果当前操作是安装,则设置为 true。
  • Values:从values.yaml文件和用户提供的文件传入模板的值。默认情况下,Values 是空的。
  • Chart:Chart.yaml文件的内容。所有的 Chart 对象都将从该文件中获取。chart 指南中Charts Guide列出了可用字段,可以前往查看。
  • Files:这提供对 chart 中所有非特殊文件的访问。虽然无法使用它来访问模板,但可以使用它来访问 chart 中的其他文件。请参阅 “访问文件” 部分。
    • Files.Get 是一个按名称获取文件的函数(.Files.Get config.ini)
    • Files.GetBytes 是将文件内容作为字节数组而不是字符串获取的函数。这对于像图片这样的东西很有用。
  • Capabilities:这提供了关于 Kubernetes 集群支持的功能的信息。
    • Capabilities.APIVersions 是一组版本信息。
    • Capabilities.APIVersions.Has $version 指示是否在群集上启用版本(batch/v1)。
    • Capabilities.KubeVersion 提供了查找 Kubernetes 版本的方法。它具有以下值:Major,Minor,GitVersion,GitCommit,GitTreeState,BuildDate,GoVersion,Compiler,和 Platform。
    • Capabilities.TillerVersion 提供了查找 Tiller 版本的方法。它具有以下值:SemVer,GitCommit,和 GitTreeState。
  • Template:包含有关正在执行的当前模板的信息
  • Name:到当前模板的文件路径(例如 mychart/templates/mytemplate.yaml)
  • BasePath:当前 chart 模板目录的路径(例如 mychart/templates)。

上面这些值可用于任何顶级模板,要注意内置值始终以大写字母开头。这也符合Go的命名约定。当你创建自己的名字时,你可以自由地使用适合你的团队的惯例。

values 文件

上面的内置对象中有一个对象就是 Values,该对象提供对传入 chart 的值的访问,Values 对象的值有4个来源:

  • chart 包中的 values.yaml 文件
  • 父 chart 包的 values.yaml 文件
  • 通过 helm install 或者 helm upgrade 的-f或者--values参数传入的自定义的 yaml 文件(上节课我们已经学习过)
  • 通过--set 参数传入的值

chart 的 values.yaml 提供的值可以被用户提供的 values 文件覆盖,而该文件同样可以被--set提供的参数所覆盖。

这里我们来重新编辑 mychart/values.yaml 文件,将默认的值全部清空,添加一个新的数据:(values.yaml)

1
course: k8s

然后我们在上面的 templates/configmap.yaml 模板文件中就可以使用这个值了:(configmap.yaml)

1
2
3
4
5
6
7
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: "Hello World"
course: {{ '{{' }}.Values.course }}

可以看到最后一行我们是通过{{.Values.course }}来获取 course 的值的。现在我们用 debug 模式来查看下我们的模板会被如何渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ helm install --dry-run --debug ./mychart
helm install --dry-run --debug .
[debug] Created tunnel using local port: '33509'

[debug] SERVER: "127.0.0.1:33509"

[debug] Original chart version: ""
[debug] CHART PATH: /root/course/kubeadm/helm/mychart

NAME: nasal-anaconda
REVISION: 1
RELEASED: Sun Sep 9 17:37:52 2018
CHART: mychart-0.1.0
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
course: k8s

HOOKS:
MANIFEST:

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nasal-anaconda-configmap
data:
myvalue: "Hello World"
course: k8s

我们可以看到 ConfigMap 中 course 的值被渲染成了 k8s,这是因为在默认的 values.yaml 文件中该参数值为 k8s,同样的我们可以通过--set参数来轻松的覆盖 course 的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ helm install --dry-run --debug --set course=python ./mychart
[debug] Created tunnel using local port: '44571'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: named-scorpion-configmap
data:
myvalue: "Hello World"
course: python

由于--set 比默认 values.yaml 文件具有更高的优先级,所以我们的模板生成为 course: python。

values 文件也可以包含更多结构化内容,例如,我们在 values.yaml 文件中可以创建 course 部分,然后在其中添加几个键:

1
2
3
course:
k8s: devops
python: django

现在我们稍微修改模板:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ '{{' }}.Values.course.k8s }}
python: {{ '{{' }}.Values.course.python }}

同样可以使用 debug 模式查看渲染结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '33801'
......
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: exhaling-turtle-configmap
data:
myvalue: "Hello World"
k8s: devops
python: django

可以看到模板中的参数已经被 values.yaml 文件中的值给替换掉了。虽然以这种方式构建数据是可以的,但我们还是建议保持 value 树浅一些,平一些,这样维护起来要简单一点。

到这里,我们已经看到了几个内置对象的使用方法,并用它们将信息注入到了模板之中。下节课我们来看看模板引擎中的其他用法,比如函数、管道、控制结构等等的用法。

模板函数与管道

上面学习了如何将信息渲染到模板之中,但是这些信息都是直接传入模板引擎中进行渲染的,有的时候我们想要转换一下这些数据才进行渲染,这就需要使用到 Go 模板语言中的一些其他用法。

模板函数

比如我们需要从.Values中读取的值变成字符串的时候就可以通过调用quote模板函数来实现:(templates/configmap.yaml)

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ '{{' }}quote .Values.course.k8s }}
python: {{ '{{' }}.Values.course.python }}

模板函数遵循调用的语法为:functionName arg1 arg2...。在上面的模板文件中,quote .Values.course.k8s调用quote函数并将后面的值作为一个参数传递给它。最终被渲染为:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '39405'
......
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: masked-saola-configmap
data:
myvalue: "Hello World"
k8s: "devops"
python: django

我们可以看到.Values.course.k8s被渲染成了字符串devops。上节课我们也提到过 Helm 是一种 Go 模板语言,拥有超过60多种可用的内置函数,一部分是由Go 模板语言本身定义的,其他大部分都是Sprig模板库提供的一部分,我们可以前往这两个文档中查看这些函数的用法。

比如我们这里使用的quote函数就是Sprig 模板库提供的一种字符串函数,用途就是用双引号将字符串括起来,如果需要双引号",则需要添加\来进行转义,而squote函数的用途则是用双引号将字符串括起来,而不会对内容进行转义。

所以在我们遇到一些需求的时候,首先要想到的是去查看下上面的两个模板文档中是否提供了对应的模板函数,这些模板函数可以很好的解决我们的需求。

管道

模板语言除了提供了丰富的内置函数之外,其另一个强大的功能就是管道的概念。和UNIX中一样,管道我们通常称为Pipeline,是一个链在一起的一系列模板命令的工具,以紧凑地表达一系列转换。简单来说,管道是可以按顺序完成一系列事情的一种方法。比如我们用管道来重写上面的 ConfigMap 模板:(templates/configmap.yaml)

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ '{{' }}.Values.course.k8s | quote }}
python: {{ '{{' }}.Values.course.python }}

这里我们直接调用quote函数,而是调换了一个顺序,使用一个管道符|将前面的参数发送给后面的模板函数{{.Values.course.k8s | quote }},使用管道我们可以将几个功能顺序的连接在一起,比如我们希望上面的 ConfigMap 模板中的 k8s 的 value 值被渲染后是大写的字符串,则我们就可以使用管道来修改:(templates/configmap.yaml)

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ '{{' }}.Values.course.k8s | upper | quote }}
python: {{ '{{' }}.Values.course.python }}

这里我们在管道中增加了一个upper函数,该函数同样是Sprig 模板库提供的,表示将字符串每一个字母都变成大写。然后我们用debug模式来查看下上面的模板最终会被渲染成什么样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '46651'
......
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: maudlin-labradoodle-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: django

我们可以看到之前我们的devops已经被渲染成了"DEVOPS"了,要注意的是使用管道操作的时候,前面的操作结果会作为参数传递给后面的模板函数,比如我们这里希望将上面模板中的 python 的值渲染为重复出现3次的字符串,则我们就可以使用到Sprig 模板库提供的repeat函数,不过该函数需要传入一个参数repeat COUNT STRING表示重复的次数:(templates/configmap.yaml)

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ '{{' }}.Values.course.k8s | upper | quote }}
python: {{ '{{' }}.Values.course.python | quote | repeat 3 }}

repeat函数会将给定的字符串重复3次返回,所以我们将得到这个输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
helm install --dry-run --debug .
[debug] Created tunnel using local port: '39712'

......

Error: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 7: did not find expected key

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: piquant-butterfly-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "django""django""django"

我们可以看到上面的输出中 python 对应的值变成了3个相同的字符串,这显然是不符合我们预期的,我们的预期是形成一个字符串,而现在是3个字符串了,而且上面还有错误信息,根据管道处理的顺序,我们将quote函数放到repeat函数后面去是不是就可以解决这个问题了:(templates/configmap.yaml)

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ '{{' }}.Values.course.k8s | upper | quote }}
python: {{ '{{' }}.Values.course.python | repeat 3 | quote }}

现在是不是就是先重复3次.Values.course.python的值,然后调用quote函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
helm install --dry-run --debug .
[debug] Created tunnel using local port: '33837'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: braided-manatee-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjango"

现在是不是就正常了,也得到了我们的预期结果,所以我们在使用管道操作的时候一定要注意是按照从前到后一步一步顺序处理的。

default 函数

另外一个我们会经常使用的一个函数是default 函数default DEFAULT_VALUE GIVEN_VALUE。该函数允许我们在模板内部指定默认值,以防止该值被忽略掉了。比如我们来修改上面的 ConfigMap 的模板:(templates/configmap.yaml)

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: {{ '{{' }}.Values.hello | default "Hello World" | quote }}
k8s: {{ '{{' }}.Values.course.k8s | upper | quote }}
python: {{ '{{' }}.Values.course.python | repeat 5 | quote }}

由于我们的values.yaml文件中只定义了 course 结构的信息,并没有定义 hello 的值,所以如果没有设置默认值的话是得不到{{.Values.hello }}的值的,这里我们为该值定义了一个默认值:Hello World,所以现在如果在values.yaml文件中没有定义这个值,则我们也可以得到默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '42670'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: orbiting-hog-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjangodjangodjango"

我们可以看到myvalue值被渲染成了Hello World,证明我们的默认值生效了。

控制流程

模板函数和管道是通过转换信息并将其插入到YAML文件中的强大方法。但有时候需要添加一些比插入字符串更复杂一些的模板逻辑。这就需要使用到模板语言中提供的控制结构了。

控制流程为我们提供了控制模板生成流程的一种能力,Helm 的模板语言提供了以下几种流程控制:

  • if/else 条件块
  • with 指定范围
  • range 循环块

除此之外,它还提供了一些声明和使用命名模板段的操作:

  • define在模板中声明一个新的命名模板
  • template导入一个命名模板
  • block声明了一种特殊的可填写的模板区域

关于命名模板的相关知识点,我们会在后面的课程中和大家接触到,这里我们暂时和大家介绍if/elsewithrange这3中控制流程的用法。

if/else 条件

if/else块是用于在模板中有条件地包含文本块的方法,条件块的基本结构如下:

1
2
3
4
5
6
7
{{ '{{' }}if PIPELINE }}
# Do something
{{ '{{' }}else if OTHER PIPELINE }}
# Do something else
{{ '{{' }}else }}
# Default case
{{ '{{' }}end }}

当然要使用条件块就得判断条件是否为真,如果值为下面的几种情况,则管道的结果为 false:

  • 一个布尔类型的
  • 一个数字
  • 一个的字符串
  • 一个nil(空或null
  • 一个空的集合(mapslicetupledictarray

除了上面的这些情况外,其他所有条件都为

同样还是以上面的 ConfigMap 模板文件为例,添加一个简单的条件判断,如果 python 被设置为 django,则添加一个web: true:(tempaltes/configmap.yaml)

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: {{ '{{' }}.Values.hello | default "Hello World" | quote }}
k8s: {{ '{{' }}.Values.course.k8s | upper | quote }}
python: {{ '{{' }}.Values.course.python | repeat 3 | quote }}
{{ '{{' }}if eq .Values.course.python "django" }}web: true{{ '{{' }}end }}

在上面的模板文件中我们增加了一个条件语句判断{{if eq .Values.course.python "django" }}web: true{{end }},其中运算符eq是判断是否相等的操作,除此之外,还有neltgtandor等运算符都是 Helm 模板已经实现了的,直接使用即可。这里我们{{.Values.course.python }}的值在values.yaml文件中默认被设置为了django,所以正常来说下面的条件语句判断为,所以模板文件最终被渲染后会有web: true这样的的一个条目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '40143'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: fallacious-prawn-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjangodjangodjango"
web: true

可以看到上面模板被渲染后出现了web: true的条目,如果我们在安装的时候覆盖下 python 的值呢,比如我们改成 ai:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
helm install --dry-run --debug --set course.python=ai .
[debug] Created tunnel using local port: '42802'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: dull-mite-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "aiaiai"

根据我们模板文件中的定义,如果{{.Values.course.python }}的值为django的话就会新增web: true这样的一个条目,但是现在我们是不是通过参数--set将值设置为了 ai,所以这里条件判断为,正常来说就不应该出现这个条目了,上面我们通过 debug 模式查看最终被渲染的值也没有出现这个条目,证明条件判断是正确的。

空格控制

上面我们的条件判断语句是在一整行中的,如果平时经常写代码的同学可能非常不习惯了,我们一般会将其格式化为更容易阅读的形式,比如:

1
2
3
{{ '{{' }}if eq .Values.course.python "django" }}
web: true
{{ '{{' }}end }}

这样的话看上去比之前要清晰很多了,但是我们通过模板引擎来渲染一下,会得到如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '44537'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: bald-narwhal-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjango"

web: true

我们可以看到渲染出来会有多余的空行,这是因为当模板引擎运行时,它将一些值渲染过后,之前的指令被删除,但它之前所占的位置完全按原样保留剩余的空白了,所以就出现了多余的空行。YAML文件中的空格是非常严格的,所以对于空格的管理非常重要,一不小心就会导致你的YAML文件格式错误。

我们可以通过使用在模板标识{{后面添加破折号和空格{{-来表示将空白左移,而在}}前面添加一个空格和破折号-}}表示应该删除右边的空格,另外需要注意的是换行符也是空格!

使用这个语法,我们来修改我们上面的模板文件去掉多余的空格:(templates/configmap.yaml)

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: {{ '{{' }}.Values.hello | default "Hello World" | quote }}
k8s: {{ '{{' }}.Values.course.k8s | upper | quote }}
python: {{ '{{' }}.Values.course.python | repeat 3 | quote }}
{{ '{{' }}- if eq .Values.course.python "django" }}
web: true
{{ '{{' }}- end }}

现在我们来查看上面模板渲染过后的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '34702'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mangy-olm-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjango"
web: true

现在是不是没有多余的空格了,另外我们需要谨慎使用-}},比如上面模板文件中:

1
2
3
4
python: {{ '{{' }}.Values.course.python | repeat 3 | quote }}
{{ '{{' }}- if eq .Values.course.python "django" -}}
web: true
{{ '{{' }}- end }}

如果我们在if条件后面增加-}},这会渲染成:

1
python: "djangodjangodjango"web: true

因为-}}它删除了双方的换行符,显然这是不正确的。

有关模板中空格控制的详细信息,请参阅官方 Go 模板文档Official Go template documentation

使用 with 修改范围

接下来我们来看下with关键词的使用,它用来控制变量作用域。还记得之前我们的{{.Release.xxx }}或者{{.Values.xxx }}吗?其中的.就是表示对当前范围的引用,.Values就是告诉模板在当前范围中查找Values对象的值。而with语句就可以来控制变量的作用域范围,其语法和一个简单的if语句比较类似:

1
2
3
{{ '{{' }}with PIPELINE }}
# restricted scope
{{ '{{' }}end }}

with语句可以允许将当前范围.设置为特定的对象,比如我们前面一直使用的.Values.course,我们可以使用with来将.范围指向.Values.course:(templates/configmap.yaml)

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: {{ '{{' }}.Values.hello | default "Hello World" | quote }}
{{ '{{' }}- with .Values.course }}
k8s: {{ '{{' }}.k8s | upper | quote }}
python: {{ '{{' }}.python | repeat 3 | quote }}
{{ '{{' }}- if eq .python "django" }}
web: true
{{ '{{' }}- end }}
{{ '{{' }}- end }}

可以看到上面我们增加了一个{{- with .Values.course }}xxx{{- end }}的一个块,这样的话我们就可以在当前的块里面直接引用.python.k8s了,而不需要进行限定了,这是因为该with声明将.指向了.Values.course,在{{- end }}.就会复原其之前的作用范围了,我们可以使用模板引擎来渲染上面的模板查看是否符合预期结果。

不过需要注意的是在with声明的范围内,此时将无法从父范围访问到其他对象了,比如下面的模板渲染的时候将会报错,因为显然.Release根本就不在当前的.范围内,当然如果我们最后两行交换下位置就正常了,因为{{- end }}之后范围就被重置了:

1
2
3
4
5
{{ '{{' }}- with .Values.course }}
k8s: {{ '{{' }}.k8s | upper | quote }}
python: {{ '{{' }}.python | repeat 3 | quote }}
release: {{ '{{' }}.Release.Name }}
{{ '{{' }}- end }}

range 循环

如果大家对编程语言熟悉的话,几乎所有的编程语言都支持类似于forforeach或者类似功能的循环机制,在 Helm 模板语言中,是使用range关键字来进行循环操作。

我们在values.yaml文件中添加上一个课程列表:

1
2
3
4
5
6
7
8
course:
k8s: devops
python: django
courselist:
- k8s
- python
- search
- golang

现在我们有一个课程列表,修改 ConfigMap 模板文件来循环打印出该列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
myvalue: {{ '{{' }}.Values.hello | default "Hello World" | quote }}
{{ '{{' }}- with .Values.course }}
k8s: {{ '{{' }}.k8s | upper | quote }}
python: {{ '{{' }}.python | repeat 3 | quote }}
{{ '{{' }}- if eq .python "django" }}
web: true
{{ '{{' }}- end }}
{{ '{{' }}- end }}
courselist:
{{ '{{' }}- range .Values.courselist }}
- {{ '{{' }}. | title | quote }}
{{ '{{' }}- end }}

可以看到最下面我们使用了一个range函数,该函数将会遍历{{.Values.courselist }}列表,循环内部我们使用的是一个.,这是因为当前的作用域就在当前循环内,这个.从列表的第一个元素一直遍历到最后一个元素,然后在遍历过程中使用了titlequote这两个函数,前面这个函数是将字符串首字母变成大写,后面就是加上双引号变成字符串,所以按照上面这个模板被渲染过后的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '34626'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: dining-terrier-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjango"
web: true
courselist:
- "K8s"
- "Python"
- "Search"
- "Golang"

我们可以看到courselist按照我们的要求循环出来了。除了 list 或者 tuple,range 还可以用于遍历具有键和值的集合(如map 或 dict),这个就需要用到变量的概念了。

变量

前面我们已经学习了函数、管理以及控制流程的使用方法,我们知道编程语言中还有一个很重要的概念叫:变量,在 Helm 模板中,使用变量的场合不是特别多,但是在合适的时候使用变量可以很好的解决我们的问题。如下面的模板:

1
2
3
4
5
{{ '{{' }}- with .Values.course }}
k8s: {{ '{{' }}.k8s | upper | quote }}
python: {{ '{{' }}.python | repeat 3 | quote }}
release: {{ '{{' }}.Release.Name }}
{{ '{{' }}- end }}

我们在with语句块内添加了一个.Release.Name对象,但这个模板是错误的,编译的时候会失败,这是因为.Release.Name不在该with语句块限制的作用范围之内,我们可以将该对象赋值给一个变量可以来解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
{{ '{{' }}- $releaseName := .Release.Name -}}
{{ '{{' }}- with .Values.course }}
k8s: {{ '{{' }}.k8s | upper | quote }}
python: {{ '{{' }}.python | repeat 3 | quote }}
release: {{ '{{' }}$releaseName }}
{{ '{{' }}- end }}

我们可以看到我们在with语句上面增加了一句{{- $releaseName := .Release.Name -}},其中$releaseName就是后面的对象的一个引用变量,它的形式就是$name,赋值操作使用:=,这样with语句块内部的$releaseName变量仍然指向的是.Release.Name,同样,我们 DEBUG 下查看结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '45474'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nosy-seagull-configmap
data:
k8s: "DEVOPS"
python: "djangodjangodjango"
release: nosy-seagull

可以看到已经正常了,另外变量在range循环中也非常有用,我们可以在循环中用变量来同时捕获索引的值:

1
2
3
4
courselist:
{{ '{{' }}- range $index, $course := .Values.courselist }}
- {{ '{{' }}$index }}: {{ '{{' }}$course | title | quote }}
{{ '{{' }}- end }}

例如上面的这个列表,我们在range循环中使用$index$course两个变量来接收后面列表循环的索引和对应的值,最终可以得到如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '38876'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: vetoed-anaconda-configmap
data:
courselist:
- 0: "K8s"
- 1: "Python"
- 2: "Search"
- 3: "Golang"

我们可以看到 courselist 下面将索引和对应的值都打印出来了,实际上具有键和值的数据结构我们都可以使用range来循环获得二者的值,比如我们可以对.Values.course这个字典来进行循环:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
data:
{{ '{{' }}- range $key, $value := .Values.course }}
{{ '{{' }}$key }}: {{ '{{' }}$value | quote }}
{{ '{{' }}- end }}

直接使用range循环,用变量$key$value来接收字段.Values.course的键和值。这就是变量在 Helm 模板中的使用方法。

命名模板

前面我们学习了一些Helm模板中的一些常用使用方法,但是我们都是操作的一个模板文件,在实际的应用中,很多都是相对比较复杂的,往往会超过一个模板,如果有多个应用模板,我们应该如何进行处理呢?这就需要用到新的概念:命名模板

命名模板我们也可以称为子模板,是限定在一个文件内部的模板,然后给一个名称。在使用命名模板的时候有一个需要特别注意的是:模板名称是全局的,如果我们声明了两个相同名称的模板,最后加载的一个模板会覆盖掉另外的模板,由于子 chart 中的模板也是和顶层的模板一起编译的,所以在命名的时候一定要注意,不要重名了。

为了避免重名,有个通用的约定就是为每个定义的模板添加上 chart 名称:{{define "mychart.labels"}}define关键字就是用来声明命名模板的,加上 chart 名称就可以避免不同 chart 间的模板出现冲突的情况。

声明和使用命名模板

使用define关键字就可以允许我们在模板文件内部创建一个命名模板,它的语法格式如下:

1
2
3
{{ '{{' }}define "ChartName.TplName" }}
# 模板内容区域
{{ '{{' }}end }}

比如,现在我们可以定义一个模板来封装一个 label 标签:

1
2
3
4
5
{{ '{{' }}- define "mychart.labels" }}
labels:
from: helm
date: {{ '{{' }}now | htmlDate }}
{{ '{{' }}- end }}

然后我们可以将该模板嵌入到现有的 ConfigMap 中,然后使用template关键字在需要的地方包含进来即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{{ '{{' }}- define "mychart.labels" }}
labels:
from: helm
date: {{ '{{' }}now | htmlDate }}
{{ '{{' }}- end }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
{{ '{{' }}- template "mychart.labels" }}
data:
{{ '{{' }}- range $key, $value := .Values.course }}
{{ '{{' }}$key }}: {{ '{{' }}$value | quote }}
{{ '{{' }}- end }}

我们这个模板文件被渲染过后的结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '42058'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ardent-bunny-configmap
labels:
from: helm
date: 2018-09-22
data:
k8s: "devops"
python: "django"

我们可以看到define区域定义的命名模板被嵌入到了template所在的区域,但是如果我们将命名模板全都写入到一个模板文件中的话无疑也会增大模板的复杂性。

还记得我们在创建 chart 包的时候,templates 目录下面默认会生成一个_helpers.tpl文件吗?我们前面也提到过 templates 目录下面除了NOTES.txt文件和以下划线_开头命令的文件之外,都会被当做 kubernetes 的资源清单文件,而这个下划线开头的文件不会被当做资源清单外,还可以被其他 chart 模板中调用,这个就是 Helm 中的partials文件,所以其实我们完全就可以将命名模板定义在这些partials文件中,默认就是_helpers.tpl文件了。

现在我们将上面定义的命名模板移动到 templates/_helpers.tpl 文件中去:

1
2
3
4
5
6
{{ '{{' }}/* 生成基本的 labels 标签 */}}
{{ '{{' }}- define "mychart.labels" }}
labels:
from: helm
date: {{ '{{' }}now | htmlDate }}
{{ '{{' }}- end }}

一般情况下面,我们都会在命名模板头部加一个简单的文档块,用/**/包裹起来,用来描述我们这个命名模板的用途的。

现在我们讲命名模板从模板文件 templates/configmap.yaml 中移除,当然还是需要保留 template 来嵌入命名模板内容,名称还是之前的 mychart.lables,这是因为模板名称是全局的,所以我们可以能够直接获取到。我们再用 DEBUG 模式来调试下是否符合预期?

模板范围

上面我们定义的命名模板中,没有使用任何对象,只是使用了一个简单的函数,如果我们在里面来使用 chart 对象相关信息呢:

1
2
3
4
5
6
7
8
{{ '{{' }}/* 生成基本的 labels 标签 */}}
{{ '{{' }}- define "mychart.labels" }}
labels:
from: helm
date: {{ '{{' }}now | htmlDate }}
chart: {{ '{{' }}.Chart.Name }}
version: {{ '{{' }}.Chart.Version }}
{{ '{{' }}- end }}

如果这样的直接进行渲染测试的话,是不会得到我们的预期结果的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ $ helm install --dry-run --debug .
[debug] Created tunnel using local port: '42058'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: peeking-zorse-configmap
labels:
from: helm
date: 2018-09-22
chart:
version:
data:
k8s: "devops"
python: "django"

chart 的名称和版本都没有正确被渲染,这是因为他们不在我们定义的模板范围内,当命名模板被渲染时,它会接收由 template 调用时传入的作用域,有我们我们这里并没有传入对应的作用域,因此模板中我们无法调用到 .Chart 对象,要解决也非常简单,我们只需要在 template 后面加上作用域范围即可:

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
{{ '{{' }}- template "mychart.labels" . }}
data:
{{ '{{' }}- range $key, $value := .Values.course }}
{{ '{{' }}$key }}: {{ '{{' }}$value | quote }}
{{ '{{' }}- end }}

我们在 template 末尾传递了.,表示当前的最顶层的作用范围,如果我们想要在命名模板中使用.Values范围内的数据,当然也是可以的,现在我们再来渲染下我们的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '32768'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: oldfashioned-mule-configmap
labels:
from: helm
date: 2018-09-22
chart: mychart
version: 0.1.0
data:
k8s: "devops"
python: "django"

我们可以看到 chart 的名称和版本号都已经被正常渲染出来了。

include 函数

假如现在我们将上面的定义的 labels 单独提取出来放置到 _helpers.tpl 文件中:

1
2
3
4
5
6
7
{{ '{{' }}/* 生成基本的 labels 标签 */}}
{{ '{{' }}- define "mychart.labels" }}
from: helm
date: {{ '{{' }}now | htmlDate }}
chart: {{ '{{' }}.Chart.Name }}
version: {{ '{{' }}.Chart.Version }}
{{ '{{' }}- end }}

现在我们将该命名模板插入到 configmap 模板文件的 labels 部分和 data 部分:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
labels:
{{ '{{' }}- template "mychart.labels" . }}
data:
{{ '{{' }}- range $key, $value := .Values.course }}
{{ '{{' }}$key }}: {{ '{{' }}$value | quote }}
{{ '{{' }}- end }}
{{ '{{' }}- template "mychart.labels" . }}

然后同样的查看下渲染的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '42652'

......

Error: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 9: mapping values are not allowed in this context

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: altered-wombat-configmap
labels:
from: helm
date: 2018-09-22
chart: mychart
version: 0.1.0
data:
k8s: "devops"
python: "django"
from: helm
date: 2018-09-22
chart: mychart
version: 0.1.0

我们可以看到渲染结果是有问题的,不是一个正常的 YAML 文件格式,这是因为template只是表示一个嵌入动作而已,不是一个函数,所以原本命名模板中是怎样的格式就是怎样的格式被嵌入进来了,比如我们可以在命名模板中给内容区域都空了两个空格,再来查看下渲染的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mortal-cricket-configmap
labels:
from: helm
date: 2018-09-22
chart: mychart
version: 0.1.0
data:
k8s: "devops"
python: "django"
from: helm
date: 2018-09-22
chart: mychart
version: 0.1.0

我们可以看到 data 区域里面的内容是渲染正确的,但是上面 labels 区域是不正常的,因为命名模板里面的内容是属于 labels 标签的,是不符合我们的预期的,但是我们又不可能再去把命名模板里面的内容再增加两个空格,因为这样的话 data 里面的格式又不符合预期了。

为了解决这个问题,Helm 提供了另外一个方案来代替template,那就是使用include函数,在需要控制空格的地方使用indent管道函数来自己控制,比如上面的例子我们替换成include函数:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ '{{' }}.Release.Name }}-configmap
labels:
{{ '{{' }}- include "mychart.labels" . | indent 4 }}
data:
{{ '{{' }}- range $key, $value := .Values.course }}
{{ '{{' }}$key }}: {{ '{{' }}$value | quote }}
{{ '{{' }}- end }}
{{ '{{' }}- include "mychart.labels" . | indent 2 }}

在 labels 区域我们需要4个空格,所以在管道函数indent中,传入参数4就可以,而在 data 区域我们只需要2个空格,所以我们传入参数2即可以,现在我们来渲染下我们这个模板看看是否符合预期呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '38481'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: torpid-bobcat-configmap
labels:
from: helm
date: 2018-09-22
chart: mychart
version: 0.1.0
data:
k8s: "devops"
python: "django"
from: helm
date: 2018-09-22
chart: mychart
version: 0.1.0

可以看到是符合我们的预期,所以在 Helm 模板中我们使用 include 函数要比 template 更好,可以更好地处理 YAML 文件输出格式。


接下文: kubernetes包管理工具Helm之 三.Helm Hooks使用