Helm是Kubernetes的包管理工具,如果把比作操作系统,那么Helm就好比yum,apt-get,homebrew。使用Helm template可以方便我们部署和管理自己的应用。本篇将基于Helm3.0+带你快速入门Helm template。
Prerequisite
若需要执行本篇示例,需要准备:
- 安装Helm (Helm 3.0+)
- 一个Kubernetes集群
Quick Start
在开始之前,我们简单理解一下Helm template的基本原理。其实也很简单,Helm使用gotemplate模板语言来编写代表Kubernetes资源(deployment,service, etc...)的模板文件,并提供让用户配置这些模板变量的能力。在部署时Helm通过模板引擎将模板渲染成真正的Kubernetes资源文件,并将它们部署到节点上。
使用以下命令创建模板:
$ helm create mychart
该命令会在当前目录下创建如下结构的文件夹:
mychart
├── Chart.yaml
├── charts
├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── service.yaml
│ ├── serviceaccount.yaml
│ └── tests
│ └── test-connection.yaml
└── values.yaml
templates/
目录下是模板文件,当Helm需要生成chart的时,会渲染该目录下的模板文件,将渲染结果发送给kubernetes。values.yaml
文件保存模板的默认值,用户可以在helm install
或者helm upgrade
可以指定新的值来覆盖默认值。Chart.yaml
文件保存chart的基本描述信息,这些描述信息也可以在模板中被引用。_helper.tpl
用于保存一些可以在该chart中复用的模板。
初始化模板为我们准备了deployment, ingress, service等常用的资源,在Quick Start中我们不需要这些资源,把/templates
文件夹下的文件都删掉,并创建mychart/templates/configmap.yaml
文件:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
该文件是一个表示ConfigMap
的模板文件,其中{{ .Release.Name }}
是gotemplate的模板语法,使用helm install
部署该模板:
# 其中clunky-serval是releaseNamed
$ helm install clunky-serval ./mychart
NAME: clunky-serval
LAST DEPLOYED: Tue Nov 1 17:45:37 2016
NAMESPACE: default
STATUS: DEPLOYED
REVISION: 1
TEST SUITE: None
部署成功后,使用helm get manifest clunky-serval
命令可以看到被部署的yaml文件:
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: clunky-serval-configmap
data:
myvalue: "Hello World"
可以看到,{{ .Release.Name }}
已经被替换成clunky-serval
了。
Go template基础
Helm template由go template编写,所以我们先需要掌握gotemplate的基础语法。gotemplate的模板指令由{{
和}}
包裹,例如Quick Start中的{{ .Release.Name }}
,模板渲染后会输出当前Release的名称。传递到模板中的值可以看作具有命名空间的对象,其中(.)
分隔了每个命名空间的元素。最左边的(.)
表示当前作用域下最顶层的命名空间。Release对象是Helm的内置对象之一,稍后我们将更深入地介绍内置对象。
模板方法和管道
除了模板变量以外,我们还可以使用模板方法,和管道(pipelines)来构建模板,这些都是go template提供的特性。例如使用quote
方法来为字符串添加双引号:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ quote .Values.favorite.drink }}
food: {{ quote .Values.favorite.food }}
gotemplate原生并没有提供太多方法,helm里面的许多方法来自这个库:github.com/Masterminds…
Helm 方法完整列表:helm.sh/docs/chart_…
管道是go template提供的一个强大的功能,其借鉴UNIX中的管道操作(|)的概念(cat foo.txt | grep bar)。管道是按顺序完成多项工作的有效方式。使用管道重写以上示例:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | quote }}
food: {{ .Values.favorite.food | quote }}
在这个例子中,我们使用管道操作符(|)代替了原有的函数调用方式。与UNIX管道一样,模板的管道也支持链式操作:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | quote }}
# 大写,然后双引号包裹
food: {{ .Values.favorite.food | upper | quote }}
在链式操作中,管道左侧的值会作为管道右侧函数中的最后一个参数,例如repeat
函数的函数签名为repeat COUNT STRING
,则可以这样使用管道传参:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
# repeat 5 .Values.favorite.drink
drink: {{ .Values.favorite.drink | repeat 5 | quote }}
food: {{ .Values.favorite.food | upper | quote }}
在编写Helm template时,建议优先使用管道来替代函数调用的方式。
控制流程
IF/ELSE
if/else判断语句的语法如下:
{{ if PIPELINE }}
# Do something
{{ else if OTHER PIPELINE }}
# Do something else
{{ else }}
# Default case
{{ end }}
当PIPELINE
值为以下内容,判定为false
:
- 布尔值false
- 数字零
- 一个空字符串
- nil
- 空的集合(map,数组)
下列模板将判断如果.Values.favorite.drink == "coffee"
则新增mug: true
。
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 }}
With
{{ with PIPELINE }}
# with声明的作用域
{{ end }}
with
用于更改当前作用域(.
)。上文提到在{{ .Release.Name }}
中,最左边的(.
)表示当前作用域下的顶层命名空间,.Values
告诉模板在当前作用域范围的顶层命名空间下查找Values
对象。使用with
可以改变模板变量的当前作用域,把(.
)赋值给另一个对象:
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 }}
在上面的例子中,在with
的作用范围内({- with .xxxx}
到 {{- end}}
之间)可以直接引用.drink
和.food
,这是因为{{- with .Values.favorite}}
把Values.favorite
赋值给了当前作用域(.
)。
range
range
用于循环遍历数组或是map。例如values.yaml
文件中有如下信息:
pizzaToppings:
- mushrooms
- cheese
- peppers
- onions
使用{{ range}}...{{ end}}
循环语句循环pizzaToppings
数组:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
toppings: |-
{{- range .Values.pizzaToppings }}
- {{ . | title | quote }}
{{- end }}
上述模板的渲染结果如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: RELEASE-NAME-configmap
data:
toppings: |-
- "Mushrooms"
- "Cheese"
- "Peppers"
- "Onions"
可以看到,range
遍历了.Values.pizzaToppings
数组,且将循环体内的作用域(.
)设置成了每一次循环的值。range
还可以同时获取迭代对象的key
,value
,例如有如下模板:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
toppings: |-
{{- range $index, $topping := .Values.pizzaToppings }}
{{ $index }}: {{ $topping }}
{{- end }}
其渲染结果如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: RELEASE-NAME-configmap
data:
toppings: |-
0: mushrooms
1: cheese
2: peppers
3: onions
空格处理
在上述的模板示例中,大家会注意到在一些模板指令里面会出现中划线-
,例如{{- xxx}}
或者{{xxx -}}
。这个中划线的作用就是消除多于空格。例如有如下的模板:
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 }}
其渲染结果是:
apiVersion: v1
kind: ConfigMap
metadata:
name: telling-chimp-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
mug: true
可以看到,在mug: true
前后各多了一个空行,这是因为在模板渲染的过程中,删除了{{
和}}
中的内容,但保留了其余的空白。使用-
来消除模板语句占用的空格,为直观展示删除效果,在下列示例中,星号(*)表示被删除的空格:
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 }}
渲染结果:
...
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
mug: true
...
中横线在左边({{- xxx}}
)表示消除左边的空格,中横线在右边({{xxx -}}
)表示消除右边的空格。要小心不要写成了:
...
food: {{ .Values.favorite.food | upper | quote }}
{{- if eq .Values.favorite.drink "coffee" -}}
mug: true
{{- end -}}
...
这样渲染出来的结果会是:food: "PIZZA"mug:true
。
除了使用-
消除模板的空格外,Helm还提供了indent
函数增加空格来进行缩进:
...
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
{{ "mug: true" | indent2 }} # 为"mug: true"增加两个空格的缩进。
...
内置对象
Helm的顶级作用域(.)
下可以直接引用的对象叫做内置对象,就好像上面示例中最常出现的.Values.xxx
,这个Values
就是Helm的内置对象之一。对象除了可以包含值以外,还可以包含方法。
文档参考: helm.sh/docs/chart_…
Release
表示当次Release
Release.Name
当次release名称Release.Namespace
发布到的命名空间(k8s namespace)Release.IsUpgrade
如果当前操作是upgrade或rollback,则设为trueRelease.IsInstall
如果当前操作是install,则为trueRelease.Revision
当前release版本,从1开始,每次upgrade或rollback递增。Release.Service
总是"Helm"
e.g:
{{ .Release.Name}}
Values
用于渲染模板的变量,如values.yaml
文件中的变量,以及用户部署时指定的变量。(-set, -f)。
Chart
Chart.yaml
文件文件中的变量,例: {{ .Chart.Name }}-{{ .Chart.Version}}
输出mychart-0.1.0
。
Files
提供文件访问方法
Files.Get
获取指定文件名称的文件内容Files.GetBytes
返回文件的二进制数组,读二进制文件时使用(例如图片)。Files.Glob
返回符合给定shell glob pattern的文件数组,例:{{ .Files.Glob "*.yaml" }}
Files.Lines
按行遍历读取文件Files.AsSecrets
返回文件内容的Base 64编码Files.AsConfig
返回文件内容对应的YAML map
Capabilities
提供Kubernetes集群相关信息
- Capabilities.APIVersions 集群支持的api version列表。
- Capabilities.APIVersions.Has $version 指出当前集群是否支持当前API版本 (e.g., batch/v1)或资源 (e.g., apps/v1/Deployment)
- Capabilities.KubeVersion / Capabilities.KubeVersion.Version Kubernetes版本号
- Capabilities.KubeVersion.Major Kubernetes主版本号
- Capabilities.KubeVersion.Minor Kubernetes子版本号
文件处理
结合Files
内置变量,我们可以在模板中嵌入文件内容,这其中最常见的用法就是将应用的配置文件作为ConfigMap
或者是Secret
随应用一起发布。在上面的内置变量章节中,介绍了Files
这个内置变量,聪明的你已经猜到其中Files.AsConfig
和Files.AsSecrets
就是用来把配置文件转换成ConfigMap
和Secret
的。这节我们来举例说明如何使用这两个函数。
假设我们有foo.yaml
和bar.yaml
两个配置文件(存放在values.yaml
同级目录下):
# foo.yaml
foo: true
fileName: foo.yaml
# bar.yaml
bar: true
fileName: bar.yaml
使用如下ConfigMap
模板:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
data:
{{(.Files.Glob "*.yaml").AsConfig | indent 2 }}
渲染结果:
apiVersion: v1
kind: ConfigMap
metadata:
name: RELEASE-NAME-config
data:
bar.yaml: |-
bar: true
fileName: bar.yaml
foo.yaml: |-
foo: true
fileName: foo.yaml
在以上例子中,我们使用.Files.Glob
函数来获取所有符合*.yaml
pattern的文件,然后调用AsConfig
方法输出以文件名称为key,文件内容为value的yaml
格式,注意使用indent
来增加合适的缩进以保证yaml
文件的语法正确性,在本例中使用ident 2
来缩进两个空格。
AsSecret
的用法是类似的:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
data:
{{(.Files.Glob "*.yaml").AsSecrets | indent 2 }}
渲染结果:
apiVersion: v1
kind: ConfigMap
metadata:
name: RELEASE-NAME-config
data:
bar.yaml: YmFyOiB0cnVlCmZpbGVOYW1lOiBiYXIueWFtbA==
foo.yaml: Zm9vOiB0cnVlCmZpbGVOYW1lOiBmb28ueWFtbA==
更多文件处理方法请参考文档:helm.sh/docs/chart_…
模板引用/嵌套
模板嵌套允许我们在一个模板中去引入另一个模板,我们可以将可以复用的模板单独抽离,在使用时再引入。例如我们希望所有发布的资源都包含一组相同的label,这时候我们不需要在每种资源(例如deployment,service,configmap ....)的模板中复制粘贴label的模板代码,而可以将渲染label的模板单独定义,然后在各资源模板中引用即可。下面我们通过一个示例来说明如何引用模板。
在示例之前,我们先要了解一下Helm的文件命名约定:
template/
中的大多数文件都被视为Kubernetes资源清单(会被发往Kubernetes创建对应资源)NOTES.txt
是个例外- 名称以下划线(_) 开头的文件不会被当做资源发往Kubernetes,但是可以被其他模板所引用。
这些下划线开头的文件用于定义局部模板。 实际上,当我们第一次创建mychart时,我们看到了一个名为_helpers.tpl的文件。 该文件是模板局部文件的默认位置。
helm create mychart
所创建的默认模板中,已经为我们提供了一套局部模板,下面就以默认模板为例,演示如何引用模板。
定义模板
使用{{ define}}...{{end}}
可以定义模板:
{{ define "MY.NAME" }}
# body of template here
{{ end }}
例如在_helpers.tpl
中,Helm定义了一些模板,以Kubernetes资源label为例:
...
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
{{ include "mychart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
...
使用{{ include 模板名称}}
引用模板,在deployment.yaml
中有:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
# include引用mychart.labels
labels:{{ include "mychart.labels" . | nindent 4 }}
要注意的是,在使用define
定义模板的时候,不要给模板加缩进,而是在使用include
引用的时候搭配nindent
或者是indent
来缩进。nindent
和indent
方法类似,不过nindent
会在最开始的地方增加一个空行,如果使用indent
,则上面的模板可以写成:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . | indent 2}}
labels:
{{ include "mychart.labels" . | indent 4 }}
附录:常用命令
helm create
helm create用于创建模板
# 创建一个名为mychart的template
$ helm create mychart
helm install
helm install用于部署chart
# 部署./redis目录,并设置release name为myredis
$ helm install myredis ./redis
# 通过-f指定变量文件,(覆盖默认的values.yaml)
$ helm install -f myvalues.yaml myredis ./redis
# -n 指定需要部署的命名空间
$ helm install myredis ./redis -n redis
# 使用--set 覆盖values.yaml中的默认值
$ helm install --set name=prod myredis ./redis
helm uninstall
根据release name卸载chart
# 卸载默认命名空间下的myredis
$ helm uninstall myredis
# 卸载redis命名空间下的myredis
$ helm uninstall myredis -n redis
helm list
列举release
# 列出默认命名空间下的所有release
$ helm list
# 列出redis命名空间下的release
$ helm list -n redis
# 列出所有命名空间下的release
$ helm list -A
--dry-run & --debug
使用--dry-run
和--debug
参数调试template
# 输出chart的渲染结果
helm install mychart --debug ./mychart
# 输出chart的渲染结果,并模拟部署(kubectl apply --dry-run ...)
helm install mychart --debug --dry-run./mychart