helm 包管理器
前言:
heml 是k8s中的一个包管理器
官方文档:https://helm.sh/zh/docs/chart_template_guide/getting_started/
Helm chart的结构如下:
mychart/ Chart.yaml values.yaml charts/ templates/ ...
templates/
目录包括了模板文件。当Helm评估chart时,会通过模板渲染引擎将所有文件发送到templates/
目录中。 然后收集模板的结果并发送给Kubernetes。
values.yaml
文件也导入到了模板。这个文件包含了chart的 默认值 。这些值会在用户执行helm install
或 helm upgrade
时被覆盖。
Chart.yaml
文件包含了该chart的描述。你可以从模板中访问它。
charts/
目录 可以 包含其他的chart(称之为 子chart)。 指南稍后我们会看到当涉及模板渲染时这些是如何工作的。
看看 mychart/templates/
目录
NOTES.txt
: chart的"帮助文本"。这会在你的用户执行helm install
时展示给他们。deployment.yaml
: 创建Kubernetes 工作负载的基本清单service.yaml
: 为你的工作负载创建一个 service终端基本清单。_helpers.tpl
: 放置可以通过chart复用的模板辅助对象
然后我们要做的是... 把它们全部删掉! 这样我们就可以从头开始学习我们的教程。我们在开始时会创造自己的NOTES.txt
和_helpers.tpl
。
基本操作
//创建一个chart包模板 helm create xxx(chart包名) //安装 helm install xxx -n namespace //卸载 helm uninstall xxx -n namespace //预填充变量/参数 helm install xxx -n namespace --dry-run
查看安装
//安装资源, 可以立即看到模板命令的结果: helm install clunky-serval ./mychart
NAME: full-coral LAST DEPLOYED: Tue Nov 1 17:36:01 2016 NAMESPACE: default STATUS: DEPLOYED REVISION: 1 TEST SUITE: None
//查看生成的完整的YAML helm get manifest clunky-serval
--- # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-configmap data: myvalue: "Hello World"
当你想测试模板渲染的内容但又不想安装任何实际应用时,可以使用helm install --debug --dry-run goodly-guppy ./mychart
。这样不会安装应用(chart)到你的kubenetes集群中,只会渲染模板内容到控制台(用于测试)。渲染后的模板如下:
$ helm install --debug --dry-run goodly-guppy ./mychart install.go:149: [debug] Original chart version: "" install.go:166: [debug] CHART PATH: /Users/ninja/mychart NAME: goodly-guppy LAST DEPLOYED: Thu Dec 26 17:24:13 2019 NAMESPACE: default STATUS: pending-install REVISION: 1 TEST SUITE: None USER-SUPPLIED VALUES: {} COMPUTED VALUES: affinity: {} fullnameOverride: "" image: pullPolicy: IfNotPresent repository: nginx imagePullSecrets: [] ingress: annotations: {} enabled: false hosts: - host: chart-example.local paths: [] tls: [] nameOverride: "" nodeSelector: {} podSecurityContext: {} replicaCount: 1 resources: {} securityContext: {} service: port: 80 type: ClusterIP serviceAccount: create: true name: null tolerations: [] HOOKS: MANIFEST: --- # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: goodly-guppy-configmap data: myvalue: "Hello World"
使用--dry-run
会让你变得更容易测试,但不能保证Kubernetes会接受你生成的模板。 最好不要仅仅因为--dry-run
可以正常运行就觉得chart可以安装。
内置对象
Release
: Release
对象描述了版本发布本身。包含了以下对象:
Values
: Values
对象是从values.yaml
文件和用户提供的文件传进模板的。默认为空
Chart
: Chart.yaml
文件内容。 Chart.yaml
里的所有数据在这里都可以可访问的。比如 {{ .Chart.Name }}-{{ .Chart.Version }}
会打印出 mychart-0.1.0
Files
: 在chart中提供访问所有的非特殊文件的对象。你不能使用它访问Template
对象,只能访问其他文件。 请查看这个 文件访问 部分了解更多信息
Capabilities
: 提供关于Kubernetes集群支持功能的信息
Template
: 包含当前被执行的当前模板信息
内置的值都是以大写字母开始。 这是符合Go的命名惯例。当你创建自己的名称时,可以按照团队约定自由设置。 就像很多你在 Artifact Hub 中看到的chart,其团队选择使用首字母小写将本地名称与内置对象区分开,本指南中我们也遵循该惯例。
详情请前往官网内置对象
Values
在chart中引用方式: {{ .Values.my_property }} (通过 . [点]来表达引用内置对象)
内容来自于多个位置:
- chart中的
values.yaml
文件- 如果是子chart,就是父chart中的
values.yaml
文件- 使用
-f
参数(helm install -f myvals.yaml ./mychart
)传递到helm install
或helm upgrade
的values文件- 使用
--set
(比如helm install --set foo=bar ./mychart
)传递的单个参数
以上列表有明确顺序:
默认使用
values.yaml
,可以被父chart的values.yaml
覆盖,继而被用户提供values文件覆盖, 最后会被--set
参数覆盖,优先级为values.yaml
最低,--set
参数最高。命令行多次使用 -f 指定文件,从左至右,优先级依次提高, 优先使用最后一个-f 指定文件
官网使用示例: https://helm.sh/zh/docs/chart_template_guide/values_files/
values.yaml:
favorite: drink: coffee food: pizza
quote函数: 把字符串用双引号括起来
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ quote .Values.favorite.drink }} food: {{ quote .Values.favorite.food }}
执行后结果:
apiVersion: v1 kind: ConfigMap metadata: name: trendsetting-p-configmap data: myvalue: "Hello World" drink: "coffee" food: "pizza"
Helm 有超过60个可用函数。其中有些通过 Go模板语言本身定义。其他大部分都是 Sprig 模板库。我们可以在示例看到其中很多函数。
管道
模板语言其中一个强大功能是 管道 概念。借鉴UNIX中的概念,管道符是将一系列的模板语言紧凑地将多个流式处理结果合并的工具。管道符是按顺序完成一系列任务的方式(白话文: 和linux 中的 管道 | 用法和作用都一样)
示例:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | quote }} food: {{ .Values.favorite.food | quote }}
倒置命令是模板中的常见做法。可以经常看到 .val | quote
而不是 quote .val
。实际上两种操作都是可以的。
结果:
apiVersion: v1 kind: ConfigMap metadata: name: trendsetting-p-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA"
default
模板中频繁使用的一个函数是default
: default DEFAULT_VALUE GIVEN_VALUE
。 这个函数允许你在模板中指定一个默认值,以防这个值被忽略
示例:
从values.yaml
中移除设置:
favorite: #drink: coffee food: pizza
重写chart:
drink: {{ .Values.favorite.drink | default "tea" | quote }}
重新运行 helm install --dry-run --debug fair-worm ./mychart 会生成如下内容:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: fair-worm-configmap data: myvalue: "Hello World" drink: "tea" food: "PIZZA"
在实际的chart中,所有的静态默认值应该设置在 values.yaml
文件中,且不应该重复使用 default
命令 (否则会出现冗余)。然而这个default
命令很适合计算值,其不能声明在values.yaml
文件中,比如:
drink: {{ .Values.favorite.drink | default (printf "%s-tea" (include "fullname" .)) }}
注意: {{ default x y }}
与 {{ y | default x }}
是相同的
lookup
函数
lookup
函数可以用于在运行的集群中 查找 资源。lookup函数简述为查找 apiVersion, kind, namespace,name
资源或者资源列表:
name
和 namespace
都是选填的,且可以传空字符串(""
)作为空。
以下是可能的参数组合:
当lookup
返回一个对象,它会返回一个字典。这个字典可以进一步被引导以获取特定值。
下面的例子将返回mynamespace
对象的annotations属性:
(lookup "v1" "Namespace" "" "mynamespace").metadata.annotations
当lookup
返回一个对象列表时,可以通过items
字段访问对象列表:
{{ range $index, $service := (lookup "v1" "Service" "mynamespace" "").items }} {{/* do something with each service */}} {{ end }}
当对象未找到时,会返回空值。可以用来检测对象是否存在。
lookup
函数使用Helm已有的Kubernetes连接配置查询Kubernetes。当与调用API服务交互时返回了错误 (比如缺少资源访问的权限),helm 的模板操作会失败。
请记住,
Helm在helm template
或者helm install|upgrade|delete|rollback --dry-run
时, 不应该请求Kubernetes API服务。由此,lookup
函数在该案例中会返回空列表(即字典)。
运算符也是函数
对于模板来说,运算符(eq
, ne
, lt
, gt
, and
, or
等等) 都是作为函数来实现的。 在管道符中,操作可以按照圆括号分组。
现在我们可以从函数和管道符返回到条件控制流,循环和范围修饰符。
模板函数
Helm 包含了很多可以在模板中利用的模板函数:
官网: https://helm.sh/zh/docs/chart_template_guide/function_list/
一功能分类目录:
tpl 救援功能
Helm 提供了一种简单的模板语言, 允许我们轻松引用在“values.yaml” 中定义的配置值。
引用值字符串类型
使用示例: 这是一个提供 API 端点 URL 列表的示例 “values.yaml” 文件
apiOneUrl:http://example.com/apiOne/v0 apiTwoUrl:http://example.com/apiTwo/v3 apiThreeUrl:http://example.com/apiThree/v7
您可能会发现所有配置字段都包含字符串“http://example.com/”。如果我们能提供如下的“值文件”就好了:
baseUrl: http://example.com apiOneUrl: "{{ .Values.baseUrl }}/apiOne/v0" apiTwoUrl: "{{ .Values.baseUrl }}/apiTwo/v3" apiThreeUrl: "{{ .Values.baseUrl }}/apiThree/v7
它不仅会减少冗余并节省大量打字,而且还会使将来更新配置值变得更加容易。但是,
由于 Helm 的模板引擎不会处理 “values.yaml”, 因此如果您尝试在模板中引用 .Values.apiOneUrl
, 您将只会得到原始字符串
{{ .Values.baseUrl }}/apiOne/v0
此时, 就需要我们 tpl 函数上场了该函数需要 2 个参数:
-
第一个参数是要处理的模板字符串。
-
最后一个参数是模板字符串处理时要使用的上下文数据。我们通常可以传递当前上下文数据 .(点) 以使所有配置值在模板处理期间可用。
在模板中尝试以下操作:
MyApiOneUrl: {{ tpl .Values.apiOneUrl . }}
.Values.apiOneUrl
的值 "{{ .Values.baseUrl }}/apiOne/v0"
将被转换为"http://example.com//apiOne/v0”
并且上面的模板将被渲染为:
MyApiOneUrl:http://example.com/apiOne/v0
非字符串类型
values.yaml 中如下:
apiUrls: apiOneUrl: "{{ .Values.baseUrl }}/apiOne/v0" apiTwoUrl: "{{ .Values.baseUrl }}/apiTwo/v3" apiThreeUrl: "{{ .Values.baseUrl }}/apiThree/v7"
此时,按照上方操作, 你会发现会报错: 错误的值类型;预期的字符串........
由于tpl
函数需要一个模板字符串作为第一个参数,因此错误消息确实有意义。
解决: 在传递给tpl
函数之前,使用toYaml
函数将非字符串类型值(在本例中为“map”)转换为“YAML”字符串:
{{- tpl (.Values.apiUrls | toYaml) .context }}
结果:
apiOneUrl**:http://example.com/apiOne/v0 apiTwoUrl:http://example.com/apiTwo/v3 apiThreeUrl:http://example.com/apiThree/v7
完美的解决方案:
{{/* Renders a value that contains template. Usage: {{ include "helm.tplvalues.render" ( dict "value" .Values.path.to.the.Value "context" $) }} */}} {{- define "helm.tplvalues.render" -}} {{- if typeIs "string" .value }} {{- tpl .value .context }} {{- else }} {{- tpl (.value | toYaml) .context }} {{- end }} {{- end -}}
在模板中使用,您可以使用函数调用“命名模板”:
MyApiOneUrl: {{ include "helm.tplvalues.render" ( dict "value" .Values.apiOneUrl "context" .) }}
你想省去定义自己的 “命名模板” 的工作量, 还可以使用 Bitnami 的 “通用”库图表 common 。要使用它,只需将以下依赖项添加到您的Chart.yaml
逻辑调用包含的 “命名模板”:
MyApiOneUrl: {{ include "common.tplvalues.render" ( dict "value" .Values.apiOneUrl "context" .) }}
流程控制(if/else)
控制结构(在模板语言中称为"actions")提供给你和模板作者控制模板迭代流的能力。 Helm的模板语言提供了以下控制结构:
if
/else
, 用来创建条件语句with
, 用来指定范围range
, 提供"for each"类型的循环
if
/else
基本的条件结构看起来像这样:
{{ if PIPELINE }} # Do something {{ else if OTHER PIPELINE }} # Do something else {{ else }} # Default case {{ end }}
注意我们讨论的是 管道 (判断条件)而不是值。这样做的原因是要清楚地说明控制结构可以执行整个管道,而不仅仅是计算一个值。
默认false的值:
如果是以下值时,管道会被设置为 false
- 布尔false
- 数字0
- 空字符串
nil
(空或null)- 空集合(
map
,slice
,tuple
,dict
,array
)
在所有其他条件下,条件都为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 }}
由于我们在最后一个例子中注释了drink: coffee
,输出中就不会包含mug: "true"
标识。 但如果将这行添加到values.yaml
文件中,输入就会是这样:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: eyewitness-elk-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" 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 SERVER: "localhost:44134" CHART PATH: /Users/mattbutcher/Code/Go/src/helm.sh/helm/_scratch/mychart Error: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 9: did not find expected key
发生了啥?因为空格导致生成了错误的YAML。
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: eyewitness-elk-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" mug: "true"
mug
的缩进是不对的。取消缩进重新执行一下:
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 }}
这个就得到了合法的YAML,但是看起来还是有点滑稽:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: telling-chimp-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" mug: "true"
注意在YAML中有一个空行,为什么?当模板引擎运行时,它 移除了 {{
和 }}
里面的内容,但是留下的空白完全保持原样。
YAML认为空白是有意义的,因此管理空白变得很重要。幸运的是,Helm模板有些工具可以处理此类问题。
首先,模板声明的大括号语法可以通过特殊的字符修改,并通知模板引擎取消空白。{{-
(包括添加的横杠和空格)表示向左删除空白, 而 -}}
表示右边的空格应该被去掉。 一定注意空格就是换行
要确保
-
和其他命令之间有一个空格。{{- 3 }}
表示“删除左边空格并打印3”,而{{-3 }}
表示“打印-3”。
使用这个语法,我们就可修改我们的模板,去掉新加的空白行:
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运行模板并查看结果:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: clunky-cat-configmap 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"
,因为这把两边的新行都删除了。
模板中的空白控制,请查看 官方Go模板文档
示例:
aa: "{{ 23 }} < {{ 45 }}" aa: "{{23 }} < {{45 }}" aa: "{{23}} < {{45}}" aa: "{{23 }} < {{ 45}}" aa: "{{23 -}} < {{- 45 }}"
执行后结果:
aa: "23 < 45" aa: "23 < 45" aa: "23 < 45" aa: "23 < 45" aa: "23<45"
最终,有时这更容易告诉模板系统如何缩进,而不是试图控制模板指令间的间距。
因此,您有时会发现使用indent
方法 ( {{ indent 2 "mug:true" }}
) 会很有用。
with 控制
这个用来控制变量范围。回想一下,
.
是对 当前作用域 的引用。因此.Values
就是告诉模板在当前作用域查找Values
对象。
with
的语法与if
语句类似,结构:
{{ with PIPELINE }}
# restricted scope
{{ end }}
作用域可以被改变。
with
允许你为特定对象设定当前作用域(.
)。比如,我们已经在使用.Values.favorite
。 修改配置映射中的.
的作用域指向.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 }}
注意我们从之前的练习中移除了if
条件,因为现在不需要了——with
后面的块只有在 PIPELINE
的值不为空时才会执行。
注意现在我们可以引用.drink
和.food
了,而不必限定他们。因为with
语句设置了.
指向.Values.favorite
。 .
被重置为{{ end }}
之后的上一个作用域。
这里有个注意事项,在限定的作用域内,无法使用.
访问父作用域的对象。这样会报错因为Release.Name
不在.
限定的作用域内。
{{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} food: {{ .food | upper | quote }} release: {{ .Release.Name }} {{- end }}
但是如果对调最后两行就是正常的, 因为在{{ end }}
之后作用域被重置了。
{{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} food: {{ .food | upper | quote }} {{- end }} release: {{ .Release.Name }}
或者,我们可以使用 $
从父作用域中访问Release.Name
对象。当模板开始执行后 $
会被映射到根作用域,且执行过程中不会更改。 下面这种方式也可以正常工作:
{{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} food: {{ .food | upper | quote }} release: {{ $.Release.Name }} {{- end }}
range (迭代方式)
很多编程语言支持使用for
循环,foreach
循环,或者类似的方法机制。在Helm的模板语言中,在一个集合中迭代的方式是使用range
操作符。
开始之前,我们先在values.yaml
文件添加一个披萨的配料列表:
favorite: drink: coffee food: pizza pizzaToppings: - mushrooms - cheese - peppers - onions
现在我们有了一个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 }}
我可以使用$
从父作用域访问Values.pizzaToppings
列表。当模板开始执行后$
会被映射到根作用域, 且执行过程中不会更改。下面这种方式也可以正常工作:
toppings: |- {{- range $.Values.pizzaToppings }} - {{ . | title | quote }} {{- end }} {{- end }}
range
方法“涵盖”(迭代)pizzaToppings
列表。但现在发生了有意思的事情。 就像with
设置了.
的作用域,range
操作符也做了同样的事。每一次循环,.
都会设置为当前的披萨配料。 也就是说,第一次.
设置成了mushrooms
,第二次迭代设置成了cheese
,等等。
我们可以直接发送.
的值给管道,因此当我们执行{{ . | title | quote }}
时,它会发送.
到title
然后发送到quote
。 如果执行这个模板,输出是这样的:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: edgy-dragonfly-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" toppings: |- - "Mushrooms" - "Cheese" - "Peppers" - "Onions"
toppings: |-
行是声明的多行字符串。 所以这个配料列表实际上不是YAML列表, 是个大字符串。为什么要这样做?因为在配置映射data
中的数据是由键值对组成,key和value都是简单的字符串。
要理解这个示例,请查看 Kubernetes ConfigMap 文档。
tuple :
Helm模板的tuple
可以很容易实现该功能。在计算机科学中, 元组表示一个有固定大小的类似列表的集合,但可以是任意数据类型。这大致表达了tuple
的用法
sizes: |- {{- range tuple "small" "medium" "large" }} - {{ . }} {{- end }}
上述模板会生成以下内容:
sizes: |- - small - medium - large
除了列表和元组,range
可被用于迭代有键值对的集合(像map
或dict
)。我们会在下一部分介绍模板变量是看到它是如何应用的。
变量
函数、管道符、对象和控制结构都可以控制,我们转向很多编程语言中更基本的思想之一:变量。 在模板中,很少被使用。但是我们可以使用变量简化代码,并更好地使用
with
和range
。
在之前的例子中,我们看到下面的代码会失败:
{{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} food: {{ .food | upper | quote }} release: {{ .Release.Name }} {{- end }}
Release.Name
不在with
块的限制范围内。解决作用域问题的一种方法是将对象分配给可以不考虑当前作用域而访问的变量。
Helm模板中,变量是对另一个对象的命名引用。遵循$name
变量的格式且指定了一个特殊的赋值运算符::=
。 我们可以使用针对Release.Name
的变量重写上述内容。
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
变量仍会执行版本名称。
运行之后会生成以下内容:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: viable-badger-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" release: viable-badger
变量在range
循环中特别有用。可以用于类似列表的对象,以捕获索引和值:
toppings: |- {{- range $index, $topping := .Values.pizzaToppings }} {{ $index }}: {{ $topping }} {{- end }}
注意先是range
,然后是变量,然后是赋值运算符,然后是列表。会将整型索引(从0开始)赋值给$index
并将值赋值给$topping
。 执行会生成:
toppings: |- 0: mushrooms 1: cheese 2: peppers 3: onions
对于数据结构有key和value,可以使用range
获取key和value。比如,可以通过.Values.favorite
进行循环:
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
。 运行之后会生成:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: eager-rabbit-configmap data: myvalue: "Hello World" drink: "coffee" food: "pizza"
变量一般不是"全局的"。作用域是其声明所在的块。上面我们在模板的顶层赋值了$relname
。变量的作用域会是整个模板。 但在最后一个例子中$key
和$val
作用域会在{{ range... }}{{ end }}
块内。
但有个变量一直是全局的 - $
- 这个变量一直是指向根的上下文。当在一个范围内循环时会很有用,同时你要知道chart的版本名称。
示例:
{{- range .Values.tlsSecrets }} apiVersion: v1 kind: Secret metadata: name: {{ .name }} labels: # Many helm templates would use `.` below, but that will not work, # however `$` will work here app.kubernetes.io/name: {{ template "fullname" $ }} # I cannot reference .Chart.Name, but I can do $.Chart.Name helm.sh/chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" app.kubernetes.io/instance: "{{ $.Release.Name }}" # Value from appVersion in Chart.yaml app.kubernetes.io/version: "{{ $.Chart.AppVersion }}" app.kubernetes.io/managed-by: "{{ $.Release.Service }}" type: kubernetes.io/tls data: tls.crt: {{ .certificate }} tls.key: {{ .key }} --- {{- end }}
命名模板
三种声明和管理模板的方法:define
,template
,和block
。在这部分,我们将使用这三种操作并介绍一种特殊用途的 include
方法,类似于template
操作。
命名模板时要记住一个重要细节:模板名称是全局的。如果您想声明两个相同名称的模板,哪个最后加载就使用哪个。 因为在子chart中的模板和顶层模板一起编译,命名时要注意 chart特定名称。
一个常见的命名惯例是用chart名称作为模板前缀:{{ define "mychart.labels" }}
。使用特定chart名称作为前缀可以避免可能因为 两个不同chart使用了相同名称的模板而引起的冲突。
局部和 _ 文件
目前为止,我们已经使用了单个文件,且单个文件中包含了单个模板。但Helm的模板语言允许你创建命名的嵌入式模板, 这样就可以在其他位置按名称访问。
在编写模板细节之前,文件的命名惯例需要注意:
templates/
中的大多数文件被视为包含Kubernetes清单NOTES.txt
是个例外- 命名以下划线(
_
)开始的文件则假定 没有 包含清单内容。这些文件不会渲染为Kubernetes对象定义,但在其他chart模板中都可用。
这些文件用来存储局部和辅助对象,实际上当我们第一次创建mychart
时,会看到一个名为_helpers.tpl
的文件,这个文件是模板局部的默认位置。
用 define 和 template 声明和使用模板
define
操作允许我们在模板文件中创建一个命名模板,语法如下:
{{- define "MY.NAME" }} # body of template here {{- end }}
示例:
{{- define "mychart.labels" }} labels: generator: helm date: {{ now | htmlDate }} {{- end }}
将模板嵌入到了已有的配置映射中,然后使用template
包含进来:
{{- 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
的引用直到template "mychart.labels"
被调用。 然后会按行渲染模板,因此结果类似这样:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: running-panda-configmap labels: generator: helm date: 2016-11-02 data: myvalue: "Hello World" drink: "coffee" food: "pizza"
注意:define
不会有输出,除非像本示例一样用模板调用它。
按照惯例,Helm chart将这些模板放置在局部文件中,一般是_helpers.tpl
。把这个方法移到那里:
{{/* Generate basic labels */}} {{- define "mychart.labels" }} labels: generator: helm date: {{ now | htmlDate }} {{- end }}
按照惯例define
方法会有个简单的文档块({{/* ... */}}
)来描述要做的事。
尽管这个定义是在_helpers.tpl
中,但它仍能访问configmap.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中的模板和顶层模板一起编译, 最好用 chart特定名称 命名你的模板。
常用的命名规则是用chart的名字作为模板的前缀: {{ define "mychart.labels" }}
。
设置模板范围(传参)
在上面定义的模板中,我们没有使用任何对象,仅仅使用了方法。修改定义好的模板让其包含chart名称和版本号(包含对象调用):{{/* Generate basic labels */}} {{- define "mychart.labels" }} labels: generator: helm date: {{ now | htmlDate }} chart: {{ .Chart.Name }} version: {{ .Chart.Version }} {{- end }}
如果渲染这个,会得到以下错误:
$ helm install --dry-run moldy-jaguar ./mychart Error: unable to build kubernetes objects from release manifest: error validating "": error validating data: [unknown object type "nil" in ConfigMap.metadata.labels.chart, unknown object type "nil" in ConfigMap.metadata.labels.version]
要查看渲染了什么,可以用--disable-openapi-validation
参数。
重新执行: helm install --dry-run --disable-openapi-validation moldy-jaguar ./mychart
。 结果并不是我们想要的:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: moldy-jaguar-configmap labels: generator: helm date: 2021-03-06 chart: version:
名字和版本号怎么了?没有出现在我们定义的模板中。当一个(使用define
创建的)命名模板被渲染时,会接收被template
调用传入的内容。 在我们的示例中,包含模板如下
{{- template "mychart.labels" }}
没有内容传入,所以模板中无法用.
访问任何内容。但这个很容易解决,只需要传递一个范围给模板:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap {{- template "mychart.labels" . }}
注意这个在template
调用末尾传入的.
,我们可以简单传入.Values
或.Values.favorite
或其他需要的范围。但一定要是顶层范围。
现在我们可以用helm install --dry-run --debug plinking-anaco ./mychart
执行模板,然后得到:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: plinking-anaco-configmap labels: generator: helm date: 2021-03-06 chart: mychart version: 0.1.0
现在{{ .Chart.Name }}
解析为mychart
,{{ .Chart.Version }}
解析为0.1.0
。
include 方法
相较于使用
template
,在helm中使用include
被认为是更好的方式 只是为了更好地处理YAML文档的输出格式
假设定义了一个简单模板如下:
{{- define "mychart.app" -}} app_name: {{ .Chart.Name }} app_version: "{{ .Chart.Version }}" {{- end -}}
现在假设我想把这个插入到模板的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" . }}
如果渲染这个,会得到以下错误:
$ helm install --dry-run measly-whippet ./mychart Error: unable to build kubernetes objects from release manifest: error validating "": error validating data: [ValidationError(ConfigMap): unknown field "app_name" in io.k8s.api.core.v1.ConfigMap, ValidationError(ConfigMap): unknown field "app_version" in io.k8s.api.core.v1.ConfigMap]
要查看渲染了什么,可以用--disable-openapi-validation
参数
重新执行: helm install --dry-run --disable-openapi-validation measly-whippet ./mychart
。 输入不是我们想要的:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: measly-whippet-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"
注意两处的app_version
缩进都不对,为啥?因为被替换的模板中文本是左对齐的。由于template
是一个行为,不是方法,无法将 template
调用的输出传给其他方法,数据只是简单地按行插入。
为了处理这个问题,Helm提供了一个template
的可选项,可以将模板内容导入当前管道,然后传递给管道中的其他方法。
下面这个示例,使用indent
正确地缩进了mychart.app
模板:
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 }}
现在生成的YAML每一部分都可以正确缩进了:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: edgy-mole-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 提供了通过
.Files
对象访问文件的方法。
不过,在我们使用模板示例之前,有些事情需要注意:
- 可以添加额外的文件到chart中。虽然这些文件会被绑定。但是要小心,由于Kubernetes对象的限制,Chart必须小于1M。
- 通常处于安全考虑,一些文件无法通过
.Files
对象访问:- 无法访问
templates/
中的文件 - 无法访问使用
.helmignore
排除的文件 - helm应用 subchart之外的文件,包括父级中的,不能被访问的
- 无法访问
- Chart不能保留UNIX模式信息,因此当文件涉及到
.Files
对象时,文件级权限不会影响文件的可用性。
基本示例
我们来写一个读取三个文件到配置映射ConfigMap的模板。开始之前,我们会在chart中添加三个文件, 直接放到mychart/
目录中。
config1.toml
:
message = Hello from config 1
config2.toml
:
message = This is config 2
config3.toml
:
message = Goo
每个都是简单的TOML文件(类似于windows老式的INI文件)。我们知道这些文件的名称,因此我们使用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: quieting-giraf-configmap data: config1.toml: |- message = Hello from config 1 config2.toml: |- message = This is config 2 config3.toml: |- message = Goodbye from config 3
更多详情请前往官网: Helm Files 对象
子 chart 和全局值
chart可以使用依赖,称为 子chart,且有自己的值和模板。
在深入研究代码之前,需要了解一些应用的子chart的重要细节:
- 子chart被认为是“独立的”,意味着子chart从来不会显示依赖它的父chart。
- 因此,子chart无法访问父chart的值。
- 父chart可以覆盖子chart的值。
- Helm有一个 全局值 的概念,所有的chart都可以访问。
这些限制不一定都适用于提供标准化辅助功能的:library charts
其中有两个 libraty chart 的示例:
创建子 chart
为了做这些练习,我们可以从本指南开始时创建的mychart/
开始,并在其中添加一个新的chart。
$ cd mychart/charts $ helm create mysubchart Creating mysubchart $ rm -rf mysubchart/templates/*
注意,和以前一样,我们删除了所有的基本模板,然后从头开始,在这个指南中,我们聚焦于模板如何工作,而不是管理依赖。 但 Chart指南提供了更多子chart运行的信息。
下一步,为mysubchart
创建一个简单的模板和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都是 独立的chart,可以单独测试mysubchart
:
$ helm install --generate-name --dry-run --debug mychart/charts/mysubchart SERVER: "localhost:44134" CHART PATH: /Users/mattbutcher/Code/Go/src/helm.sh/helm/_scratch/mychart/charts/mysubchart NAME: newbie-elk TARGET NAMESPACE: default CHART: mysubchart 0.1.0 MANIFEST: --- # Source: mysubchart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: newbie-elk-cfgmap2 data: dessert: cake
用父 chart 的值来覆盖
原始chart,mychart
现在是mysubchart
的 父。这种关系是基于mysubchart
在mychart/charts
中这一事实。
因为mychart
是父级,可以在mychart
指定配置并将配置推送到mysubchart
。比如可以修改mychart/values.yaml
如下:
favorite: drink: coffee food: pizza pizzaToppings: - mushrooms - cheese - peppers - onions mysubchart: dessert: ice cream
注意最后两行,在mysubchart
中的所有指令会被发送到mysubchart
chart中。因此如果运行helm install --dry-run --debug mychart
,会看到一项mysubchart
的配置:
# Source: mychart/charts/mysubchart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: unhinged-bee-cfgmap2 data: dessert: ice cream
现在,子chart的值已经被顶层的值覆盖了。
这里需要注意个重要细节。我们不会改变mychart/charts/mysubchart/templates/configmap.yaml
模板到 .Values.mysubchart.dessert
的指向。
从模板的角度来看,值依然是在.Values.dessert
。当模板引擎传递值时,会设置范围。 因此对于mysubchart
模板,.Values
中只提供专门用于mysubchart
的值。
但是有时确实希望某些值对所有模板都可用。这是使用全局chart值完成的。
全局值 chart
全局值是使用完全一样的名字在所有的chart及子chart中都能访问的值。全局变量需要显示声明。不能将现有的非全局值作为全局值使用。
这些值数据类型有个保留部分叫Values.global
,可以用来设置全局值。在mychart/values.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
:
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 }}
现在如果预安装,两个输出会看到相同的值:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: silly-snake-configmap data: salad: caesar --- # Source: mychart/charts/mysubchart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: silly-snake-cfgmap2 data: dessert: ice cream salad: caesar
全局值在类似这样传递信息时很有用,不过要确保使用全局值配置正确的模板,确实需要一些计划。
避免用 块(block)
Go 模板语言提供了一个 block
关键字允许开发者提供一个稍后会被重写的默认实现。在Helm chart中, 块并不是用于覆盖的最好工具,因为如果提供了同一个块的多个实现,无法预测哪个会被选定。
建议改为使用include
。
NOTES.txt文件
NOTES.txt 为chart用户提供说明的Helm工具。在
helm install
或helm upgrade
命令的最后,Helm会打印出对用户有用的信息。 使用模板可以高度自定义这部分信息。
要在chart添加安装说明,只需创建templates/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 all {{ .Release.Name }}
现在如果我们执行helm install rude-cardinal ./mychart
会在底部看到:
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 all rude-cardinal
使用NOTES.txt
这种方式是给用户提供关于如何使用新安装的chart细节信息的好方法。尽管并不是必需的,强烈建议创建一个NOTES.txt
文件。
.helmignore 文件
.helmignore
文件用来指定你不想包含在你的helm chart中的文件。
如果该文件存在,helm package
命令会在打包应用时忽略所有在.helmignore
文件中匹配的文件。
这有助于避免不需要的或敏感文件及目录添加到你的helm chart中。
.helmignore
文件支持Unix shell的全局匹配,相对路径匹配,以及反向匹配(以!作为前缀)。每行只考虑一种模式。
这里是一个.helmignore
文件示例:
# comment # Match any file or path named .helmignore .helmignore # Match any file or path named .git .git # Match any text file *.txt # Match only directories named mydir mydir/ # Match only text files in the top-level directory /*.txt # Match only the file foo.txt in the top-level directory /foo.txt # Match any file named ab.txt, ac.txt, or ad.txt a[b-d].txt # Match any file under subdir matching temp* */temp* */*/temp* temp?
一些值得注意的和.gitignore不同之处:
- 不支持'**'语法。
- globbing库是Go的 'filepath.Match',不是fnmatch(3)
- 末尾空格总会被忽略(不支持转义序列)
- 不支持'!'作为特殊的引导序列
- 默认不会排除自身,需要显式添加
.helmignore
调式模板
调试模板可能很棘手,因为渲染后的模板发送给了Kubernetes API server,可能会以格式化以外的原因拒绝YAML文件。
以下命令有助于调试:
helm lint
是验证chart是否遵循最佳实践的首选工具。helm template --debug
在本地测试渲染chart模板。helm install --dry-run --debug
:我们已经看到过这个技巧了,这是让服务器渲染模板的好方法,然后返回生成的清单文件。helm get manifest
: 这是查看安装在服务器上的模板的好方法。
当你的YAML文件解析失败,但你想知道生成了什么,检索YAML一个简单的方式是注释掉模板中有问题的部分,
然后重新运行 helm install --dry-run --debug
:
apiVersion: v2
# some: problem section
# {{ .Values.foo | quote }}
以上内容会被渲染同时返回完整的注释:
apiVersion: v2 # some: problem section # "bar"
这样就提供了一种快速查看没有被YAML错误解析阻塞的生成内容的方式。
博主常用:
1、另一种方式是通过 --disable-openapi-validation 参数。 模板渲染失败, 要查看渲染了什么,可以用 --disable-openapi-validation 参数
helm install --dry-run --disable-openapi-validation moldy-jaguar ./mychart
2、 本地测试渲染chart模板, 不会向服务器端API发送请求 helm template
helm template -f Helm-test/values-sit.yaml -f Helm-test/values-dev.yaml my-helm Helm-test/
3、渲染单个template文件:
1、cd mychart/(--show-only 的模板参数必须 templates 开头) 2、helm template --show-only templates/test.yaml test2 ../mychart/
常见报错记录
推荐:在执行前先执行 helm lint 验证chart是否合法
1. helm install -f mychart/values-1.yaml --disable-openapi-validation --debug --dry-run mychart/
Error: INSTALLATION FAILED: must either provide a name or specify --generate-name
原因: 执行时 没有为取chart 的名称
解决:
helm install -f mychart/values-1.yaml --disable-openapi-validation --generate-name --debug --dry-run mychart/
或
helm install -f mychart/values-1.yaml --disable-openapi-validation --debug --dry-run test-chart mychart/
2、error converting YAML to JSON: yaml: line 21: did not find expected key
原因 1:这个问题是由于缩进而发生的.
原因 2:引用时名称错误(如:多一个空格...)
个人经验:
安装:
helm install xxx ./chart
测试模板渲染的内容(不实际安装)
helm install --debug --dry-run xxx ./chart
指定values.yaml 文件
helm install -f mychart/values-1.yaml --debug --dry-run xxx ./chart
模板渲染失败, 要查看渲染了什么,可以用--disable-openapi-validation参数
helm install --dry-run --disable-openapi-validation moldy-jaguar ./mychart
渲染单个template文件:
1、cd mychart/(--show-only 的模板参数必须 templates 开头) 2、helm template --show-only templates/test.yaml test2 ../mychart/
断言:
required 方法 的例子声明了一个.Values.who需要的条目,并且当这个条目不存在时会打印错误信息:
value: {{ required "A valid .Values.who entry required!" .Values.who }}
NOTES.txt 文件在template 下:
NOTES.txt这种方式是给用户提供关于如何使用新安装的chart细节信息的好方法。不是必需的,建议创建一个NOTES.txt文件。 $ : 模板开始执行后$会被映射到根作用域,且执行过程中不会更改 |- : 标识是指多行字符串。(k8s 中的 结构类型:lists) --- :是分隔符,是可选的,在单一文件中,可用连续三个连字号---区分多个文件。 yaml 中 data: 对应的value 必须是string 类型
强制推断特定类型:
age: !!str 21 port: !!int "80"
!!str 告诉解释器 age 是一个字符串,即使它看起来像是整型。即使 port 被引号括起来,也会被视为 int。