简化Kubernetes应用部署工具-Helm之Release配置


【编者的话】微服务和容器化给复杂应用部署与管理带来了极大的挑战。Helm是目前Kubernetes服务编排领域的唯一开源子项目,做为Kubernetes应用的一个包管理工具,可理解为Kubernetes的apt-get / yum,由Deis 公司发起,该公司已经被微软收购。Helm通过软件打包的形式,支持发布的版本管理和控制,很大程度上简化了Kubernetes应用部署和管理的复杂性。

随着业务容器化与向微服务架构转变,通过分解巨大的单体应用为多个服务的方式,分解了单体应用的复杂性,使每个微服务都可以独立部署和扩展,实现了敏捷开发和快速迭代和部署。但任何事情都有两面性,虽然微服务给我们带来了很多便利,但由于应用被拆分成多个组件,导致服务数量大幅增加,对于Kubernetest编排来说,每个组件有自己的资源文件,并且可以独立的部署与伸缩,这给采用Kubernetes做应用编排带来了诸多挑战:
  1. 管理、编辑与更新大量的K8s配置文件
  2. 部署一个含有大量配置文件的复杂K8s应用
  3. 分享和复用K8s配置和应用
  4. 参数化配置模板支持多个环境
  5. 管理应用的发布:回滚、diff和查看发布历史
  6. 控制一个部署周期中的某一些环节
  7. 发布后的验证


而采用Helm,可以轻松的解决上面的问题。

Helm把Kubernetes资源(比如deployments、services或 ingress等) 打包到一个chart中,而chart被保存到chart仓库。通过chart仓库可用来存储和分享chart。Helm使发布可配置,支持发布应用配置的版本管理,简化了Kubernetes部署应用的版本控制、打包、发布、删除、更新等操作。

本文的目标是展示Helm如何做应用配置管理以及服务依赖的处理。

关于Helm安装、简介以及使用请参考:
简化Kubernetes应用部署工具-Helm安装
简化Kubernetes应用部署工具-Helm简介
简化Kubernetes应用部署工具-Helm之应用部署

配置发布

前面简化Kubernetes应用部署工具-Helm之应用部署文章中展示了一次Helm发布的生命周期,包含了chart创建、更新、回滚、删除等。但这并不是我们使用Helm的唯一原因,我们还需要一个管理配置发布的工具。

Helm Chart模板采用go模板语言编写 Go template language,模板的值记录在values.yaml文件中。values.yaml文件中的值可以在安装chart过程中,通过参数–values YAML_FILE_PATH或–set key1=value1, key2=value2替换。

注意:模板语言中的引用数值型需要注意,不加引号,会被解释为数值。

创建自定义chart
$ helm create mychart

查看mychart结构:
|-- charts
|-- Chart.yaml
|-- templates
| |-- deployment.yaml
| |-- _helpers.tpl
| |-- ingress.yaml
| |-- NOTES.txt
| `-- service.yaml
`-- values.yaml

2 directories, 7 files

生成chart目录里有Chart.yaml, values.yaml and NOTES.txt等文件,下面分别对chart中几个重要文件解释:
  • Chart.yaml 包含了chart的metadata,描述了Chart名称、描述信息与版本。
  • values.yaml:存储了模板文件变量。
  • templates/:记录了全部模板文件。
  • charts/:依赖chart存储路径。


其中mychart/templates/的文件及其作用如下:
  • NOTES.txt:给出了部署chart后的帮助文档,例如如何使用chart、列出默认的设置等。
  • deployment.yaml:创建 Kubernetes deployment的yaml文件。
  • service.yaml:创建deployment的service endpoint yams文件。
  • _helpers.tpl: 模板使用帮助文件。


创建mychart/templates/configmap.yaml文件,内容如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"

一个模板指令由{{}}标记。{{ .Release.Name }}会把release name插入模板。传入模板的值可被认作是namespaces对象,通过”.”来分隔namespaces元素。

内置对象

对象通过模板引擎传给模板,有几种方式在模板内创建新的对象,比如后面要讲到的tuple。
模板内置对象请参见:https://docs.helm.sh/chart_tem ... jects

Values文件

上面章节提到Helm模板提供了内置的对象,而Values做为4个内置对象之一,有4种来源:
  • 可通过chart中的values.yaml文件
  • 对于subchart,父chart中values.yaml文件
  • helm install 或 helm update 指定 -f flag (helm install -f myvals.yaml ./mychart)
  • 通过--set 参数传值(例如 helm install --set foo=bar ./mychart)


说明:values.yaml默认会被引用,但其会被父chart中的values.yaml文件内容覆盖(overridden),而父chart中的values.yaml文件会被户用-f指定的文件内容覆盖,而用户用-f指定的文件会被–set参数传的值覆盖。简单的说,也就是上面4种Values来源的重要性由上到下的顺序,优先级由低到高。

删除默认键值

如需要删除键值对中删除默认key,需要将key的值设置为null, helm也会将从覆盖的键值对中将其剔除。举例说明,期望将下面例子中的livenessProbe替换为 exec
livenessProbe:
httpGet:
path: /user/login
port: http
initialDelaySeconds: 120

在helm install 时,使用–set livenessProbe.exec.command=[cat,docroot/CHANGELOG.txt],结果如下:
livenessProbe:
httpGet:
path: /user/login
port: http
exec:
command:
- cat
- docroot/CHANGELOG.txt
initialDelaySeconds: 120

这并非我们的预期,而通过将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

模板功能和Pipeline

前面展示的模板例子中,模板内容基本不去修改,但有一些情况下,我们希望提供给模板的数据对我们更加简单易用。例如对于 .Values对象注入模板的键值可利用模板提供的 quote功能引用。
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ quote .Values.favorite.drink }}
food: {{ quote .Values.favorite.food }}


Helm 提供了超过60功能函数,其中一些定义在Go模板语言自身: Go template language . 这些函数是 Sprig template library的一部分。

PIPELINES

Pipelines是模板语言的非常强大的特性之一,与Linux/Unix的pipeline类似,pipelines是把模板语言的多个命令通过 (|)的分隔形式,以描述的顺序的实现命令聚合功能。
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 }}


上面例子中,对.Values.favorite.drink | repeat 5 | quote的值coffee重复了5次后引用,而对.Values.favorite.food先做了一次大写转换然后加以引用。类似结果如下:
data:
myvalue: "Hello World"
drink: "coffeecoffeecoffeecoffeecoffee"
food: "PIZZA"

使用 DEFAULT功能

default是模板中经常使用一个功能,该功能是在模板中指定一个默认值。例如下面例子中,如果.Values.favorite.drink没有被赋值,那么模板被引用时,tea便为其默认值。
drink: {{ .Values.favorite.drink | default "tea" | quote }}


操作符和函数

对于Helm模板,类似eq, ne, lt, gt, and, or等操作符已经实现为函数,

控制流(Flow Control)

Helm模板语言提供了如下控制结构:
  • if/else 条件语句
  • with 指定范围
  • range, 提供了“for each”-形式的循环


除此之外,Helm还提供了一些命名模板的行为:
  • define 模板内部定义一个命名模板
  • template 导入一个命名模板
  • block declares a special kind of fillable template area


IF/ELSE

条件基本的控制结构如下:
{{ if PIPELINE }}
# Do something
{{ else if OTHER PIPELINE }}
# Do something else
{{ else }}
# Default case
{{ end }}


pipeline会被认为是false 如果值是下面几种类型:
  • 布尔类型 false
  • 数字 0
  • 空串
  • nil (empty or null)
  • 空集合 (map, slice, tuple, dict, array)


下面configmap例子中,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 }}


执行helm install --dry-run --debug mychart命令:
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
mug: true

空格敏感

Helm模板语言对空格非常敏感,左侧有{{-(破折号后面有一个空格)表明空格需要被移除,而右侧 -}}表示空格会被消费。
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}}


如果不加-,举例如下:
{{if eq .Values.favorite.drink "coffee"}}
mug: true
{{end}}


那么输出时会移除 {{ 与 }}而留下空格:
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"

mug: true

或者:
food: {{ .Values.favorite.food | upper | quote }}
{{- if eq .Values.favorite.drink "coffee" -}}
mug: true
{{- end -}}


结果如下,因为-}}已经把其所在的行移除了。

food: "PIZZA"mug:true

使用 with限制引用范围

通过 with 与(.) 指定当前范围到某一个指定的对象,例如下面例子中 .Values.favorites,后面再引用drink与food 时,无需再指定Values.favorite
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 }}


RANGE循环

像多数编程语言支持 for 与 foreach 等循环一样,Helm模板语言通过支持range操作符遍历一个集合。

下面例子中 values.yaml 文件部分内容如下:
favorite:
drink: coffee
food: pizza
pizzaToppings:
- mushrooms
- cheese
- peppers
- onions

在ConfigMap中需要遍历Values.pizzaToppings的全部值,可以采用下面形式:
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 }}


其中 |-的作用是生成一个占用多行的字符串。

另外Helm提供了 tuple函数,可以在模板内建立一个list并且遍历它。例如:
sizes: |-
{{- range tuple "small" "medium" "large" }}
- {{ . }}
{{- end }}


结果如下:
sizes: |-
- small
- medium
- large

模板变量

模板变量在 range的循环中非常有用,变量的定义格式为$name ,可通过 :=赋值。举例如下:
toppings: |-
{{- range $index, $topping := .Values.pizzaToppings }}
  {{ $index }}: {{ $topping }}
{{- end }}


每循环一次 $index的值从0开始递增,并且将遍历的值赋给$topping
toppings: |-
  0: mushrooms
  1: cheese
  2: peppers
  3: onions

或者,对应存在键值对的对象,可以用 range获取:
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
{{- end}}


结果如下:
data:
myvalue: "Hello World"
drink: "coffee"
food: "pizza"

命名模板

Helm允许我们创建一个命名模板,通过模板名称,可以在任何地方引用它。一般情况下命名模板会写在_helpers.tpl 中,并以形式 ({{/* ... */}})加上模板说明。比如下面例子中定义了一个my_labels命名模板,并且用 include通过 {{- include "my_labels" }}嵌入到configmap的metadata中。通常 include也可以用 template代替,但是 include在处理YAML文件的格式输出上会更胜一筹,另外 include可以包含一个变量的模板{{ include $mytemplate }}。
{{/* Generate basic labels */}}
{{- define "my_labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
{{- end }}


在configmap.yaml文件中引用该命名模板:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
{{- include "my_labels" . }}
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
{{- end }}


需要注意的是,命名模板名称是全局的,如果存在多个同名的模板,最后被load到Tiller中的模板会被最终使用。另外模板的命名通常以chart的名字做为前缀,比如 {{ define "mychart.labels" }} 或 {{ define "mychart_labels" }}。

设置模板使用范围

前面的命名模板中,并没有使用模板的内置对象。下面例子中在命名模板中使用了内置对象:
{{/* Generate basic labels */}}
{{- define "my_labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
chart: {{ .Chart.Name }}
version: {{ .Chart.Version }}
{{- end }}


在configmap中消费这个命名模板,注意在命名模板调用的末尾加上 ., indent 2表示引用的模板内容向右缩进2格。
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 }}


模板访问文件

上个章节中展示了几种创建和访问命名模板的方式,通过命名模板,可以很容易在一个模板中引用另外一个模板。但有的时候,我们还有导入一个普通文件内容来渲染模板的需求,而不仅仅是以模板的形式。Helm提供了一个内置 .Files对象。

Helm支持chart中存在一个普通类型的文件,这些文件也会被与其他模板文件一同打包发给给Tiller,但文件大小目前被限制在1M以内。由于安全原因,一般情况下 templates/的.Files对象不可访问,另外,chart并不会保留 UNIX mode的信息,文件层面的权限并不会影响对.Files对象的访问控制。

NOTES.txt文件

在 chart install 或 chart upgrade的尾部,会打印出用户帮助信息,该信息在 templates/NOTES.txt中定制。Helm并不强制提供用户帮助信息,但为了用户更加方便了解和使用所安装的应用,强烈建议在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 }}


SubChart和全局变量

所谓subchart,指的是一个chart依赖其他chart,被依赖的chart即被称为subchart。

关于subchart的几点说明:
  • subchart无需依赖父chart,其是一个完全独立操作的chart,拥有自己values和模板。
  • subchart没权限访问父chart的values,但父chart可以覆盖subchart的values。
  • 无论subchart或父chart都可以访问helm全局values。


操作subchart

创建subchart

创建subchart的过程与普通chart基本一致,唯一需要注意的是,subchart需要创建在父chart的charts文件夹内,举例如下:
$ cd mychart/charts
$ helm create mysubchart
Creating mysubchart

为sub chart添加模板和values:

添加 values.yaml 到 mychart/charts/mysubchart :
dessert: cake

在 mychart/charts/mysubchart/templates/configmap.yaml 中创建ConfigMap模板:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-cfgmap2
data:
dessert: {{ .Values.dessert }}


subchart可以单独安装:
$ helm install --dry-run --debug mychart/charts/mysubchart

父chart的values可以覆盖子chart,在mychart/values.yaml添加:
mysubchart:
dessert: ice cream

然后执行 helm install --dry-run --debug mychart命令:
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: unhinged-bee-cfgmap2
data:
dessert: ice cream

从结果可知,子chart的dessert的已由cake被替换为ice cream。

全局chart values

全部chart,包括子chart都可以访问全局chart values。一般全局chart values记录在父chart Values.global中。以mychart/values.yaml为例:
mysubchart:
dessert: ice cream

global:
salad: caesar

那么在mysubchart/templates/configmap.yaml 中,即可以通过 .Values.global.salad 访问:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-cfgmap2
data:
dessert: {{ .Values.dessert }}
salad: {{ .Values.global.salad }}


父子chart共享模板

Helm中父子chart之间可以共享模板,在任何一个chart中定义的块,对其他chart均可访问。

调试模板

模板在Tiller server端渲染,而非Helm client端。经过Tiller server渲染的模板,最后发送给Kubernetes API server,而YAML文件除了格式问题外,可能还有其他多种因为会被Kubernetes API server拒绝。
  • helm lint 验证chart是否遵循了一些好的实践
  • helm install --dry-run --debug: chart发送给Tiller server并用value.yaml中的值来渲染相关模板。但实际上chart并没有安装, 只是输出了渲染后的模板。
  • helm get manifest: 查看Tiller server端已安装的模板。


欢迎转载,请注明作者出处:张夏,FreeWheel Lead Engineer,DockOne社区

0 个评论

要回复文章请先登录注册