Skip to content

变量

作者: ryan 发布于: 2025/8/13 更新于: 2025/8/13 字数: 0 字 阅读: 0 分钟

有了函数、管道、对象以及控制结构,我们可以想象下大多数编程语言中更基本的思想之一:变量。

在模板中,变量的使用频率较低,但是,我们还是可以使用他们来简化代码,以及更好地使用 withrange

在前面的示例中,我们知道下面的模板渲染会出错:

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

因为 Release.Name 不在 with 语句块限制的范围之内,解决作用域问题的一种方法是将对象分配给在不考虑当前作用域情况下访问的变量。

变量定义

在 Helm 模板中,变量是对另外一个对象的命名引用。它遵循 $name 格式,变量使用特殊的赋值运算符进行赋值 :=,我们可以修改上面的模板,为 Release.Name 声明一个变量:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  {{- $relname := .Release.Name -}}
  {{- with .Values.favorite }}
  drink: {{ .drink | default "tea" | quote }}
  food: {{ .food | upper | quote }}
  release: {{ $relname }}
  {{- end }}

注意在 with 语句之前,我们先分配了 $relname := .Release.Name,然后在 with 语句块中,$relname 变量仍然表示 release 的名称,我们渲染该模板,可以得到如下的正确结果:

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

变量在 range 循环里面非常有用,它们可以用于类似于列表的对象来捕获索引和 value 值:

yaml
toppings: |-
  {{- range $index, $topping := .Values.pizzaToppings }}
    {{ $index }}: {{ $topping }}
  {{- end }}
yaml
pizzaToppings:
  - mushrooms
  - cheese
  - peppers
  - onions

使用 Go 模板中的 range 语法,我们可以遍历这个列表并输出每个元素的索引和值。下面是完整的示例模板代码及其渲染后的输出:

注意 range 在前面,然后是变量,然后是赋值运算符,然后才是列表,这会将整数索引(从0开始)分配给 $index,并将 value 值分配给 $topping,上面的内容会被渲染成如下内容:

yaml
toppings: |-
  0: mushrooms
  1: cheese
  2: peppers
  3: onions

对于同时具有 key 和 value 的数据结构,我们也可以使用 range 来获得 key、value 的值,比如,我们可以像这样循环遍历 .Values.favorite

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}

在第一次迭代中,$key 是 drink,$val 是 coffee,在第二次迭代中,$key 是 food,$val 是 pizza。运行上面的命令将生成下面的内容:

yaml
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575983119-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"
一般来说变量不是全局的,它们的作用域是声明它们的块区域,之前,我们在模板的顶层分配了 `$relname`,该变量将在整个模板的范围内,但是在我们上面的示例中,`$key` 和 `$val `作用域只在 {{ range }} ... {{ end }} 区域内。

全局变量

但是,有一个始终是全局变量的 $始终指向顶层根上下文,当我们在 range 循环内需要知道 chart 包的 release 名称的时候,该功能就非常有用了,比如下面的模板文件:

yaml
{{- range .Values.tlsSecrets }} #遍历 tlsSecrets 列表,每个元素在循环内可以通过 . 访问。
apiVersion: v1
kind: Secret
metadata:
  name: {{ .name }} #访问当前 tlsSecrets 元素的 name 属性
  labels:
    # helm 模板经常使用 `.`,但是这里是无效的,用 `$` 是可以生效的。
    app.kubernetes.io/name: {{ template "fullname" $ }}
    # 这里不能引用 `.Chart.Name`,但是可用使用 `$.Chart.Name`
    helm.sh/chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" #使用 $.Chart.Name 和 $.Chart.Version 来访问全局上下文中的 Chart 名称和版本。
    app.kubernetes.io/instance: "{{ $.Release.Name }}"  #这个$表示的是最外层的一个作用域
    # 值来自于 Chart.yaml 文件中的 appVersion
    app.kubernetes.io/version: "{{ $.Chart.AppVersion }}" #访问全局上下文中的 Chart 应用版本
    app.kubernetes.io/managed-by: "{{ $.Release.Service }}" #访问全局上下文中的 Release 服务
type: kubernetes.io/tls
data:
  tls.crt: {{ .certificate }}
  tls.key: {{ .key }}
---
{{- end }}

到现在为止,我们只研究了在一个文件中声明的一个模板,但是,Helm 模板语言的强大功能之一是它能够声明多个模板并将其一起使用。

我们将在下面的章节中来讨论这一点。

命名模板

前面我们都是只操作的一个模板,现在我们来尝试使用多个模板文件。

在本节中,我们可以了解到如何在一个文件中定义命名模板,然后在其他地方使用它们。

命名模板(有时也叫子模板)只是在文件内部定义的有名称的模板。

主要有两种创建方式以及几种不同的使用方式。

当使用命名模板的时候有几个重要细节:模板名称是全局的,如果声明两个具有相同名称的模板,则会使用最后被加载的模板。

由于子 chart 中的模板是与顶级模板一起编译的,所以需要谨慎命名。

yaml

一种流行的命名约定是在每个定义的模板前添加 chart 名称:{{ define "mychart.labels" }},通过使用特定的 chart 名作为前缀,我们可以避免由于两个不同的 chart 实现了相同名称的模板而引起的冲突。

例如:在 _helpers.tpl 文件中定义命名模板:

yaml
{{- define "mychart.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}

{{- define "mychart.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name -}}
{{- end -}}

partials 和 _ 文件

到目前为止,我们只使用了一个模板文件,但是 Helm 的模板语言允许我们创建命名的嵌入式模板,可以在其他位置进行访问。

Partials 是指可以在其他模板中包含的部分模板。它们通常定义在 _helpers.tpl文件中

在编写这些模板之前,有一些值得一提的命名约定:

  • templates/ 中的大多数文件都被视为 Kubernetes 资源清单文件(NOTES.txt 除外)
  • _ 开头命名的文件也不会被当做 Kubernetes 资源清单文件
  • 下划线开头的文件不会被当做资源清单之外,还可以被其他 chart 模板调用

_ 开头的这些文件其实就是 Helm 中的 partials 文件,所以其实我们完全可以将命名模板定义在这些 partials 文件中,默认就是_helpers.tpl文件,其实在前面我们创建的 mychart 包中也可以找到这个文件。

define 和 template

define 关键字可以让我们在模板文件中创建命名模板,它的语法如下所示:

yaml
{{ define "MY.NAME" }}
  # 模板内容区域
{{ end }}

比如我们可以定义一个模板来封装下 Kubernetes 的 labels 标签:

yaml
{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
{{- end }}

现在我们可以将该模板嵌入到前面的 ConfigMap 模板中,然后将其包含在模板中:

yaml
{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  {{- template "mychart.labels" }}
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}

当模板引擎读取这个文件的时候,它会存储 mychart.labels 的引用,直到该模板被调用,然后会内联渲染该模板。

我们渲染这个模板可以都到如下所示的结果(记得先删掉默认生成的 _helpers.tpl 文件):

yaml
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576034036-configmap
  labels:
    generator: helm
    date: 2019-12-11
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"

一般来说,Helm 中约定将这些模板统一放到一个 partials 文件中,通常就是 _helpers.tpl 文件中,我们将上面的命名模板移动到该文件(templates/_helpers.tpl)中去:

yaml
{{/* 生成基本的 Label 标签 */}}
{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
{{- end }}
一般来说,我们也会用一个简单的块({{/*...*/}})来注释这个命名模板的作用。

现在虽然我们把命名模板放到了 _helpers.tpl 文件中,但是我们在 configmap.yaml 模板中还是可以访问,因为命名模板是全局的:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  {{- template "mychart.labels" }}
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}

因为上面我们提到过命名模板是全局的,我们可以再渲染下上面的模板可以得到正确的结果。

设置模板范围

上面我们定义的模板中,还没有使用到任何对象,只使用了函数,现在我们来修改下定义的命名模板,包含 chart 的名称和版本:

yaml
{{/* 生成基本的 Label 标签 */}}
{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
    chart: {{ .Chart.Name }}
    version: {{ .Chart.Version }}
{{- end }}

现在我们来渲染下模板,会出现下面的错误:

shell
$ helm install --generate-name --dry-run --debug ./my
chart
install.go:148: [debug] Original chart version: ""
install.go:165: [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/cour
se/k8strain/content/helm/manifests/mychart

Error: unable to build kubernetes objects from release manifest: error validati
ng "": error validating data: [unknown object type "nil" in ConfigMap.metadata.
labels.chart, unknown object type "nil" in ConfigMap.metadata.labels.version]
helm.go:76: [debug] error validating "": error validating data: [unknown object
 type "nil" in ConfigMap.metadata.labels.chart, unknown object type "nil" in Co
nfigMap.metadata.labels.version]
......

我们可以看到提示 labels.chart 为 nil,这是因为我们使用的 .Chart.Name 不在定义的这个模板的作用域范围内,当渲染命名模板(使用 define 定义)的时候,它将接收模板调用传递的作用域。在我们这个示例中,我们是这样引用这个模板的:

yaml
{{- template "mychart.labels" }}

没有传入任何作用域,所以在模板内我们无法访问 . 中的任何内容,当然要解决很简单,我们只需要把作用域范围传递给模板即可:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  {{- template "mychart.labels" . }}
......

我们这里在使用 template 调用模板的时候传递了 .,我们可以很容易传递 .Values 或者 .Values.favorite 或者我们想要的任何范围,但是这里我们想要的是顶级作用域,所以我们传递的是 .

现在我们再来重新渲染我们的模板,可以得到如下所示的结果:

yaml
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576035668-configmap
  labels:
    generator: helm
    date: 2019-12-11
    chart: mychart
    version: 0.1.0
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"


现在 {{ .Chart.Name }} 解析为了 mychart,而 {{ .Chart.Version }} 解析为了 0.1.0。

include 函数

假设我们定义了一个如下所示的简单模板:

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

现在我们想把上面的内容插入到模板的 labels 部分,在 data 部分也想要这个内容:

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

但是我们直接渲染上面的模板还是会有错误:

shell
$ helm install --generate-name --dry-run --debug ./my
chart
install.go:148: [debug] Original chart version: ""
install.go:165: [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/cour
se/k8strain/content/helm/manifests/mychart

Error: unable to build kubernetes objects from release manifest: error validati
ng "": error validating data: [ValidationError(ConfigMap): unknown field "app_n
ame" in io.k8s.api.core.v1.ConfigMap, ValidationError(ConfigMap): unknown field
 "app_version" in io.k8s.api.core.v1.ConfigMap]
helm.go:76: [debug] error validating "": error validating data: [ValidationErro
r(ConfigMap): unknown field "app_name" in io.k8s.api.core.v1.ConfigMap, Validat
ionError(ConfigMap): unknown field "app_version" in io.k8s.api.core.v1.ConfigMap]
......

因为 template 只是一个动作,而不是一个函数,所以无法将模板调用的输出传递给其他函数,只是内联插入,相当于渲染的结果是这样的:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: measly-whippet-configmap
  labels:
    app_name: mychart
app_version: "0.1.0+1478129847"
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"
  app_name: mychart
app_version: "0.1.0+1478129847"

很明显上面的 YAML 文件是不符合 ConfigMap 资源对象的格式要求的,所以报错了。为解决这个问题,Helm 提供了代替 template 的函数 include,可以将模板的内容导入到当前的管道中,这样就可以在管道中传递给其他函数进行处理了,如下所示:

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

现在我们重新渲染就可以得到正确的结果了,这是因为我们用 include 函数得到模板内容后通过管道传给了后面的 indent 函数来保证了缩进:

yaml
Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576036671-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"

建议

在 Helm 模板中最好使用 include 而不是 template,这样可以更好地处理 YAML 文档的输出格式。

有时候如果我们只想导入内容而不是模板,这个时候我们可以通过下面描述的 .Files 对象来访问文件实现。

在 secret.yaml 文件中使用命名模板:

plain
yaml
复制代码
{{- range .Values.tlsSecrets }}
apiVersion: v1
kind: Secret
metadata:
  name: {{ .name }}
  labels:
    {{ include "mychart.labels" . | indent 4 }}
type: kubernetes.io/tls
data:
  tls.crt: {{ .certificate }}
  tls.key: {{ .key }}
---
{{- end }}

在 deployment.yaml 文件中使用命名模板和部分模板:

yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{ include "mychart.labels" . | indent 4 }}
spec:
  replicas: {{ .Values.replicas }}
  selector:
    matchLabels:
      app: {{ include "mychart.fullname" . }}
  template:
    metadata:
      labels:
        app: {{ include "mychart.fullname" . }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ include "mychart.image" . }}"
        ports:
        - containerPort: {{ .Values.service.port }}

访问文件

在上一节中我们介绍了几种创建和访问命名模板的方法,这使得从另一个模板中导入一个模板变得很容易,但是有时候需要导入一个不是模板的文件并注入其内容,而不通过模板渲染器获得内容。

Helm 提供了一个 .Files 对象对文件的访问,但是在模板中使用这个对象之前,还有几个需要注意的事项值得一提:

  • 可以在 Helm chart 中添加额外的文件,这些文件也会被打包,不过需要注意,由于 Kubernetes 对象的存储限制,Charts 必须小于 1M
  • 由于一些安全原因,通过 .Files 对象无法访问某些文件
    • 无法访问 templates/ 下面的文件
    • 无法访问使用 .helmignore 排除的文件
  • Chart 不会保留 UNIX 模式的信息,所以,当使用 .Files 对象时,文件级别的权限不会对文件的可用性产生影响。

基本示例

现在我们来编写一个模板,将3个文件读入到 ConfigMap 模板中,首先我们在 chart 中添加3个文件,将3个文件都直接放置在 mychart/目录中。

config1.toml:

plain
message = Hello from config 1

config2.toml:

plain
message = This is config 2

config3.toml:

plain
message = Goodbye from config 3

3个文件都是简单的 TOML 文件,我们知道这些文件的名称,所以我们可以使用 range 函数来遍历它们,并将其内容注入到 ConfigMap 中去。

yaml
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 . }} 获取文件内容。

现在我们渲染这个模板会产生包含3个文件内容的单个 ConfigMap:

yaml
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576046462-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 的路径包中导入了许多功能供你使用,它们都可以使用与 Go 包中相同的相同名称来访问,但是首字母需要小写,比如 Base 需要变成 base,导入的函数有:- Base - Dir - Ext - IsAbs - Clean

Glob 模式

随着 chart 的增长,你可能需要更多地组织文件,因此 Helm 提供了 Files.Glob 的方法来帮助我们获取具有 glob 模式的文件。

.Glob 返回 Files 类型,所以你可以在返回的对象上调用任何 Files 方法。比如,我们的文件目录结构如下所示:

yaml
foo/:
  foo.txt foo.yaml

bar/:
  bar.go bar.conf baz.yaml

我们可以用 Glob 进行多种选择:

yaml
{{ range $path := .Files.Glob "**.yaml" }}
{{ $path }}: |
{{ .Files.Get $path }}
{{ end }}

或者

yaml
{{ range $path, $bytes := .Files.Glob "foo/*" }}
{{ $path }}: '{{ b64enc $bytes }}'
{{ end }}

ConfigMap 和 Secrets

想要将文件内容同时放入 ConfigMap 和 Secrets 中,以便在运行时安装到 Pod 中,这种需求很常见,为了解决这个问题,Helm 在 Files 类型上添加了一个实用的方法。

根据上面的目录结构,我们可以按照如下的方式进行处理:

yaml
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 编码进行编码:

yaml
apiVersion: v1
kind: Secret
metadata:
  name: {{ .Release.Name }}-secret
type: Opaque
data:
  token: |-
    {{ .Files.Get "config1.toml" | b64enc }}

上面将采用我们上面的 config1.toml 文件并对其内容进行 base64 编码,渲染会得到如下所示的结果:

yaml
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: Secret
metadata:
  name: mychart-1576048287-secret
type: Opaque
data:
  token: |-
    bWVzc2FnZSA9IEhlbGxvIGZyb20gY29uZmlnIDEK

Lines

有时,需要访问模板中文件的每一行内容,Helm 也提供了方法的 Lines 方法,我们可以使用 range 函数遍历每行内容:

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

在 Helm 安装的时候无法将文件传递到 chart 外部,所以,如果你要求用户提供数据的话,则必须使用 helm install -f 或者 helm install --set 来获取。

NOTES txt 文件

在本节中我们将来了解为 chart 用户提供说明的一个 NOTES.txt 文件,在 chart 安装或者升级结束时,Helm 可以为用户打印出一些有用的信息,使用模板也可以自定义这些信息。

创建 NOTES.txt 文件

要将安装说明添加到 chart 中,只需要创建一个 templates/NOTES.txt 文件,该文件纯文本的,但是可以像模板一样进行处理,并具有所有常规模板的功能和可用对象。

现在让我们来创建一个简单的 NOTES.txt 文件:

bash
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 ./mychart,我们就可以在底部看到这样的消息:

shell
RESOURCES:
==> v1/Secret
NAME                   TYPE      DATA      AGE
rude-cardinal-secret   Opaque    1         0s

==> v1/ConfigMap
NAME                      DATA      AGE
rude-cardinal-configmap   3         0s


NOTES:
Thank you for installing mychart.

Your release is named rude-cardinal.

To learn more about the release, try:

  $ helm status rude-cardinal
  $ helm get rude-cardinal

用这种方式可以向用户提供一个有关如何使用其新安装的 chart 的详细信息,强烈建议创建 NOTES.txt 文件,虽然这不是必须的。

Subcharts 和 Global Values

到现在为止,我们从单一模板,到多个模板文件,但是都仅仅是处理的一个 chart 包,但是 charts 可能具有一些依赖项,我们称为 subcharts(子 chart),接下来我们将创建一个子 chart。

同样在深入了解之前,我们需要了解下子 chart 相关的一些信息。

  • 子 chart 是独立的,这意味着子 chart 不能显示依赖其父 chart
  • 所以子 chart 无法访问其父级的值
  • 父 chart 可以覆盖子 chart 的值
  • Helm 中有可以被所有 charts 访问的全局值的概念

创建子chart

同样还是在之前操作的 mychart/ 这个 chart 包中,我们来尝试添加一些新的子 chart:

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

和前面一样,我们删除了所有的基本模板,这样我们可以从头开始。

添加 values 和 模板

接下来我们为 mysubchart 这个子 chart 创建一个简单的模板和 values 值文件,mychart/charts/mysubchart 中已经有一个 values.yaml 文件了,在文件中添加下面的 values:

yaml
dessert: cake

下面我们再创建一个新的 ConfigMap 模板 mychart/charts/mysubchart/templates/configmap.yaml

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

因为每个子 chart 都是独立的 chart,所以我们可以单独测试 mysubchart:

shell
$ helm install --generate-name --dry-run --debug mychart/charts/mysubchart
install.go:148: [debug] Original chart version: ""
install.go:165: [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/course/k8strain/content/helm/manifests/mychart/charts/mysubchart

NAME: mysubchart-1576050755
LAST DEPLOYED: Wed Dec 11 15:52:36 2019
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
dessert: cake

HOOKS:
MANIFEST:
---
# Source: mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysubchart-1576050755-cfgmap2
data:
  dessert: cake

从父 chart 覆盖 values

我们原来的 chart - mychart 现在是 mysubchart 的父级 chart 了。

由于 mychart 是父级,所以我们可以在 mychart 中指定配置,并将该配置发送到 mysubchart 中去,比如,我们可以这样修改 mychart/values.yaml

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

mysubchart:
  dessert: ice cream

最后两行,mysubchart 部分中的所有指令都回被发送到 mysubchart 子 chart 中,所以,如果我们现在渲染模板,我们可以看到 mysubchart 的 ConfigMap 会被渲染成如下的内容:

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

我们可以看到顶层的 values 值覆盖了子 chart 中的值。

这里有一个细节需要注意,我们没有将 mychart/charts/mysubchart/templates/configmap.yaml 模板更改为指向 .Values.mysubchart.dessert,因为从该模板的绝度来看,该值仍然位于 .Values.dessert,当模板引擎传递 values 值的时候,它会设置这个作用域,所以,对于 mysubchart 模板,.Values 中仅仅提供用于该子 chart 的值。

但是有时候如果我们确实希望某些值可以用于所有模板,这个时候就可以使用全局 chart values 值来完成了。

全局值

全局值是可以从任何 chart 或子 chart 中都可以访问的值,全局值需要显示的声明,不能将现有的非全局对象当作全局对象使用。

Values 数据类型具有一个名为 Values.global 的保留部分,可以在其中设置全局值,我们在 mychart/values.yaml 文件中添加一个全局值:

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

mysubchart:
  dessert: ice cream

global:
  salad: caesar
由于全局值的原因,在 mychart/templates/configmap.yaml 和 mysubchart/templates/configmap.yaml 下面都应该可以以 {{ .Values.global.salad }} 的形式来访问这个值。

mychart/templates/configmap.yaml

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  salad: {{ .Values.global.salad }}

mysubchart/templates/configmap.yaml:

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

然后我们渲染这个模板,可以得到如下所示的内容:

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

全局值对于传递这样的数据比较有用。

共享模板

父级 chart 和子 chart 可以共享模板,任何 chart 中已定义的块都可以用于其他 chart。比如,我们可以定义一个简单的模板,如下所示:

yaml
{{- define "labels" }}from: mychart{{ end }}

前面我们提到过可以使用在模板中使用 include 和 template,但是使用 include 的一个优点是可以动态引入模板的内容:

yaml
{{ include $mytemplate }}

模板调试

调试模板可能比较麻烦,因为渲染的模板会发送到 Kubernetes API server,而 API server 可能会因为格式以外的一些原因而拒绝 YAML 文件。

下面这些命令可以帮助你调试一些问题:

  • helm lint 是验证 chart 是否遵循最佳实践的首选工具
  • helm install --dry-run --debug 或者 helm template --debug:前面我们已经使用了这个技巧,这个是让服务器渲染模板,然后返回生成的资源清单文件的好方法,而且不会真正的去安装这些资源
  • helm get manifest:这是查看服务器上安装了哪些模板的好方法

当你的 YAML 文件无法解析的时候,但你想要查看生成的内容的时候,检索 YAML 的一种简单方法是注释掉模板中的问题部分,然后重新运行 helm install --dry-run --debug

yaml
apiVersion: v2
# some: problem section
# {{ .Values.foo | quote }}

上面的内容将呈现并返回完整的注释:

yaml
apiVersion: v2
# some: problem section
#  "bar"

这提供了一种查看生成的内容的快速方法。

Chart Hooks

Helm 也提供了一种 Hook 机制,可以允许 chart 开发人员在 release 生命周期的某些时间点进行干预。

比如,可以使用 hook 来进行下面的操作:

  • 在加载任何 charts 之前,在安装的时候加载 ConfigMap 或者 Secret
  • 在安装新的 chart 之前,执行一个 Job 来备份数据库,然后在升级后执行第二个 Job 还原数据
  • 在删除 release 之前运行一个 JOb,以在删除 release 之前适当地取消相关服务

Hooks 的工作方式类似于普通的模板,但是他们具有特殊的注解,这些注解使 Helm 可以用不同的方式来使用他们。

Hooks

在 Helm 中定义了如下一些可供我们使用的 Hooks:

  • 预安装pre-install:在模板渲染后,kubernetes 创建任何资源之前执行
  • 安装后post-install:在所有 kubernetes 资源安装到集群后执行
  • 预删除pre-delete:在从 kubernetes 删除任何资源之前执行删除请求
  • 删除后post-delete:删除所有 release 的资源后执行
  • 升级前pre-upgrade:在模板渲染后,但在任何资源升级之前执行
  • 升级后post-upgrade:在所有资源升级后执行
  • 预回滚pre-rollback:在模板渲染后,在任何资源回滚之前执行
  • 回滚后post-rollback:在修改所有资源后执行回滚请求
  • 测试test:在调用 Helm test 子命令的时候执行(可以查看测试文档

生命周期

Hooks 允许开发人员在 release 的生命周期中的一些关键节点执行一些钩子函数,我们正常安装一个 chart 包的时候的生命周期如下所示:

  1. 用户运行 helm install foo
  2. Helm 库文件调用安装 API
  3. 经过一些验证,Helm 库渲染 foo 模板
  4. Helm 库将产生的资源加载到 kubernetes 中去
  5. Helm 库将 release 对象和其他数据返回给客户端
  6. Helm 客户端退出

如果开发人员在 install 的生命周期中定义了两个 hook:pre-installpost-install,那么我们安装一个 chart 包的生命周期就会多一些步骤了:

  1. 用户运行helm install foo
  2. Helm 库文件调用安装 API
  3. crds/ 目录下面的 CRDs 被安装
  4. 经过一些验证,Helm 库渲染 foo 模板
  5. Helm 库将 hook 资源加载到 kubernetes 中,准备执行pre-install hooks
  6. Helm 库会根据权重对 hooks 进行排序(默认分配权重0,权重相同的 hook 按升序排序)
  7. Helm 库然后加载最低权重的 hook
  8. Helm 库会等待,直到 hook 准备就绪
  9. Helm 库将产生的资源加载到 kubernetes 中,注意如果添加了 --wait 参数,Helm 库会等待所有资源都准备好,在这之前不会运行 post-install hook
  10. Helm 库执行 post-install hook(加载 hook 资源)
  11. Helm 库等待,直到 hook 准备就绪
  12. Helm 库将 release 对象和其他数据返回给客户端
  13. Helm 客户端退出

等待 hook 准备就绪,这是一个阻塞的操作,如果 hook 中声明的是一个 Job 资源,Helm 将等待 Job 成功完成,如果失败,则发布失败,在这个期间,Helm 客户端是处于暂停状态的。

对于所有其他类型,只要 kubernetes 将资源标记为加载(添加或更新),资源就被视为就绪状态,当一个 hook 声明了很多资源是,这些资源是被串行执行的。

另外需要注意的是 hook 创建的资源不会作为 release 的一部分进行跟踪和管理,一旦 Helm 验证了 hook 已经达到了就绪状态,它就不会去管它了。

所以,如果我们在 hook 中创建了资源,那么不能依赖 helm uninstall 去删除资源,因为 hook 创建的资源已经不受控制了,要销毁这些资源,你需要将 helm.sh/hook-delete-policy 这个 annotation 添加到 hook 模板文件中,或者设置 Job 资源的生存(TTL)字段

编写 Hook

Hooks 就是 Kubernetes 资源清单文件,在元数据部分带有一些特殊的注解,因为他们是模板文件,所以你可以使用普通模板所有的功能,包括读取 .Values.Release.Template

例如,在 templates/post-install-job.yaml 文件中声明一个 post-install 的 hook:

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: "{{ .Release.Name }}"
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
    app.kubernetes.io/instance: {{ .Release.Name | quote }}
    app.kubernetes.io/version: {{ .Chart.AppVersion }}
    helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
  annotations:
    # 因为添加了这个 hook,所以我们这个资源被定义为了 hook
    # 如果没有这行,则当前这个 Job 会被当成 release 的一部分内容。
    "helm.sh/hook": post-install
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  template:
    metadata:
      name: "{{ .Release.Name }}"
      labels:
        app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
        app.kubernetes.io/instance: {{ .Release.Name | quote }}
        helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
    spec:
      restartPolicy: Never
      containers:
      - name: post-install-job
        image: "alpine:3.3"
        command: ["/bin/sleep","{{ default "10" .Values.sleepyTime }}"]

当前这个模板成为 hook 的原因就是添加这个注解:

yaml
annotations:
  "helm.sh/hook": post-install

一种资源也可以实现多个 hooks:

yaml
annotations:
  "helm.sh/hook": post-install,post-upgrade

类似的,实现给定 hook 的资源数量也没有限制,比如可以将 secret 和一个 configmap 都声明为 pre-install hook

当子 chart 声明 hooks 的时候,也会对其进行调用,顶层的 chart 无法禁用子 chart 所声明的 hooks。可以为 hooks 定义权重,这将有助于确定 hooks 的执行顺序:

yaml
annotations:
  "helm.sh/hook-weight": "5"

hook 权重可以是正数也可以是负数,但是必须用字符串表示,当 Helm 开始执行特定种类的 hooks 的时候,它将以升序的方式对这些 hooks 进行排序。

Hook 删除策略

我们还可以定义确定何时删除相应 hook 资源的策略,hook 删除策略可以使用下面的注解进行定义:

yaml
annotations:
  "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded

我们也可以选择一个或多个已定义的注解:

  • before-hook-creation:运行一个新的 hook 之前删除前面的资源(默认)
  • hook-succeeded:hook 成功执行后删除资源
  • hook-failed:hook 如果执行失败则删除资源

如果未指定任何 hook 删除策略注解,则默认情况下会使用 before-hook-creation 策略。