简化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把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
| |-- configmap.yaml
| |-- deployment.yaml
| |-- _helpers.tpl
| |-- ingress.yaml
| |-- NOTES.txt
| `-- service.yaml
`-- values.yaml

2 directories, 8 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_template_guide/#built-in-objects

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模板,类似eqneltgtandor等操作符已经实现为函数,

控制流(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)
  • 空集合 (mapslicetupledictarray)

下面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,后面再引用drinkfood 时,无需再指定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的几点说明:

  1. subchart无需依赖父chart,其是一个完全独立操作的chart,拥有自己values和模板。
  2. subchart没权限访问父chart的values,但父chart可以覆盖subchart的values。
  3. 无论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,Kubernetes中文社区

K8S中文社区微信公众号