侧边栏壁纸
  • 累计撰写 53 篇文章
  • 累计创建 23 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

K8s Helm 2

zhanjie.me
2020-11-20 / 0 评论 / 0 点赞 / 1 阅读 / 0 字

2.1 Helm 模板简介

首先,可以对 Helm 模板有一个简单的认识。先创建一个 Chart 并添加一个简单的模板。

在前面介绍过 Helm Charts 的基本目录结构如下所示:

mychart/
  Chart.yaml  # Chart 的描述信息
  values.yaml # 设置模板文件的默认值,当 install 或是 upgrade 时这些值也可以被覆盖
  charts/     # 依赖的 Chart 文件
  templates/  # 存放模板文件,当创建一个 Chart 时,会将所有这些模板文件的内容发送给 kubernetes 集群
  ...

创建一个名为 mychart 的简单 Chart,然后在自定义模板:

$ helm create mychart
Creating mychart

在 templates 目录有两个比较特别的文件,这里说明如下:

NOTES.txt # 作用类型于“帮助文档”,当 install 安装的时候,这个文件中的内容会被展示出来
_helpers.tpl  # 作用类似于“模板助手”,在这里定义需要在 Chart 中反复使用到的模板或是结构

将 templates 目录下的所有文件都删除掉,后续自己重写这些文件:

$ rm -rf mychart/templates/*

创建一个 ConfigMap 模板,在 mychart/templates 目录下新建 configmap.yaml 文件并向其中写入如下内容:

apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-configmap
data:
  myvalue: "Hello World"

创建完这个简单的模板以后,就可以直接安装:

$ helm install full-coral ./mychart
NAME: full-coral
LAST DEPLOYED: Fri Nov 20 04:12:18 2020
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None

可以看到 ConfigMap 已经创建成功,现在来检索版本并查看加载的实际模板:

$ helm get manifest full-coral
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-configmap
data:
  myvalue: "Hello World"

现在可以使用命令 helm uninstall full-coral 删除这个 release 了。

$ helm uninstall full-coral
release "full-coral" uninstalled

接下来添加一个简单的模板调用。在上面的配置中, name 是硬编码的,这并不是一种好的方式,推荐的方式是采用模板来设置值,而不是每次都修改源文件。

Helm Chart 模板使用的是 Go 语言模板 编写,并添加了 sprig 库 中的 50 多个附件模板函数。

我们希望 Configmap 的名称是生成的 Release 的名称,现在将 configmap.yaml 文件的 name 配置修改为 name: {{ .Release.Name }}-configmap

模板指令是包含在 {{ 和 }} 内的。

Release 模板对象是 Helm 内置对象的一种,传递给模板的值是 namespace 对象,以 . 分隔每个 namespace 元素,Release 前面的小圆点 . 表示从顶层命名空间开始。所以综合起来关于 .Release.Name 的理解是:从顶层 namespace 开始,找到 Release 对象,然后在 Release 对象中查找 Name 对象。

执行安装:

$ helm install clunky-serval ./mychart
NAME: clunky-serval
LAST DEPLOYED: Fri Nov 20 04:20:41 2020
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None

查看配置信息:

$ helm get manifest clunky-serval
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: clunky-serval-configmap
data:
  myvalue: "Hello World"

想要测试模板渲染的时候可以使用参数 --debug --dry-run,这样会返回渲染的模板、同时不会立刻执行安装操作:

$ helm install --debug --dry-run goodly-guppy ./mychart
install.go:172: [debug] Original chart version: ""
install.go:189: [debug] CHART PATH: /root/learning/mychart

NAME: goodly-guppy
LAST DEPLOYED: Fri Nov 20 04:22:16 2020
NAMESPACE: kube-system
STATUS: pending-install
REVISION: 1
TEST SUITE: None
USER-SUPPLIED VALUES:
...
HOOKS:
MANIFEST:
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: goodly-guppy-configmap
data:
  myvalue: "Hello World"

2.2 内置对象与 Values 文件

内置对象

通过模板引擎可以将对象传递到模板中,使用代码也可以传递对象(比如使用 with、range 语句时),甚至有些方法可以直接在模板中创建新对象(比如 tuple 函数)。

对象通常都比较简单,只有一个值,或者包含其它对象/函数。比如 Release 对象包含了好多对象(像 Release.Name),而 Files 对象包含一些函数。

内置对象列表如下:

  • Release:这个对象描述了 Release 本身,它里面还包含好几个对象:
  • Release.Name:Release 的名称。
  • Release.Namespace:Release 所在的命名空间。
  • Release.IsUpgrade:如果当前操作是升级/回滚,它的值就设置为 true。
  • Release.IsInstall:如果当前操作是安装,它的值就设置为 true。
  • Release.Revision:Release 的修订版本号,它的值默认为 1,每当升级或回滚一次,值就增加 1。
  • Release.Service:Release 服务的名称,它的值始终是 Helm。* Values:从 values.yaml 文件或是用户提供的文件传递 Values 的值,默认情况下为空。
  • Chart:Chart.yaml 文件的内容,所有的 Chart 对象都会从这个文件中获取。比如在前面的例子中 {{ .Chart.Name }}-{{ .Chart.Version }}在模板中会被渲染为 mychart-0.1.0。
  • Files:提供对 Chart 中所有非特殊文件的访问,虽然不能使用这个对象来访问模板,但是可以使用它来访问 Chart 中的其它文件。
  • Files.Get:按名称获取文件的函数,比如 {{ .Files.Get config.ini }}。
  • Files.GetBytes:将文件内容作为 bytes 数组而不是 string 获取到函数,对于处理图片非常有用。
  • Files.Glob:这个函数可以返回匹配要求的文件列表。
  • Files.Lines:这个函数可以按行读取文件。
  • Files.AsSecrets:这个函数可以将文件内容经过 Base64 加密。
  • Files.AsConfig:这个函数可以将文件内容以 YAML 的形式返回。* Capabilities:提供关于 kubernetes 集群支持的信息。
  • Capabilities.APIVersions:一组版本信息。
  • Capabilities.APIVersions.Has $version:提示在集群中版本(比如 batch/v1)或是资源(比如 apps/v1/Deployment)是否可用。
  • Capabilities.KubeVersion 或是 Capabilities.KubeVersion.Version:kubernetes 的版本。
  • Capabilities.KubeVersion.Major:kubernetes 的主版本号。
  • Capabilities.KubeVersion.Minor:kubernetes 的次版本号。* Template:包含当前正在被执行的模板的信息。
  • Name:完整的当前模板的路径信息(比如 mychart/templates/mytemplate.yaml)。
  • BasePath:当前模板所在的目录信息(比如 mychart/templates)。

注意:内置对象的名称总是已大写字母开头。

Values 文件

在前面的内置对象中,我们提到过 Values 对象,这个对象提供对传入 Chart 的值的访问。Values 的值有 4 种来源:

  • chart 包中的 values.yaml 文件。
  • 父 chart 包中的 values.yaml 文件。
  • 在 helm install 或是 helm upgrade 时通过 -f 参数传入的 yaml 文件,比如 helm install -f myvals.yaml ./mychart。
  • 通过 --set 参数单独传递值,比如 helm install --set foo=bar ./mychart。

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

删除 mychart/values.yaml 文件中的内容然后向其中写入:

favoriteDrink: coffee

然后修改 mychart/templates/configmap.yaml 文件中的内容如下所示:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favoriteDrink }}  # 这里使用模板,从 Values 中获取 favoriteDrink 的值

渲染模板:

$ helm install --debug --dry-run test ./mychart
install.go:172: [debug] Original chart version: ""
install.go:189: [debug] CHART PATH: /root/learning/mychart

NAME: test
LAST DEPLOYED: Fri Nov 20 04:43:55 2020
NAMESPACE: kube-system
STATUS: pending-install
REVISION: 1
TEST SUITE: None
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
favoriteDrink: coffee

HOOKS:
MANIFEST:
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-configmap
data:
  myvalue: "Hello World"
  drink: coffee  # 这里使用模板,从 Values 中获取 favoriteDrink 的值

现在使用 --set 参数覆盖之前设置的值,将 coffee 修改为 slurm:

$ helm install  --debug --dry-run --set favoriteDrink=slurm test ./mychart
install.go:172: [debug] Original chart version: ""
install.go:189: [debug] CHART PATH: /root/learning/mychart

NAME: test
LAST DEPLOYED: Fri Nov 20 04:44:59 2020
NAMESPACE: kube-system
STATUS: pending-install
REVISION: 1
TEST SUITE: None
USER-SUPPLIED VALUES:
favoriteDrink: slurm

COMPUTED VALUES:
favoriteDrink: slurm

HOOKS:
MANIFEST:
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-configmap
data:
  myvalue: "Hello World"
  drink: slurm  # 这里使用模板,从 Values 中获取 favoriteDrink 的值

由于 --set 参数比默认的 values.yaml 文件具有更高的优先级,所以模板最终渲染出来的值为 drink: slurm。

Values 文件也可以包含更多具有结构化的内容,比如可以在 values.yaml 文件中创建 favorite 部分,然后再添加多个键:

favorite:
  drink: coffee
  food: pizza

对应的,模板部分也需要修改:

---
data:
  myvalue: "Hello World"
  drink: { { .Values.favorite.drink } }
  food: { { .Values.favorite.food } }

删除默认 Key

如果想要从默认值中删除某个键,可以覆盖这个键的值为 null,在这种情况下当 Helm 合并覆盖值时会直接删除这个键。

比如,stable 版本的 Drupal Chart 可以配置 liveness 探测器,它的默认值为:

---
livenessProbe:
  httpGet:
    path: /user/login
    port: http
  initialDelaySeconds: 120

如果想要覆盖 liveness 探测器的 httpGet 方式改为 exec 方式,可以使用命令:

# 需要设置 httpGet 的值为 null,然后再设置 exec 的值
helm install stable/drupal --set livenessProbe.httpGet=null --set livenessProbe.exec.command=[cat,docroot/CHANGELOG.txt]

最终覆盖完成的结果为:

---
livenessProbe:
  exec:
    command:
      - cat
      - docroot/CHANGELOG.txt
  initialDelaySeconds: 120

2.3 模板函数与管道

到目前为止,已经了解了如何把信息放入模板中。但是放入模板的这些信息都是未经修改的,有的时候我们想要转换这些信息,让它们对于我们来说更加有用。

模板函数

当想要从 .Values 中读取的值成为字符串的时候可以调用 quote 模板函数实现,修改 mychart/templates/configmap.yaml 文件中的内容如下所示:

---
data:
  myvalue: "Hello World"
  drink: { { quote .Values.favorite.drink } }
  food: { { quote .Values.favorite.food } }

模板函数的语法规则是 函数名 参数1 参数2...,在上面的例子中 quote .Values.favorite.drink 就是调用了 quote 函数并将后面的值作为参数传递给它。模板渲染后的结果如下所示:

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

Helm 是一种 Go 模板语言,拥有超过 60 种可用函数,其中一部分是由 Go 模板语言 本身定义的,其它大部分都是来自于 Spring 模板仓库。比如上面使用到的 quote 函数就是 Spring 模板库提供的一种字符串函数,主要作用就是用双引号将字符串括起来。

管道

模板语言的强大功能之一是它的管道概念,类似于 UNIX,管道是一个链在一起的一系列模板命令的工具,可以紧凑地表达一系列转换。换句话说,管道是按顺序完成一系列事情的一种方法,可以使用管道重写上面的例子:

---
data:
  myvalue: "Hello World"
  drink: { { .Values.favorite.drink | quote } }
  food: { { .Values.favorite.food | quote } }

其中,.Values.favorite.food | quote 表示使用管道 | 将参数发送给函数。进一步地,使用管道可以将多个功能链接到一起,比如:

---
data:
  myvalue: "Hello World"
  drink: { { .Values.favorite.drink | repeat 5 | quote } } # 先重复 5 次,再加上双引号
  food: { { .Values.favorite.food | upper | quote } } # 先大写,然后再加上双引号

模板渲染后的结果如下所示:

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

使用 default 函数

在模板中经常使用的一个函数是 default 函数:default 默认值 给的值。这个函数可以在模板中指定默认值,避免这个值被省略。比如修改上面 configmap.yaml 文件中 drink 的表达式为如下:

drink: { { .Values.favorite.drink | default "tea" | quote } }

如果正常运行,模板渲染后 drink 的值依然会是 coffee:

---
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"

现在修改 mychart/values.yaml 文件,将其中 drink 部分注释掉:

favorite:
  #drink: coffee
  food: pizza

在实际应用中,所有的静态默认值都应该写到 values.yaml 文件中,default 命令比较适合使用到计算值的时候设置默认值,因为计算值不能在 values.yaml 文件中声明。比如:

drink:
  {
    {
      .Values.favorite.drink | default (printf "%s-tea" (include "fullname" .)),
    },
  }

有的时候使用 if 条件判断会比 default 函数更加合适。

运算符函数

对于模板而言,所有的运算符(比如 eq、ne、lt、gt、and、or 等)都可以被实现为可以像函数一样的调用。在管道中,运算符可以用圆括号 () 进行分组。


2.4 流程控制

流程控制,使得我们可以控制模板的生成流程。Helm 模板语言提供了如下 3 种控制结构的语法:

  • if/else:用于创建条件块。
  • with:用于指定范围。
  • range:提供了一个类似“for each”风格的循环。

if/else 条件

if/else 主要用于在模板中有条件的包含文本。它的基本结构如下所示:

{{ if PIPELINE }}
  # Do something
{{ else if OTHER PIPELINE }}
  # Do something else
{{ else }}
  # Default case
{{ end }}

在进行条件判断的时候不仅仅只能使用 Value 值,还可以使用 PIPELINE 管道。条件判断需要根据结果为 true 还是 false 进行。当值为如下几种情况时,就表示为 false:

  • 布尔类型的 false。
  • 数字类型的 0。
  • 空的字符串。
  • 空或者 null。
  • 空的集合(map、slice、tuple、dict、array)。

在 configmap.yaml 文件中添加一个简单的条件判断:如果 drink 的值为 coffee,就在配置中添加 mug: true。在 configmap.yaml 文件最后一行添加条件判断:

...
data:
  myvalue: "Hello World"
  drink: {{.Values.favorite.drink | default "tea" | quote}}
  food: {{.Values.favorite.food | upper | quote}}
  {{ if and .Values.favorite.drink (eq .Values.favorite.drink "coffee") }}mug: true{{ end }}

然后将 values.yaml 文件中对于 drink 的注释取消掉,最后渲染模板输出如下所示:

---
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
  mug: true

空格控制

在前面写的条件判断是一整行,这样不方便查看,可以写为如下的方式,更加符合人类可读性:

...
data:
  myvalue: "Hello World"
  drink: {{.Values.favorite.drink | default "tea" | quote}}
  food: {{.Values.favorite.food | upper | quote}}
  {{- if and .Values.favorite.drink (eq .Values.favorite.drink "coffee") }}mug: true
  {{- end }} # 格式为 {{- xxxx }},在左边大括号中添加了破折号,以及破折号与值的中间需要有一个空格

渲染模板,正常输出:

---
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
  mug: true

使用 with 修改范围

with 可以控制变量的作用域。前面提到过,. 是对当前作用域的引用,.Values 表示在当前作用域中查找 Values 对象。

with 的语法结构如下所示:

{{ with PIPELINE }}
  # restricted scope
{{ end }}

作用域范围是可以改变的。with 语法可以将当前作用域范围(也就是 .)设置为一个特定的对象。比如我们可以工作在 .Values.favorites 这个小作用域范围内。修改 configmap.yaml 文件内容为如下所示:

...
data:
  myvalue: "Hello World"
  {{- with .Values.favorite }}
  drink: {{ .drink | default "tea" | quote }}  # 使用 with 限定作用范围为 .Values.favorite,那么 drink 和 food 变量就不需要再限定了
  food: {{ .food | upper | quote }}
  {{- end }} # 在这个之后,. 复位为其先前设置的范围

需要注意的是,在受限范围内,就不能越过限制范围直接访问父范围了。如果想要访问父范围,可以写在限定范围之外,如下所示:

...
data:
  myvalue: "Hello World"
  {{- with .Values.favorite }}
  drink: {{ .drink | default "tea" | quote }}
  food: {{ .food | upper | quote }}
  {{- end }}
  release: {{ .Release.Name }}

range 循环

range 可以遍历集合。

向 values.yaml 文件中添加一份披萨配料列表,完整的文件内容如下所示:

favorite:
  drink: coffee
  food: pizza
pizzaToppings:
  - mushrooms
  - cheese
  - peppers
  - onions

然后修改 configmap.yaml 文件,将这个列表打印出来:

...
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 }} 列表,所以在循环内部我们直接使用了一个 .(因为当前作用域就是这个循环),这个 . 会从列表的第一个元素一直遍历到最后一个元素,然后在遍历过程中使用了 title 和 quote 这两个函数,title 函数的作用是将字符串首字母变成大写,quote 函数的作用是给字符串加上双引号。

最后模板渲染的结果如下所示:

---
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
  toppings: |-
    - "Mushrooms"
    - "Cheese"
    - "Peppers"
    - "Onions"

除了列表,也可以直接在模板中遍历元组 tuple,比如:

sizes: |-
  {{- range tuple "small" "medium" "large"}}
  - {{.}}
  {{- end}}

模板渲染后的结果为:

sizes: |-
  - small
  - medium
  - large

range 也可以遍历具有键值对的集合,比如 map 或是 dict。


2.5 变量

在 Helm 中,使用变量的场合不是会特别多,但是如果使用的好,就可以帮助我们让代码更加简单易读。这里以 with 和 range 举例。

在前面 with 例子中,我们曾提到过如果想要访问父范围,需要写在限定范围之外,如下所示:

...
data:
  myvalue: "Hello World"
  {{- with .Values.favorite }}
  drink: {{ .drink | default "tea" | quote }}
  food: {{ .food | upper | quote }}
  {{- end }}
  release: {{ .Release.Name }}

如果想要把 release 写在 with 块内部应该怎么办呢?这个时候变量就可以用上场啦。可以将限定范围赋值给在不考虑当前范围的情况下能够直接访问的变量。

变量表示的是对另一个对象的命名引用,命名方式为 $name,赋值操作符为 :=。所以我们可以将 Release.Name 赋值给变量 relname,然后在 with 块中引用这个变量即可。

将 configmap.yaml 文件的内容修改为如下:

...
data:
  myvalue: "Hello World"
  {{- $relname := .Release.Name -}}  # 这个变量在整个模板中都起作用
  {{- with .Values.favorite }}
  drink: {{ .drink | default "tea" | quote }}
  food: {{ .food | upper | quote }}
  release: {{ $relname }}
  {{- end }}

渲染模板,结果如下所示:

---
metadata:
  name: test-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
  release: test

变量在 range 循环中也特别有用,可以用来同时捕获索引和值。

将 configmap.yaml 文件的内容修改为如下:

...
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }} # 在 range 循环中使用 $key 和 $val 两个变量分别接收列表循环的索引及其对应的值,$key 和 $val 只在 range 块中起作用,下放同理。
  {{ $key }}: {{ $val | quote }}
  {{- end }}
  toppings: |- # 这里的索引就是整数值(从 0 开始)
    {{- range $index, $topping := .Values.pizzaToppings }}
      {{ $index }}: {{ $topping | title | quote }}
    {{- end }}

模板渲染后的结果为:

---
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"
  toppings: |-
    0: "Mushrooms"
    1: "Cheese"
    2: "Peppers"
    3: "Onions"

变量既可以是全局变量,也可以是某个块内部的变量,这取决于在什么位置定义它。但是在 Helm 中,有一个全局变量 $(这个变量一直指向的都是根上下文)。比如下面这个例子:

{{- range .Values.tlsSecrets}} # 在这个 range 块中,作用域为 .Values.tlsSecrets
apiVersion: v1
kind: Secret
metadata:
  name: {{.name}}
  labels:
    app.kubernetes.io/name: {{template "fullname" $}}
    # 在 .Values.tlsSecrets 作用域中不能直接引用其它父作用域的值,即不能使用 .Chart.Name,但是可以使用 $.Chart.Name
    helm.sh/chart: "{{$.Chart.Name}}-{{ $.Chart.Version }}"
    app.kubernetes.io/instance: "{{$.Release.Name}}"
    app.kubernetes.io/managed-by: "{{$.Release.Service}}"
type: kubernetes.io/tls
data:
  tls.crt: {{.certificate}}
  tls.key: {{.key}}
---
{{- end}}

2.6 命名模板

有时在一个模板文件中,有些数据/结构会被反复使用,这时可以考虑在文件中定义多个命名模板,然后在需要的时候直接引用已经定义好的命名模板即可。

命名模板又被称为子模板,是限定在一个文件内部的模板,并且起一个名字。

对于声明和使用命名的模板,提供了如下 3 种语法:

  • define:在模板中声明一个新的命名的模板。
  • template:导入(使用)一个命名模板。
  • block:声明一个特殊的可填写的模板区域。

给模板起名需要注意:模板名称是全局的。如果有两个相同名称的模板,那么后加载的那个模板才会真正起作用。如何有效避免命名冲突呢?可以通过给每个模板都添加 Chart 名称,类似于 {{ define "mychart.labels" }}。

这里需要提一下关于 templates 目录下的文件命名,除了 NOTES.txt 文件和以下划线 _ 开头的文件,其它文件都会被当做资源清单文件。以下划线开头的文件可以被 Chart 其它模块调用,可以把公共模块放到该文件下,也就是默认的 _helpers.tpl 文件。

用 define 和 template 声明和使用模板

使用 define 可以在模板文件中创建一个命名模板,语法为:

{{ define "MY.NAME" }}
  # body of template here
{{ end }}

比如可以定义一个模板封装 kubernetes 标签块。将这个模板嵌套进现有的 ConfigMap 中,并使用 template 关键字引用这个模板:

# define 定义模板封装标签块,命名方式为 `Chart名称.模块名`,避免模板出现命名重复
{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  # template 引用前面定义好的模板
  {{- template "mychart.labels" }}
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}

当模板引擎读取这个文件时,它将会存储对于 mychart.labels 的引用直到使用 template 调用这个模板,然后它会在 template 所在行渲染这个模板。最终模板渲染的结果如下所示:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-configmap
  labels:
    generator: helm
    date: 2020-01-20
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"

当然,也可以把这些定义的模板放到 templates/_helpers.tpl 文件中,现在就将 mychart.labels 模板移入文件中:

{{/* Generate basic labels */}}  # 文档块,描述这个模板的作用
{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
{{- end }}

设置模板的范围

对于命名的模板,它在被 template 调用时需要传入一个作用域。

比如想要在 mychart.labels 模板中也打印出 Chart 的名称和版本号,正确的模板定义方法为如下所示:

{{/* Generate basic labels */}}
{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
    chart: {{ .Chart.Name }}      # 获取 Chart 的名称
    version: {{ .Chart.Version }} # 获取 Chart 的版本号
{{- end }}

然后在 configmap.yaml 文件中引用模板:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  # 在模板调用的末尾传入 .,表示当前最顶层作用范围
  {{- template "mychart.labels" . }}

执行渲染:

---
metadata:
  name: test-configmap
  labels:
    generator: helm
    date: 2020-01-20
    chart: mychart
    version: 0.1.0

include 函数

现在定义一个简单的模板,放到 _helpers.tpl 文件中,如下所示(纯键值对形式,没有格式):

{{- define "mychart.app" -}}
app_name: {{ .Chart.Name }}
app_version: "{{ .Chart.Version }}"
{{- end -}}

然后将 mychart.app 模板分别插入到 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" . }}

这样查看渲染模板其实会直接报错,因为会出现格式问题(主要是空格问题)。这时就需要使用 include 函数,它的主要作用是将模板的内容导入到当前管道中,然后在需要控制空格的地方使用 indent 管道函数进行控制。将上面的 ConfigMap 修改为如下所示:(在 labels 部分限制 4 个空格,在 data 部分限制 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 }}

现在渲染模板结果如下所示:

apiVersion: v1
kind: ConfigMap
metadata:
  name: test-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"

2.7 在模板中访问文件

在上一节中介绍了向一个模板导入另一个模板的方法。但是有的时候我们需要导入一个 File 文件而不是模板,也就是说不通过模板渲染器发送内容。

可以通过 .Files 对象访问文件,但是需要注意几点:

  • 可以向 Helm Chart 添加额外的文件,这些文件也会被绑定发送给 kubernetes。但是由于 kubernetes 的存储限制,Chart 必须小于 1M。
  • 出于安全考虑,有些文件不能通过 .Files 对象访问:
  • 在 templates/ 目录下的文件不能被访问。
  • 使用 .helmignore 排除的文件不能被访问。* Chart 并不保留 UNIX 模式信息,所以文件级别的权限不会影响 .Files 对象的访问。

基础例子

编写一个模板,读取三个文件的内容到 ConfigMap 中。直接在 mychart/ 目录下新建三个文件:

第一个文件 config1.toml:

message = Hello from config 1

第二个文件 config2.toml:

message = This is config 2

第三个文件 config3.toml:

message = Goodbye from config 3

使用 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 . }} 表示打印出对应文件名的内容。

渲染这个模板,结果如下所示:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-configmap
data:
  config1.toml: |-
    message = Hello from config 1

  config2.toml: |-
    message = This is config 2

  config3.toml: |-
    message = Goodbye from config 3

路径助手

在处理文件的时候,有时会需要对文件路径本身执行一些标准操作。Helm 从 Go 的 path 包导入了很多函数加以使用,它们都可以使用 Go 包中的相同名称访问,但是使用时需要小写第一个字母,比如 Base 变成 base。

导入的函数有:Base、Dir、Ext、IsAbs、Clean。

Glob 模式

Files.Glob(pattern string) 可以批量化的提取更多符合指定 pattern 的文件。

.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 }}

如果在 ConfigMaps 和 Secrets 中使用这些文件内容,可以 AsConfig 和 AsSecrets 方法,如下所示:

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 }}

如果想要对一个文件内容进行 Base64 编码,可以读取到文件内容后再传递给 b64enc 进行处理,比如:

---
data:
  token: |-
    {{ .Files.Get "config1.toml" | b64enc }}

如果想要访问模板文件的每一行,可以使用 Lines 方法,比如:

data:
  some-file.txt: {{ range .Files.Lines "foo/bar.txt" }}
    {{ . }}{{ end }}

但是对于 install 安装过程中提供的外部配置,依然只能使用 helm install -f 或是 helm install --set 进行加载。


2.8 忽略不需要的文件和文件夹

.helmignore 文件可以明确指定不想要打包(helm package)进 Chart 的文件。

其中的语法支持 Unix shell 全局匹配、相对路径匹配以及使用 ! 表示否定,每行只能填写一种模式。比如:

# comment
.git
*/temp*
*/*/temp*
temp?

2.9 notes 文件

创建一个 templates/NOTES.txt 文件,可以在 install 安装或是 upgrade 更新完成时,打印出一些有用的信息。虽然 NOTES.txt 文件是纯文本文件,但是其中依然可以写模板语言。

比如创建一个简单的 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 }}

然后执行安装 helm install test ./mychart,在底部会看到如下信息:

...
Thank you for installing mychart.

Your release is named test.

To learn more about the release, try:

  $ helm status test
  $ helm get test

2.10 子 chart 和全局值

在前面的内容中,我们都只使用了一个 Chart,但是 Chart 是可以有依赖关系的,称为子 Chart(subcharts)。这些子 Chart 有它们自己的值和模板。

对于 subcharts 而言:

  • 子 Chart 应该是相对独立的,它不能依赖父 Chart。
  • 子 Chart 不能访问父 Chart 的值。
  • 父 Chart 可以覆盖子 Chart 的值。
  • Helm 可以有全局值,这个值可以被所有 Chart 访问。

创建一个子 Chart

在 mychart/ 目录下新创建一个子 Chart:

$ cd mychart/charts
$ helm create mysubchart
Creating mysubchart
$ rm -rf mysubchart/templates/*

在子 Chart 中添加 Values 和模板

修改 mychart/charts/mysubchart/values.yaml 文件如下所示:

dessert: cake

新建 mychart/charts/mysubchart/templates/configmap.yaml 文件并写入如下内容:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-cfgmap2
data:
  dessert: {{ .Values.dessert }}

渲染子 Chart 进行测试:

$ helm install --debug --dry-run test mychart/charts/mysubchart
...
# Source: mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cfgmap2
data:
  dessert: cake

覆盖子 Chart 的值

现在的层级关系是:mychart 是 mysubchart 的父 Chart,因为 mysubchart 在 mychart/charts 目录中。

由于 mychart 是父级,我们可以在 mychart 中配置覆盖 mysubchart 中的值。

在 mychart/values.yaml 文件的末尾添加如下所示的内容:

---
mysubchart: # 在 mysubchart 块中配置的值会直接发送给 mysubchart 包
  dessert: ice cream

如果执行安装 helm install --dry-run --debug test mychart,可以看到 mysubchart 的 ConfigMap 为:

---
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cfgmap2
data:
  dessert: ice cream

这就表示在父 Chart 中配置的值已经生效。

全局 Chart 值

有时我们希望一些值在所有模板中都可以使用,这时就需要使用全局 Chart 值了。全局值是可以从 Chart 或子 Chart 中使用相同名称进行访问的值,使用 Values.global 就可以设置。

在 mychart/values.yaml 文件的末尾添加如下所示的内容:

---
global:
  salad: caesar

这样在任意的模板中都可以通过 {{.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 }}

运行 install,输出如下:

...
---
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cfgmap2
data:
  dessert: cake
  salad: caesar
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-configmap
data:
  salad: caesar
0

评论区