T3、Kubernetes Helm

作者: Brinnatt 分类: 小工具 发布时间: 2025-02-16 00:03

Helm 是 Kubernetes 包管理工具。

Helm 可以帮助我们管理 Kubernetes 应用程序,Helm Charts 可以定义、安装和升级复杂的 Kubernetes 应用程序,Charts 包很容易创建、管理、分享和分布。

Helm 对于 Kubernetes 来说就相当于 yum 对于 Centos 的意义,如果没有 yum 的话,我们在 Centos 下面安装应用程序是很麻烦的,同样的,对于越来越复杂的 Kubernetes 应用程序来说,如果单纯依靠我们手动维护应用程序的 YAML 资源清单,成本也是巨大的。

接下来我们就来了解一下 Helm 的使用方法。

T3.1、安装

前提是需要一个可用的 Kubernetes 集群,然后在我们使用 Helm 的节点上已经配置好可以通过 kubectl 访问集群,因为 Helm 其实就是读取的 kubeconfig 文件来访问集群的。

由于 Helm V2 版本必须在 Kubernetes 集群中安装一个 Tiller 服务进行通信,这样大大降低了其安全性和可用性。

Helm V3 版本中移除了服务端,采用了通用的 Kubernetes CRD 资源来进行管理,这样就只需要连接上 Kubernetes 即可,而且 V3 版本已经发布了稳定版。

我们这里安装 v3.4.2 版本,软件包下载地址为:https://github.com/helm/helm/releases

我们可以根据自己的节点选择合适的包,比如我这里是 Mac,就下载 MacOS amd64 的版本。

$ helm version
version.BuildInfo{Version:"v3.4.2", GitCommit:"23dd3af5e19a02d4f4baa5b2f242645a1a3af629", GitTreeState:"clean", GoVersion:"go1.14.13"}

看到上面的版本信息证明已经成功了。

一旦 Helm 客户端准备成功后,我们就可以添加一个 chart 仓库,当然最常用的就是官方的 Helm stable charts 仓库,但是由于官方的 charts 仓库地址需要科学上网,我们可以使用微软的 charts 仓库代替:

$ helm repo add stable http://mirror.azure.cn/kubernetes/charts/
$ helm repo list
NAME            URL
stable          http://mirror.azure.cn/kubernetes/charts/

安装完成后可以用 search 命令来搜索可以安装的 chart 包:

$ helm search repo stable
NAME                                    CHART VERSION   APP VERSION                     DESCRIPTION
stable/acs-engine-autoscaler            2.2.2           2.1.1                           DEPRECATED Scales worker nodes within agent pools
stable/aerospike                        0.3.1           v4.5.0.5                        A Helm chart for Aerospike in Kubernetes
stable/airflow                          5.2.1           1.10.4                          Airflow is a platform to programmatically autho...
stable/ambassador                       5.1.0           0.85.0                          A Helm chart for Datawire Ambassador
stable/anchore-engine                   1.3.7           0.5.2                           Anchore container analysis and policy evaluatio...
stable/apm-server                       2.1.5           7.0.0                           The server receives data from the Elastic APM a...
......

T3.2、示例

为了安装一个 chart 包,我们可以使用 helm install 命令,Helm 有多种方法来找到和安装 chart 包,但是最简单的方法当然是使用官方的 stable 这个仓库直接安装:

首先从仓库中将可用的 charts 信息同步到本地,可以确保我们获取到最新的 charts 列表:

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈

比如我们现在安装一个 mysql 应用:

$ helm install stable/mysql --generate-name
NAME: mysql-1575619811
LAST DEPLOYED: Fri Dec  6 16:10:14 2019
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
MySQL can be accessed via port 3306 on the following DNS name from within your cluster:
mysql-1575619811.default.svc.cluster.local
......

我们可以看到 stable/mysql 这个 chart 已经安装成功了,我们将安装成功的这个应用叫做一个 release,由于我们在安装的时候指定了 --generate-name 参数,所以生成的 release 名称是随机生成的,名为 mysql-1575619811。我们可以用下面的命令来查看 release 安装以后对应的 Kubernetes 资源的状态:

$ kubectl get all -l release=mysql-1575619811
NAME                                    READY   STATUS    RESTARTS   AGE
pod/mysql-1575619811-8479b5b796-dgggz   0/1     Pending   0          27m

NAME                       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/mysql-1575619811   ClusterIP   10.106.141.228   <none>        3306/TCP   27m

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/mysql-1575619811   0/1     1            0           27m

NAME                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/mysql-1575619811-8479b5b796   1         1         0       27m

我们也可以 helm show chart 命令来了解 MySQL 这个 chart 包的一些特性:

$ helm show chart stable/mysql
......

如果想要了解更多信息,可以用 helm show all 命令:

$ helm show all stable/mysql
......

需要注意的是无论什么时候安装 chart,都会创建一个新的 release,所以一个 chart 包是可以多次安装到同一个集群中的,每个都可以独立管理和升级。

同样我们也可以用 Helm 很容易查看到已经安装的 release:

$ helm ls
NAME                NAMESPACE   REVISION    UPDATED                                 STATUS      CHART       APP VERSION
mysql-1575619811    default     1           2019-12-06 16:10:14.682302 +0800 CST    deployed    mysql-1.5.0 5.7.27

如果需要删除这个 release,也很简单,只需要使用 helm uninstall 命令即可:

$ helm uninstall mysql-1575619811
release "mysql-1575619811" uninstalled
$ kubectl get all -l release=mysql-1575619811
No resources found.
$ helm status mysql-1575619811
Error: release: not found

uninstall 命令会从 Kubernetes 中删除 release,也会删除与 release 相关的所有 Kubernetes 资源以及 release 历史记录。也可以在删除的时候使用 --keep-history 参数,则会保留 release 的历史记录,可以获取该 release 的状态就是 UNINSTALLED,而不是找不到 release 了:

$ helm uninstall mysql-1575619811 --keep-history
release "mysql-1575619811" uninstalled
$ helm status mysql-1575619811
helm status mysql-1575619811
NAME: mysql-1575619811
LAST DEPLOYED: Fri Dec  6 16:47:14 2019
NAMESPACE: default
STATUS: uninstalled
...
$ helm ls -a
NAME                NAMESPACE   REVISION    UPDATED                                 STATUS      CHART       APP VERSION
mysql-1575619811    default     1           2019-12-06 16:47:14.415214 +0800 CST    uninstalled mysql-1.5.0 5.7.27

因为 Helm 会在删除 release 后跟踪你的 release,所以你可以审查历史,甚至取消删除 release(使用 helm rollback 命令)。

T3.3、定制

上面我们都是直接使用 helm install 命令安装 chart 包,这种情况下只会使用 chart 的默认配置选项,但是更多的时候,是各种各样的需求,所以我们希望根据自己的需求来定制 chart 包的配置参数。

我们可以使用 helm show values 命令来查看一个 chart 包的所有可配置的参数选项:

$ helm show values stable/mysql
## mysql image version
## ref: https://hub.docker.com/r/library/mysql/tags/
##
image: "mysql"
imageTag: "5.7.14"

busybox:
  image: "busybox"
  tag: "1.29.3"

testFramework:
  enabled: true
  image: "dduportal/bats"
  tag: "0.4.0"

## Specify password for root user
##
## Default: random 10 character string
# mysqlRootPassword: testing

## Create a database user
##
# mysqlUser:
## Default: random 10 character string
# mysqlPassword:

## Allow unauthenticated access, uncomment to enable
##
# mysqlAllowEmptyPassword: true

## Create a database
##
# mysqlDatabase:

## Specify an imagePullPolicy (Required)
## It's recommended to change this to 'Always' if the image tag is 'latest'
## ref: http://kubernetes.io/docs/user-guide/images/#updating-images
##
imagePullPolicy: IfNotPresent
......

上面我们看到的所有参数都是可以用自己的数据来覆盖的,可以在安装的时候通过 YAML 格式的文件来传递这些参数:

$ cat config.yaml
mysqlUser:
  user0
mysqlPassword: user0pwd
mysqlDatabase: user0db
persistence:
  enabled: false
$ helm install -f config.yaml stable/mysql
helm install -f config.yaml mysql stable/mysql
NAME: mysql
LAST DEPLOYED: Fri Dec  6 17:46:56 2019
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
MySQL can be accessed via port 3306 on the following DNS name from within your cluster:
mysql.default.svc.cluster.local
......

release 安装成功后,可以查看对应的 Pod 信息:

$ kubectl get pod -l release=mysql
NAME                    READY   STATUS            RESTARTS   AGE
mysql-ddd798f48-gnrzd   0/1     PodInitializing   0          119s
$ kubectl describe pod  mysql-ddd798f48-gnrzd
......
Environment:
      MYSQL_ROOT_PASSWORD:  <set to the key 'mysql-root-password' in secret 'mysql'>  Optional: false
      MYSQL_PASSWORD:       <set to the key 'mysql-password' in secret 'mysql'>       Optional: false
      MYSQL_USER:           user0
      MYSQL_DATABASE:       user0db
......

可以看到环境变量 MYSQL_USER=user0,MYSQL_DATABASE=user0db 的值和我们上面配置的值是一致的。在安装过程中,有两种方法可以传递配置数据:

  • --values(或者 -f):指定一个 YAML 文件来覆盖 values 值,可以指定多个值,最后边的文件优先。
  • --set:在命令行上指定配置进行覆盖。

如果同时使用这两个参数,--values(-f) 将被合并到具有更高优先级的 --set,使用 --set 指定的值将持久化在 ConfigMap 中,对于给定的 release,可以使用 helm get values <release-name> 来查看已经设置的值,已设置的值也通常允许 helm upgrade 指定 --reset 值来清除。

--set 选项接收零个或多个 name/value 对,最简单的用法就是 --set name=value,相当于 YAML 文件中的:

name: value

多个值之间用 , 隔开,用法就是 --set a=b,c=d,相当于 YAML 文件中的:

a: b
c: d

也支持更加复杂的表达式,例如 --set outer.inner=value,对应 YAML:

outer:
  inner: value

对于列表数组可以用 来包裹,比如 --set name={a, b, c},对应 YAML:

name:
 - a
 - b
 - c

从 Helm 2.5.0 开始,就可以使用数组索引语法来访问列表中某个项,比如 --set servers[0].port=80,对应的 YAML 为:

servers:
 - port: 80

也可以这样设置多个值,比如 --set servers[0].port=80,servers[0].host=example,对应的 YAML 为:

servers
  - port: 80
    host: example

有时候你可能需要在 --set 选项中使用特殊的字符,这个时候可以使用反斜杠来转义字符,比如 --set name=value1\,value2,对应的 YAML 为:

name: "value1,value2"

类似的,你还可以转义 .,当 chart 模板中使用 toYaml 函数来解析 annotations、labels 以及 node selectors 之类的时候,这非常有用,比如 --set nodeSelector."kubernetes\.io/role"=master,对应的 YAML 文件:

nodeSelector:
  kubernetes.io/role: master

深度嵌套的数据结构可能很难使用 --set 来表示,所以一般推荐还是使用 YAML 文件来进行覆盖,当然在设计 chart 模板的时候也可以结合考虑 --set 这种用法。

更多安装方式,helm install 命令可以从多个源进行安装:

  • chart 仓库(类似于上面我们提到的)
  • 本地 chart 压缩包(helm install foo-0.1.1.tgz)
  • 本地解压缩的 chart 目录(helm install foo path/to/foo)
  • 在线的 URL(helm install fool https://example.com/charts/foo-1.2.3.tgz

T3.4、升级和回滚

当新版本的 chart 包发布的时候,或者当你要更改 release 配置的时候,你可以使用 helm upgrade 命令来操作。升级需要一个现有的 release,并根据提供的信息对其进行升级。因为 Kubernetes charts 可能很大而且很复杂,Helm 会尝试以最小的侵入性进行升级,它只会更新自上一版本以来发生的变化:

$ helm upgrade -f panda.yaml mysql stable/mysql
helm upgrade -f panda.yaml mysql stable/mysql
Release "mysql" has been upgraded. Happy Helming!
NAME: mysql
LAST DEPLOYED: Fri Dec  6 21:06:11 2019
NAMESPACE: default
STATUS: deployed
REVISION: 2
...

我们这里 mysql 这个 release 用相同的 chart 包进行升级,但是新增了一个配置项:

mysqlRootPassword: passw0rd

我们可以使用 helm get values 来查看新设置是否生效:

$ helm get values mysql
USER-SUPPLIED VALUES:
mysqlDatabase: user0db
mysqlPassword: user0pwd
mysqlRootPassword: passw0rd
mysqlUser: user0
persistence:
  enabled: false

helm get 命令是查看集群中 release 非常有用的命令,正如我们在上面看到的,它显示了 panda.yaml 中的新配置值被部署到了集群中,现在如果某个版本在发布期间没有按计划进行,那么可以使用 helm rollback [RELEASE] [REVISION] 命令很容易回滚到之前的版本:

$ helm ls
NAME    NAMESPACE   REVISION    UPDATED                                 STATUS      CHART       APP VERSION
mysql   default     2           2019-12-06 21:06:11.36358 +0800 CST     deployed    mysql-1.5.0 5.7.27
$ helm history mysql
REVISION    UPDATED                     STATUS      CHART       APP VERSION DESCRIPTION
1           Fri Dec  6 17:53:03 2019    superseded  mysql-1.5.0 5.7.27      Install complete
2           Fri Dec  6 21:06:11 2019    deployed    mysql-1.5.0 5.7.27      Upgrade complete
$ helm rollback mysql 1
Rollback was a success! Happy Helming!
$ kubectl get pods -l release=mysql
NAME                    READY   STATUS    RESTARTS   AGE
mysql-ddd798f48-gnrzd   1/1     Running   0          3h25m
$ helm get values mysql
USER-SUPPLIED VALUES:
mysqlDatabase: user0db
mysqlPassword: user0pwd
mysqlUser: user0
persistence:
  enabled: false

可以看到 values 配置已经回滚到之前的版本了。上面的命令回滚到了 release 的第一个版本,每次进行安装、升级或回滚时,修订号都会加 1,第一个修订号始终为1,我们可以使用 helm history [RELEASE] 来查看某个版本的修订号。

除此之外我们还可以指定一些有用的选项来定制 install/upgrade/rollback 的一些行为,要查看完整的参数标志,我们可以运行 helm <command> --help 来查看,这里我们介绍几个有用的参数:

  • --timeout:等待 Kubernetes 命令完成的时间,默认是 300(5分钟)

  • --wait:等待直到所有 Pods 都处于就绪状态、PVCs 已经绑定、Deployments 具有处于就绪状态的最小 Pods 数量(期望值减去 maxUnavailable)以及 Service 有一个 IP 地址,然后才标记 release 为成功状态。

    它将等待与 --timeout 值一样长的时间,如果达到超时,则 release 将标记为失败。

    注意:在 Deployment 中将副本设置为 1 并且作为滚动更新策略的一部分,maxUnavailable 也未设置为 0,--wait 将返回就绪状态,因为它已满足就绪状态下的最小 Pod 数量。

  • --no-hooks:将会跳过命令的运行 hooks。

  • --recreate-pods:仅适用于 upgrade 和 rollback,这个标志将导致重新创建所有的 Pods。(Helm3 中启用了)

T3.5、Charts

Helm 使用一种名为 charts 的包格式,一个 chart 是描述一组相关 Kubernetes 资源的文件集合,单个 chart 可能用于部署简单的应用,比如 memcached pod,或者复杂的应用,比如一个带有 HTTP 服务、数据库、缓存等等功能的完整 web 应用程序。

Charts 是创建在特定目录下面的文件集合,然后可以将它们打包到一个版本化的存档中用来部署。接下来我们就来看看使用 Helm 构建 charts 的一些基本方法。

T3.5.1、文件结构

chart 是一个目录中的文件集合,目录名称就是 chart 的名称(不包含版本信息),下面是一个 WordPress 的 chart,会被存储在 wordpress/ 目录下面,基本结构如下所示:

wordpress/
  Chart.yaml          # 包含当前 chart 信息的 YAML 文件
  LICENSE             # 可选:包含 chart 的 license 文本文件
  README.md           # 可选:一个可读性高的 README 文件
  values.yaml         # 当前 chart 的默认配置 values
  values.schema.json  # 可选: 一个作用在 values.yaml 文件上的 JSON 模式
  charts/             # 包含该 chart 依赖的所有 chart 的目录
  crds/               # Custom Resource Definitions
  templates/          # 模板目录,与 values 结合使用时,将渲染生成 Kubernetes 资源清单文件
  templates/NOTES.txt # 可选: 包含简短使用使用的文本文件

T3.5.2、Chart.yaml 文件

对于一个 chart 包来说 Chart.yaml 文件是必须的,它包含下面的这些字段:

apiVersion: chart API 版本 (必须)
name: chart 名 (必须)
version: SemVer 2版本 (必须)
kubeVersion: 兼容的 Kubernetes 版本 (可选)
description: 一句话描述 (可选)
type: chart 类型 (可选)
keywords:
  - 当前项目关键字集合 (可选)
home: 当前项目的 URL (可选)
sources:
  - 当前项目源码 URL (可选)
dependencies: # chart 依赖列表 (可选)
  - name: chart 名称 (nginx)
    version: chart 版本 ("1.2.3")
    repository: 仓库地址 ("https://example.com/charts")
maintainers: # (可选)
  - name: 维护者名字 (对每个 maintainer 是必须的)
    email: 维护者的 email (可选)
    url: 维护者 URL (可选)
icon: chart 的 SVG 或者 PNG 图标 URL (可选).
appVersion: 包含的应用程序版本 (可选). 不需要 SemVer 版本
deprecated: chart 是否已被弃用 (可选, boolean)

其他字段默认会被忽略。

T3.5.2.1、版本

每个 chart 都必须有一个版本号,版本必须遵循 SemVer2 标准,和 Helm Classic 不同,Kubernetes Helm 使用版本号作为 release 的标记,仓库中的软件包通过名称加上版本号来标识的。

例如,将一个 nginx 的 chart 包 version 字段设置为:1.2.3,则 chart 最终名称为:

nginx-1.2.3.tgz

还支持更复杂的 SemVer2 名称,例如版本:1.2.3-alpha.1+ef365,但是需要注意的是系统明确禁止使用非 SemVer 的名称。

Chart.yaml 中的 version 字段被很多 Helm 工具使用,包括 CLI 工具,生成包的时候,命令 helm package 将使用该字段作为包名称中的标记,系统是默认 Chart 包中的版本号与 chart.yaml 中的版本号相匹配的,如果不匹配的话就导致一系列错误。

T3.5.2.2、apiVersion 字段

对于 Helm 3 以上的版本 apiVersion 字段应该是 v2,之前版本的 Chart 应该设置为 1,并且也可以由 Helm 3 进行安装。

T3.5.2.3、appVersion 字段

要注意 appVersion 字段与 version 字段无关,这是一种指定应用程序版本的方法,比如 drupal 的 Chart 包可能有一个 appVersion: 8.2.1 的字段,表示 Chart 中包含的 drupal 版本是 8.2.1,该字段仅供参考,对 Chart 版本的计算不会产生影响。

T3.5.2.4、弃用 Charts

当在 Chart 仓库中管理 charts 的时候,有时候需要弃用一个 chart,Chart.yaml 中的可选字段 deprecated 可以用来标记一个 chart 为弃用状态。如果将仓库中最新版本的 chart 标记为弃用,则整个 chart 都会被当做弃用状态了。以后可以通过发布一个未被标记为弃用状态的新版本来重新使用该 chart。弃用 charts 的工作流程如下所示:

  • 更新 chart 的 Chart.yaml 来标记 chart 为弃用状态
  • 发布该新版本到 Chart 仓库
  • 从源码仓库(比如 git)中删除 chart

T3.5.2.5、Chart 类型

type 字段定义 chart 的类型,可以定义两种类型:应用程序(application)和库(library)。应用程序是默认的类型,它是一个可以完整操作的标准 chart,库或者辅助类 chart 为 chart 提供了一些实用的功能,library 不同于应用程序 chart,因为它没有资源对象,所以无法安装。

注意:一个应用 chart 也可以当作库进行使用。通过将类型设置为 library,然后该 chart 就会渲染成一个库,可以在其中使用所有的实用性功能,chart 的所有资源对象都不会被渲染。

T3.5.2.6、LICENSE, README 和 NOTES

Chart 还可以包含用于描述 chart 的安装、配置、用法和许可证书的文件。

LICENSE 是一个纯文本文件,其中包含 chart 的许可证书。chart 可以包含一个许可证书,因为它可能在模板中具有编程逻辑,所以不只是配置,如果需要,chart 还可以为应用程序提供单独的 license(s)。

Chart 的 README 文件应该采用 Markdown(README.md)格式,并且通常应该包含如下的一些信息:

  • chart 提供的应用程序的描述信息
  • 运行 chart 的任何先决条件或者要求
  • values.yaml 和默认值中的一些选项说明
  • 与 chart 的安装或配置有关的任何其他信息

chart 还可以包含简短的纯文本模板或者 NOTES.txt 文件,该文件将在安装后以及查看 release 状态的时候打印出来。该文件会被当成模板文件,并且可以用作使用说明,关于后续步骤或与 release 有关的任何其他信息。例如,可以提供用于连接到数据或访问 Web UI 的指令。由于在运行 helm install 或者 helm status 的时候该文件会打印到 STDOUT 中,所以建议该文件内容保持简短,然后可以指向 README 文件来获取更多详细信息。

T3.5.2.7、依赖

在 Helm 中,一个 chart 包可能会依赖许多其他的 chart。这些依赖关系可以使用 Chart.yaml 中的依赖关系字段动态链接,也可以引入到 charts/ 目录手动进行管理。

1、使用 dependencies 字段管理依赖

当前 chart 所需的依赖 chart 需要在 dependencies 字段中进行定义,如下所示:

dependencies:
  - name: apache
    version: 1.2.3
    repository: https://example.com/charts
  - name: mysql
    version: 3.2.1
    repository: https://another.example.com/charts
  • name 字段是所依赖的 chart 的名称
  • version 字段是依赖的 chart 版本
  • repository 字段是 chart 仓库的完整 URL,不过需要注意,必须使用 helm repo add 在本地添加该 repo

定义了依赖项后,可以运行 helm dependency update 来更新依赖项,它将根据你的依赖项文件把你所有指定的 chart 包下载到 charts/ 目录中:

$ helm dependency update foochart
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "local" chart repository
...Successfully got an update from the "stable" chart repository
...Successfully got an update from the "example" chart repository
...Successfully got an update from the "another" chart repository
Update Complete. Happy Helming!
Saving 2 charts
Downloading apache from repo https://example.com/charts
Downloading mysql from repo https://another.example.com/charts

当执行 helm dependency update 命令的时候会解析 chart 的依赖项,依赖的 chart 包会被下载存放到 charts/ 目录中,所以,对于上面的示例,我们可以在 charts 目录中看到如下的文件:

charts/
  apache-1.2.3.tgz
  mysql-3.2.1.tgz

2、alias 字段

除了上面的几个字段之外,每个依赖项还可以包含一个可选的 alias 别名字段。为依赖 chart 添加别名,将使用别名作为依赖的名称。在处理同名 chart 时,就可以使用别名,如下所示:

# parentchart/Chart.yaml

dependencies:
  - name: subchart
    repository: http://localhost:10191
    version: 0.1.0
    alias: new-subchart-1
  - name: subchart
    repository: http://localhost:10191
    version: 0.1.0
    alias: new-subchart-2
  - name: subchart
    repository: http://localhost:10191
    version: 0.1.0

在上面示例中,我们将获得 3 个依赖项:

subchart
new-subchart-1
new-subchart-2

其实我们也可以手动来实现,将同一个 chart 以不同的名称多次复制/粘贴到 charts/ 目录中也是可以的。

T3.5.2.8、TEMPLATES 和 VALUES

Helm Chart 模板是用 Go template 语言 进行编写的,另外还额外增加了 Sprig 库中的 50 个左右的附加模板函数和一些其他专用函数

所有模板文件都存储在 chart 的 templates/ 目录下面,当 Helm 渲染 charts 的时候,它将通过模板引擎传递该目录中的每个文件。模板的 Values 可以通过两种方式提供:

  • Chart 开发人员可以在 chart 内部提供一个名为 values.yaml 的文件,该文件可以包含默认的 values 值内容。
  • Chart 用户可以提供包含 values 值的 YAML 文件,可以在命令行中通过 helm install 来指定该文件。

当用户提供自定义 values 值的时候,这些值将覆盖 chart 中 values.yaml 文件中的相应的值。

1、模板文件

模板文件遵循编写 Go 模板的标准约定(参考 text/template 包文档),下面是一个模板文件示例:

apiVersion: v1
kind: ReplicationController
metadata:
  name: deis-database
  namespace: deis
  labels:
    app.kubernetes.io/managed-by: deis
spec:
  replicas: 1
  selector:
    app.kubernetes.io/name: deis-database
  template:
    metadata:
      labels:
        app.kubernetes.io/name: deis-database
    spec:
      serviceAccount: deis-database
      containers:
        - name: deis-database
          image: {{ .Values.imageRegistry }}/postgres:{{ .Values.dockerTag }}
          imagePullPolicy: {{ .Values.pullPolicy }}
          ports:
            - containerPort: 5432
          env:
            - name: DATABASE_STORAGE
              value: {{ default "minio" .Values.storage }}

上面这个示例是 Kubernetes replication 控制器的一个模板,它可以使用以下 4 个模板值(通常在 values.yaml 文件中定义的):

  • imageRegistry:Docker 镜像仓库
  • dockerTag:Docker 镜像 tag
  • pullPolicy:镜像拉取策略
  • storage:存储后端,默认设置为 "minio"

所有的 values 值都是由模板作者来定义的,Helm 不会也不需要规定这些参数。参考 Kubernetes Charts 项目去了解更多的 charts 项目的详细内容。

2、预定义 Values

在模板中用 .Values 可以获取到 values.yaml 文件(或者 --set 参数)提供的 values 值,此外,还可以在模板中访问其他预定义的数据。下面是一些预定义的、可用于每个模板、并且不能被覆盖的 values 值,与所有 values 值一样,名称都是区分大小写的:

  • Release.Name:release 的名称(不是 chart)
  • Release.Namespace:release 被安装到的命名空间
  • Release.Service:渲染当前模板的服务,在 Helm 上,实际上该值始终为 Helm
  • Release.IsUpgrade:如果当前操作是升级或回滚,则该值为 true
  • Release.IsInstall:如果当前操作是安装,则该值为 true
  • ChartChart.yaml 文件的内容,可以通过 Chart.Version 来获得 Chart 的版本,通过 Chart.Maintainers 可以获取维护者信息
  • Files: 一个包含 chart 中所有非特殊文件的 map 对象,这不会给你访问模板的权限,但是会给你访问存在的其他文件的权限(除非使用 .helmignore 排除它们),可以使用 { index .Files "file.name" } 或者 { .Files.Get name } 或者 { .Files.GetString name } 函数来访问文件,你还可以使用 { .Files.GetBytes }[]byte 的形式获取访问文件的内容
  • Capabilities:也是一个类 map 的对象,其中包含有关 Kubernetes 版本({ .Capabilities.KubeVersion })和支持的 Kubernetes API 版本({ .Capabilities.APIVersions.Has "batch/v1" })信息。

注意:任何未知的 Chart.yaml 字段都会被删除,在 Chart 对象内部无法访问他们,所以,Chart.yaml 不能用于将任意结构化的数据传递到模板中,但是可以使用 values 文件来传递。

3、Values 文件

为模板提供一些必需值的 values.yaml 文件如下所示:

imageRegistry: "quay.io/deis"
dockerTag: "latest"
pullPolicy: "Always"
storage: "s3"

values 文件的格式是 YAML,一个 chart 包可能包含一个默认的 values.yaml 文件,helm install 命令允许用户通过提供其他的 YAML 值文件来覆盖默认的值:

$ helm install --values=myvals.yaml wordpress

用这种方式来传递 values 值的时候,它们将合并到默认值文件中,比如有一个 myvals.yaml 文件如下所示:

storage: "gcs"

将其与 chart 的 values.yaml 文件合并后,得到的结果为:

imageRegistry: "quay.io/deis"
dockerTag: "latest"
pullPolicy: "Always"
storage: "gcs"

我们可以看到只有最后一个字段被覆盖了。

注意:chart 内包含的默认 values 文件必须命名为 values.yaml,但是在命令行上指定的文件可以任意命名。 如果在 helm install 或者 helm upgrade 的时候使用 --set 参数,则这些值将在客户端转换为 YAML 格式。 如果 values 文件存在任何必须的条目,则可以使用 required 函数在 chart 模板中将它们声明为必须选项。

然后我们就可以使用 .Values 对象在模板中访问任意一个 values 值,类似于下面的模板文件:

apiVersion: v1
kind: ReplicationController
metadata:
  name: deis-database
  namespace: deis
  labels:
    app.kubernetes.io/managed-by: deis
spec:
  replicas: 1
  selector:
    app.kubernetes.io/name: deis-database
  template:
    metadata:
      labels:
        app.kubernetes.io/name: deis-database
    spec:
      serviceAccount: deis-database
      containers:
        - name: deis-database
          image: {{ .Values.imageRegistry }}/postgres:{{ .Values.dockerTag }}
          imagePullPolicy: {{ .Values.pullPolicy }}
          ports:
            - containerPort: 5432
          env:
            - name: DATABASE_STORAGE
              value: {{ default "minio" .Values.storage }}

4、作用范围、依赖和 Values

values 文件可以为 chart 以及他的任何依赖项提供 values 值。例如,上面提到了 WordPress 这个 chart 同时依赖 mysql 和 apache 这两个依赖项,values 文件可以为所有这些组件提供 values 值:

title: "My WordPress Site" # 传递到 WordPress 模板

mysql:
  max_connections: 100 # 传递到 MySQL
  password: "secret"

apache:
  port: 8080 # 传递到 Apache

较高级别的 Charts 可以访问下面定义的所有变量,所以,WordPress 这个 chart 可以通过 .Values.mysql.password 来访问 MySQL 的密码,但是较低级别的 chart 是无法访问父 chart 中的内容的,所以 MySQL 无法获取到 title 属性,当然同样也不能访问 apache.port

Values 是有命名空间概念的,比如对于 WordPress 这个 chart 来说,它可以通过 .Values.mysql.password 来进行访问,但是对于 MySQL 这个 chart 本身来说,values 的范围缩小了,命名空间前缀会被删除,所以它只需要通过 .Values.password 就可以访问到。

5、全局 Values

2.0.0-Alpha.2 版本开始,Helm 开始支持特殊的 global 全局值,比如将上面的示例修改如下:

title: "My WordPress Site" # 传递到 WordPress 模板

global:
  app: MyWordPress

mysql:
  max_connections: 100 # 传递到 MySQL
  password: "secret"

apache:
  port: 8080 # 传递到 Apache

上面我们添加了一个全局范围的 value 值:app: MyWordPress,该值可以通过 .Values.global.app 提供给所有 chart 使用。

例如,mysql 模板可以用 { .Values.global.app } 来访问 app,apache chart 也可以,实际上,上面的 values 文件会这样重新生成:

title: "My WordPress Site" # 传递到 WordPress 模板

global:
  app: MyWordPress

mysql:
  global:
    app: MyWordPress
  max_connections: 100 # 传递到 MySQL
  password: "secret"

apache:
  global:
    app: MyWordPress
  port: 8080 # 传递到 Apache

这提供了一种与所有子 chart 共享一个顶级变量的方式,这对于设置 meta 数据这种属性是非常有用的。如果子 chart 声明了全局变量,则该全局变量将向下(传递到子 chart 的子 chart 中)传递,而不会向上传递到父 chart,子 chart 无法影响父 chart 的值。同样,父 chart 的全局遍历优先于子 chart 中的全局变量。

6、Schema 文件

有时候,chart 开发者可能希望在其 values 值上面定义一个结构,这种情况下可以通过在 values.schema.json 文件中定义一个 schema 来完成,这里的 schema 就是一个 JSON Schema 文件结构规范,如下所示:

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "properties": {
    "image": {
      "description": "Container Image",
      "properties": {
        "repo": {
          "type": "string"
        },
        "tag": {
          "type": "string"
        }
      },
      "type": "object"
    },
    "name": {
      "description": "Service name",
      "type": "string"
    },
    "port": {
      "description": "Port",
      "minimum": 0,
      "type": "integer"
    },
    "protocol": {
      "type": "string"
    }
  },
  "required": [
    "protocol",
    "port"
  ],
  "title": "Values",
  "type": "object"
}

该 schema 会对 values 值进行校验,调用以下任何命令时,都会进行验证:

  • helm install
  • helm upgrade
  • helm lint
  • helm template

比如下面的示例文件就可以满足上面的 schema 要求:

name: frontend
protocol: https
port: 443

需要注意的是该 schema 将应用于最终的 .Values 对象,而不仅仅是应用于 values.yaml 文件,所以下面的文件也是可以满足 schema 要求的:

name: frontend
protocol: https

因为在安装的时候我们通过 --set 选项传递了必须的 port 属性:

$ helm install --set port=443

此外,还会根据所有的子 chart schemas 来检查最终的 .Values 对象,这意味着父 chart 无法规避对子 chart 的限制。同样的,如果子 chart 要求未满足子 chart 的 values.yaml 文件,则父 chart 必须满足这些限制才能生效。

7、参考文档

在编写模板、values、和 schema 文件的时候,下面这些文档可以提供一些帮助:

T3.5.2.9、CRDS

Kubernetes 提供了一种声明新类型对象的机制,使用 CustomResourceDefinitions(CRDS)可以让 Kubernetes 开发人员声明自定义资源类型。

在 Helm 3 中,CRD 被视为一种特殊的对象,它们会优先于其它 chart 部分被安装,并且会受到一些限制。CRD YAML 文件应该放在 chart 内的 crds/ 目录下面。多个 CRDs 可以放在同一个文件中,Helm 会尝试将 CRD 目录中的所有文件加载到 Kubernetes 中。

需要注意的是 CRD 文件不能模板化,它们必须是纯的 YAML 文件。

当 Helm 安装一个新的 chart 的时候,会优先安装 CRDs,然后会暂停,直到 API Server 提供 CRD 为止,然后才开始启动模板引擎,渲染其余的 chart 模板,进而安装到 Kubernetes 中。

由于这个安装顺序的原因,CRD 信息在 Helm 模板的 .Capabilities 对象中是可以获取到的,并且 Helm 模板中可以定义 CRD 中声明的资源类型。

比如,如果你的 chart 在 crds 目录下面有一个 CronTab 的 CRD,则可以在 templates/ 目录下面创建 CronTab 资源类型的实例:

crontabs/
  Chart.yaml
  crds/
    crontab.yaml
  templates/
    mycrontab.yaml

crontab.yaml 文件只能定义不带模板的 CRD 资源:

kind: CustomResourceDefinition
metadata:
  name: crontabs.stable.example.com
spec:
  group: stable.example.com
  versions:
    - name: v1
      served: true
      storage: true
  scope: Namespaced
  names:
    plural: crontabs
    singular: crontab
    kind: CronTab

然后模板 mycrontab.yaml 可以创建一个新的 CronTab:

apiVersion: stable.example.com
kind: CronTab
metadata:
  name: {{ .Values.name }}
spec:
   # ...

在继续安装 templates/ 之前,Helm 会确保 CRD 定义生效,也就是说可以从 Kubernetes API server 上获得 CRD 定义的 CronTab 资源类型。

CRDs 的限制:

于 Kubernetes 中的大多数对象不同,CRDs 是全局安装的,所以 Helm 在管理 CRD 的时候比较谨慎,会有一些限制:

  • CRDs 不会重新安装,如果 Helm 确定 crds/ 目录中的 CRD 已经存在(无论版本如何),Helm 都不会重新安装或升级。
  • CRDs 不会在升级或回滚的时候安装,只会在安装操作的时候创建 CRDs。
  • CRDs 不会被删除,删除 CRD 会自动删除集群中所有 namespace 中的 CRDs 内容,所以 Helm 不会删除 CRD。

Helm 希望想要升级或删除 CRDs 的操作人员可以手动来仔细地操作。

T3.5.2.10、使用 Helm 管理 Charts

helm 工具有几个用于操作 charts 的命令,如下所示。

创建一个新的 chart 包:

$ helm create mychart
Created mychart/

如果你编辑完了一个 chart 包,Helm 可以将其打包到一个独立文件中:

$ helm package mychart
Archived mychart-0.1.-.tgz

你还可以使用 helm 帮助你查找 chart 包的格式问题:

$ helm lint mychart
No issues found

T3.5.2.11、Chart 仓库

chart 仓库实际上就是一个 HTTP 服务器,其中包含一个或多个打包的 chart 包,虽然可以使用 helm 来管理本地 chart 目录,但是在共享 charts 的时候,最好的还是使用 chart 仓库。

可以提供 YAML 文件和 tar文件并且可以响应 GET 请求的任何 HTTP 服务器都可以作为 chart 仓库服务器。仓库的主要特征是存在一个名为 index.yaml 的特殊文件,该文件记录仓库中提供的所有软件包的列表,以及允许检索和验证这些软件包的元数据。

Helm 客户端提供了管理仓库的功能,但不提供上传 Chart 到远程仓库的功能。

作为 Helm 客户端用户,你可以使用一系列 helm repo 命令来操作 Chart 仓库

  • helm repo add:添加一个远程仓库(例如,添加官方的 bitnami 仓库)。
  • helm repo list:列出所有已配置的仓库。
  • helm repo update:从远程仓库获取最新的 Chart 索引信息。
  • helm repo remove:移除一个仓库。

那么,如何上传 Chart 到远程仓库?

这完全取决于你使用的 Chart 仓库服务器类型。你需要使用该服务器提供的特定工具或方法:

  1. HTTP/HTTPS 服务器 + index.yaml 文件(最经典和通用的方式):

    • 你需要一个能托管静态文件的 Web 服务器(如 Nginx、Apache、AWS S3、Google Cloud Storage、GitHub Pages 等)。
    • 上传流程
      a. 使用 helm package 命令把你的 Chart 目录打包成一个 .tgz 文件。
      b. 将这个 .tgz 文件手动(或用 CI/CD 脚本)上传到你的 Web 服务器的特定目录。
      c. 在该目录下,使用 helm repo index 命令生成或更新 index.yaml 文件(这个文件是所有可用 Chart 的索引清单)。
      d. 将更新后的 index.yaml 文件也上传到服务器。
    • 别人就可以用 helm repo add <你的仓库名> <你的服务器URL> 来添加你的仓库了。
  2. 专门的 Chart 仓库服务器

    • Harbor:一个流行的云原生制品仓库,原生支持 Helm Chart。它提供 Web 界面和 API 来上传和管理 Chart。你可以使用 helm cm-push 插件(来自 helm-push 插件)来推送 Chart 到 Harbor。

    • ChartMuseum:一个专用的、开源的 Helm Chart 仓库服务器。它也提供了自己的 CLI 工具和 API 来上传 Chart,通常也使用 helm-push 插件。

    • OCI 注册中心:从 Helm 3.8+ 开始,Helm 全面支持将 Chart 作为 OCI 制品存储在与 Docker 镜像兼容的 OCI 注册中心(如 AWS ECR, Azure ACR, Google Artifact Registry, Docker Hub 等)。对于 OCI,你可以使用 Helm 客户端的原生命令:

      # 将 Chart 保存为 OCI 镜像
      helm package mychart/
      helm push mychart-1.0.0.tgz oci://<你的注册中心地址>/charts

T3.6、模板开发

T3.6.1、内置对象

前面我们介绍了 Helm Chart 的一些基本概念和使用,接下来我们重点介绍下 Chart 模板的编写。模板会被渲染成 Kubernetes 的资源清单文件,下面我们将来学习下模板的结构,如何使用它们,如何编写 Go 模板以及如何调试。

对象从模板引擎传递到模板中,在代码中可以传递对象,也可以在模板宏中创建新的对象,比如 tuple 函数。对象可以很简单,也可以包含其他对象或函数,例如,Release 对象就包含几个对象(比如 Release.Name),Files 对象就包含几个函数。

前面提到过我们可以在模板中使用 { .Release.Name } 获取 release 的名称,Release 是我们可以在模板中访问的几个顶级对象之一:

  • Release:该对象描述了 release 本身的相关信息,它内部有几个对象:
    • Release.Name:release 名称
    • Release.Namespace:release 安装所在的命名空间
    • Release.IsUpgrade:如果当前操作是升级或回滚,则该值为 true
    • Release.IsInstall:如果当前操作是安装,则将其设置为 true
    • Release.Revision:release 的 revision 版本号,在安装的时候,值为1,每次升级或回滚都会增加
    • Release.Service:渲染当前模板的服务,在 Helm 上,实际上该值始终为 Helm
  • Values:从 values.yaml 文件和用户提供的 values 文件传递到模板的 Values 值,默认情况下,Values 是空的。
  • Chart:获取 Chart.yaml 文件的内容,该文件中的任何数据都可以访问,例如 { .Chart.Name }}-{{ .Chart.Version} 可以渲染成 mychart-0.1.0,该对象下面可用的字段前面我们已经提到过了。
  • Files:可以访问 chart 中的所有非特殊文件,虽然无法使用它来访问模板文件,但是可以来访问 chart 中的其他文件。
    • Files.Get:用于根据名称获取文件(比如 .Files.Get config.ini
    • Files.GetBytes:用于以 bytes 数组而不是字符串的形式来获取文件内容,这对于类似于图片之类的东西很有用
    • Files.Glob:用于返回 shell glob 模式匹配的文件名称列表
    • Files.Lines:可以逐行读取文件,对于遍历文件中的每行内容很有用
    • Files.AsSecrets:将文件内容以 Base64 编码的字符串返回
    • Files.AsConfig:将文件正文作为 YAML 字典返回
  • Capabilities:获取有关 Kubernetes 集群支持的相关信息
    • Capabilities.APIVersions:支持的版本集合
    • Capabilities.APIVersions.Has $version:判断一个版本(比如 batch/v1)或资源(比如 apps/v1/Deployment)是否可用
    • Capabilities.Kube.Version:Kubernetes 的版本
    • Capabilities.Kube:Kubernetes 版本的缩写
    • Capabilities.Kube.Major:Kubernetes 主版本
    • Capabilities.Kube.Minor:Kubernetes 的次版本
  • Template:包含当前正在执行的模板的相关信息
    • Name:当前模板的命名空间文件路径(比如 mychart/templates/mytemplate.yaml
    • BaePath:当前 chart 模板目录的命名空间路径(比如 mychart/templates

需要注意的是内置的对象始终是以大写字母开头的,这也符合 Go 的命名约定,创建自己名称的时候,可以使用适合你团队的约定,比如 Kubernetes Charts 团队,选择仅使用首字母小写,以区分本地名称和内置名称,这里我们也会遵循该约定。

T3.6.2、Values 文件

前面我们介绍了 Helm 模板提供的内置对象,其中就有一个内置对象 Values,该对象提供对传递到 chart 中的 values 值的访问,其内容主要有 4 个来源:

  • chart 包中的 values.yaml 文件
  • 如果这是子 chart,父 chart 的 values.yaml 文件
  • -f 参数传递给 helm installhelm upgrade 的 values 值文件(例如 helm install -f myvals.yaml ./mychart
  • --set 传递的各个参数(例如 helm install --set foo=bar ./mychart

values.yaml 文件是默认值,可以被父 chart 的 values.yaml 文件覆盖,而后者又可以由用户提供的 values 值文件覆盖,用户指定的 values 值文件又可以被 --set 参数覆盖。

values 值文件是纯 YAML 文件,我们可以来编辑 mychart/values.yaml 文件,然后编辑 ConfigMap 模板。删除 values.yaml 中的默认设置后,我们将只设置一个参数:

favoriteDrink: coffee

现在我们可以在模板中直接使用它:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favoriteDrink }}

可以看到在最后一行我们将 favoriteDrink 作为 Values 的属性进行访问:{ .Values.favoriteDrink }。我们可以来看看是如何渲染的:

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

NAME: mychart-1575963545
LAST DEPLOYED: Tue Dec 10 15:39:06 2019
NAMESPACE: default
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: mychart-1575963545-configmap
data:
  myvalue: "Hello World"
  drink: coffee

由于在默认的 values.yaml 文件中将 favoriteDrink 设置为了 coffee,所以这就是模板中显示的值,我们可以通过在调用 helm install 的过程中添加 --set 参数来覆盖它:

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

NAME: mychart-1575963760
LAST DEPLOYED: Tue Dec 10 15:42:43 2019
NAMESPACE: default
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: mychart-1575963760-configmap
data:
  myvalue: "Hello World"
  drink: slurm

因为 --set 的优先级高于默认的 values.yaml 文件,所以我们的模板会生成 drink: slurm。Values 值文件也可以包含更多结构化的内容,例如我们可以在 values.yaml 文件中创建一个 favorite 的部分,然后在其中添加几个 keys:

favorite:
  drink: coffee
  food: pizza

现在我们再去修改下我们的模板:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink }}
  food: {{ .Values.favorite.food }}

虽然我们可以通过这种方式来构造数据,但还是建议你将 values 树保持更浅,这样在使用的时候更加简单。

T3.6.2.1、删除默认 KEY

如果你需要从默认值中删除 key,则可以将该 key 的值覆盖为 null,在这种情况下,Helm 将从覆盖的 values 中删除该 key。例如,在 Drupal chart 中配置一个 liveness 探针:

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

如下我们本想使用 --set livenessProbe.exec.command=[cat, docroot/CHANGELOG.txt] 将 livenessProbe 的处理程序覆盖为 exec 而不是 httpGet,但是 Helm 并不会覆盖而是让二者并存,如下所示:

livenessProbe:
  httpGet:
    path: /user/login
    port: http
  exec:
    command:
    - cat
    - docroot/CHANGELOG.txt
  initialDelaySeconds: 120

这样就达不到我们的预期,而且有问题,因为你不能声明多个 livenessProbe 处理程序,为了解决这个问题,你可以让 Helm 通过将 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

T3.6.3、函数和管道

现在我们已经了解了如何将信息加入到模板中,但是这些信息都是直接原样植入,有的时候,我们希望转换提供的数据。

下面让我们从一个最佳实践开始:将 .Values 对象中的字符串注入模板时,我们可以通过在 template 中调用 quote 函数来处理包含特殊字符的字符串,比如:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ quote .Values.favorite.drink }} # 如果传过来的值是orange juice,quote会加个引号,不然报错了
  food: {{ quote .Values.favorite.food }} # 如果传过来的值是空,quote会显示"",否则什么都没有也会报错

模板函数遵循的语法规则是 functionName arg1 arg2...,在上面的代码片段中,quote .Values.favorite.drink 会调用 quote 函数并传递一个参数。

Helm 有 60 多种可用的函数,其中一些是由 Go 模板语言本身定义的,其他大多数都是 Sprig 模板库提供的,接下来我们会通过部分示例来逐步介绍其中的一些函数功能。

Helm 模板:

当我们谈论 Helm 模板语言 的时候,就好像是特定于 Helm 一样,但实际上它是 Go 模板语言加上一些额外的函数以及各种封装程序的组合。当我们需要学习模板的时候,Go 模板上有许多资源会对我们有所帮助。

T3.6.3.1、管道

模板语言有一个强大的功能就是管道(Pipeline)概念,管道利用 UNIX 的概念,将一系列模板函数连接在一起,链式处理数据,得到最终想要的结果,我们来使用管道重写上面的示例模板:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | quote }}
  food: {{ .Values.favorite.food | quote }}

在这里我们没有调用 quote ARGUMENT 函数,而是颠倒了下顺序,我们使用管道符(|)将参数发送给函数:.Values.favorite.drink | quote,使用管道,我们可以将多个函数处理连接在一起:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | quote }}
  food: {{ .Values.favorite.food | upper | quote }}

最后,模板渲染后,会产生如下所示的结果:

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

NAME: mychart-1575966483
LAST DEPLOYED: Tue Dec 10 16:28:04 2019
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
favorite:
  drink: coffee
  food: pizza
favoriteDrink: coffee

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

我们可以看到 values 中的 pizza 值已经被转换成了 "PIZZA"。当这样传递参数的时候,第一个求值结果(.Values.favorite.drink)会作为一个参数传递给后面函数,我们可以修改上面的 drink 示例,用带有两个参数的函数进行处理:repeat COUNT STRING

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

repeat 函数将字符串重复 COUNT 次,渲染后我们可以得到如下结果:

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

T3.6.3.2、default 函数

在模板中经常会使用到的一个函数是 default 函数:default DEFAULT_VALUE GIVEN_VALUE,该函数允许你在模板内部指定默认值,我们来修改上面示例中的模板:

food: {{ .Values.favorite.food | default "rice" | upper | quote }}

正常运行,我们还是可以得到 values.yaml 文件中定义的 pizza:

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

现在我们从 values.yaml 文件中移除 food 的定义:

favorite:
  drink: coffee
  # food: pizza

现在我们重新运行 helm install --generate-name --dry-run --debug ./mychart 将渲染成如下的 YAML 文件:

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

在一个真实的 chart 模板中,所有的静态默认值都应该位于 values.yaml 文件中,并且不应该重复使用 default 函数,但是,默认命令非常适合计算不能在 values.yaml 文件中声明的 values 值,例如:

food: {{ .Values.favorite.food | default (printf "%s-rice" (include "fullname" .)) }}

不过有些地方,if 条件语句可能比 default 函数更合适,我们会在后面了解到。

注意:在模板中,运算符(eq、ne、lt、gt、and、or 等等)均实现为函数,在管道中,运算符可以用括号 () 进行分割。

T3.7、流程控制

Helm 的模板语言提供了以下流程控制:

  • if/else 条件语句
  • with 指定一个作用域范围
  • range 提供类似于 for each 这样的循环样式

除此之外,还提供了一些声明和使用命名模板的操作:

  • define 在模板内部声明一个新的命名模板
  • template 导入一个命名模板
  • block 声明了一种特殊的可填充模板区域

这里我们先来了解 ifwithrange 语句的使用,其他将在后面的命名模板部分介绍。

T3.7.1、if/else

if/else 就是在模板中占用一个文本区域,该区域实现逻辑控制,基本结构如下:

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

可以看到我们这里判断的是管道而不是一个 values 值,这是因为控制结构可以执行整个管道,而不仅仅是判断值。如果值为以下内容,则将管道判断为 false:

  • 布尔 false
  • 数字零
  • 一个空字符串
  • nil(empty 或者 null)
  • 一个空集合(map、slice、tuple、dict、array)

在其他条件下,条件都为真。

现在我们在上面的示例模板 ConfigMap 中添加一个简单的条件,如果 drink 设置为 coffee,我们就添加另外一个设置:

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

我们把 values.yaml 文件内容设置成下面的样子:

favorite:
  # drink: coffee
  food: pizza

由于我们注释掉了 drink: coffee,所以渲染后输出不会包含 mug: true 的标志,但是如果我们把注释取消掉,则应该输出如下所示的内容:

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

这是因为上面模板中我们添加了 if eq .Values.favorite.drink "coffee" 这样的条件判断,相当于是判断 .Values.favorite.drink 值是否等于 "coffee",如果相等则渲染 mug: true

T3.7.2、空格控制

还有一个非常重要的功能点就是关于空格的控制,因为空格对于 YAML 文件非常重要,不是说任意缩进就可以,还是以前面的例子为例,我们来格式化模板以便阅读:

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 --generate-name --dry-run --debug ./mychart
install.go:148: [debug] Original chart version: ""
install.go:165: [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/course/k8strain/content/helm/manifests/mychart

Error: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 9: did not find expected key

这是因为我们在模板中添加了空格,生成了不正确的 YAML 文件:

apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575970308-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
    mug: true

我们可以看到 mug: true 的缩进是有问题的,不符合 YAML 文件格式,现在我们将缩进去掉:

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: mychart-1575971172-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 }}

渲染后可以看到空行被移除掉了:

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

所以我们这里用 {{- 表示的就是删除本行开头的两个空格以及上一行的换行符,这样是不是就将空行都删除了啊。

在使用移除空格的时候还需要小心,比如下面的操作:

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

我们依然还是可以用 * 来代替空格进行分析,如下所示:

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

第一个 {{- 会删除前面的空格和前面的换行符,然后后面的 -}} 会删除当前行的换行符,这样就会把 mug: true 移动到 food: "PIZZA" 后面去了,最终渲染过后就会变成:food: "PIZZA"mug: true,因为在两侧都去掉了换行符。

有关模板中空格控制的详细信息,可以查看 Go 模板官方文档介绍。

不过有时候模板控制如何缩进比控制间距更加容易,所以,有时候你会发现缩进函数({ indent 2 "mug: true" })更有用。

T3.7.3、使用 with 修改作用域

接下来需要了解的是 with 操作,它可以控制变量的作用域,然后重新用 . 调用就表示对当前作用域的引用,所以,.Values 是告诉模板引擎在当前作用域下查找 Values 对象。

withif 语法格式比较类似:

{{ with PIPELINE }}
  # 限制范围
{{ end }}

范围可以更改,可以让你将当前范围 . 设置为特定的对象,例如,我们一直在使用 .Values.favorites,让我们重写下模板文件 ConfigMap,更改 . 的范围指向 .Values.favorites

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 .Values.favorite } 的语句,意思就是说在 with 语句的作用范围内可以用 . 表示 .Values.favorite 了,所以我们可以引用 .drink.food 了,但是在 { end } 之后就会重置为之前的作用域了。

不过需要注意得是,在受限的作用域内,你无法从父级范围访问到其他对象,比如,下面的模板会失败:

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

因为这里的 .Release.Name 并不在 .Values.favorite 的限制范围内,所以会产生错误,但是,如果我们交换最后两行,就可以正常工作了,因为 { end } 之后会重置作用域。

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

下面我们先来了解下 range,然后再去了解下模板变量,可以为上面的这个范围问题找到一种解决方案。

T3.7.4、range 循环操作

我们知道许多编程语言都支持使用 for 循环、foreach 循环或者类似功能,在 Helm 的模板语言中,迭代列表的方法是使用 range 运算符。

首先我们在 values.yaml 文件中添加一份 pizza 馅料列表:

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

现在我们有了 pizzaToppings 列表(在模板中称为切片),我们可以修改下模板将列表打印到 ConfigMap 中:

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

我们仔细观察下模板中的 toppings: 列表,range 函数将遍历 .Values.pizzaToppings 列表,表达式中的 . 就是每一轮的循环值,也就是说第一轮设置为 mushrooms,第二轮设置为 cheese,依次类推。

我们可以直接传递 . 这个值到管道上,所以我们这里 { . | title | quote } 就相当于发送 .title(标题大小写函数)函数,然后发送给 quote 函数,我们渲染这个模板,会输出如下的内容:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575975849-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
  toppings: |-
    - "Mushrooms"
    - "Cheese"
    - "Peppers"
    - "Onions"

在上面模板中,我们做了一些小小的特殊处理,toppings: |- 行表示声明一个多行字符串,所以其实我们的 toppings 列表不是一个 YAML 列表,而是一个比较大的字符串,这是因为 ConfigMap 中的数据由 key/value 对组成,所有的 key 和 value 都是简单的字符串,要了解为什么是这样的,可以查看 Kubernetes ConfigMap 文档,不过这个细节在这里不重要。

YAML:

多行字符串可以使用 | 保留换行符,也可以使用 > 折叠换行,如:

this: |
 Foo
 Bar
that: >
 Foo
 Bar

对应的意思就是:this: 'Foo\nBar\n', that: 'Foo Bar\n'

+ 表示保留文字块末尾的换行,- 表示删除字符串末尾的换行,如:

s1: |
Foo

s2: |+
Foo

s3: |-
Foo

对应的意思就是:s1: 'Foo\n', s2: 'Foo\n\n\n', s3: 'Foo'

有时候,在模板中快速创建一个列表,然后遍历该列表很有用,Helm 模板具有简化该功能的函数:tuple。元组是固定大小的列表集合,支持任意数据类型,下面是元组的大概使用方法:

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

上面的模板最终会被渲染成如下的 YAML:

sizes: |-
  - small
  - medium
  - large

除了列表和元组之外,range 还可以用于遍历字典,我们在下一节介绍模板变量的时候再来了解这个用法吧。

T3.8、变量

有了函数、管道、对象以及控制结构,还需要变量。在模板中,变量的使用频率较低,但是,我们还是可以使用变量来简化代码,以及更好地使用 withrange

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

{{- 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 变量仍然表示 release 的名称,我们渲染该模板,可以得到如下的正确结果:

# 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 循环里面非常有用:

toppings: |-
  {{- range $index, $topping := .Values.pizzaToppings }}
    {{ $index }}: {{ $topping }}
  {{- end }}

注意 range 在前面,然后是变量,然后是赋值运算符,然后才是列表,这会将整数索引(从0开始)分配给 $index,并将 value 值分配给 $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 }}

在第一次迭代中,$keydrink$valcoffee,在第二次迭代中,$keyfood$valpizza。渲染模板得到如下内容:

# 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 名称的时候,该功能就非常有用了,比如下面的模板文件:

{{- range .Values.tlsSecrets }}
apiVersion: v1
kind: Secret
metadata:
  name: {{ .name }}
  labels:
    # helm 模板经常使用 `.`,但是这里是无效的,用 `$` 是可以生效的。
    app.kubernetes.io/name: {{ template "fullname" $ }}
    # 这里不能引用 `.Chart.Name`,但是可用使用 `$.Chart.Name`
    helm.sh/chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}"
    app.kubernetes.io/instance: "{{ $.Release.Name }}"
    # 值来自于 Chart.yaml 文件中的 appVersion
    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 }}

到现在为止,我们只研究了在一个文件中声明一个模板,但是,Helm 模板语言的强大功能之一是可以声明多个模板并将其一起使用。我们将在下面的章节中来讨论这一点。

T3.9、命名模板

前面我们都是只操作一个模板,现在我们来尝试使用多个模板文件。在本节中,我们可以了解到如何在一个文件中定义命名模板,然后在其他地方使用它们。命名模板(或称子模板)是在文件内部定义的有名称的模板。

注意:

模板名称是全局的,如果声明两个相同名称的模板,则会使用最后被加载的模板。由于子 chart 中的模板是与顶级模板一起编译的,所以需要谨慎命名。

通常约定模板定义时添加 chart 名称:{ define "mychart.labels" },通过使用特定的 chart 名作为前缀,可以避免两个不同的 chart 名称冲突。

T3.9.1、partials 和 _ 文件

Helm 的模板语言支持创建可复用的命名模板。在定义这些模板时,需注意 templates/ 目录下文件的处理规则:

  1. Kubernetes 清单文件:默认情况下,templates/ 目录内除了以下例外,所有文件在渲染后都会发送给 Kubernetes API,被视为资源清单文件。
  2. 例外情况:文件 NOTES.txt 是一个特殊文件,仅用于生成安装后的用户说明,不会被当作资源清单处理。
  3. 以下划线(_)开头的文件:此类文件在渲染时不会被作为 Kubernetes 资源清单生成。它们的主要用途是存储命名模板(也称为局部模板或 Partials),供目录内的其他模板文件调用。

这些以下划线开头的文件(例如 _helpers.tpl)即是 Helm 中的 Partials 文件。按照惯例,Chart 的命名模板通常定义在 _helpers.tpl 文件中。在我们之前创建的 mychart Chart 中,即可找到此默认文件。

核心要点:

  • 功能_ 开头的文件是存放可复用代码片段(命名模板)的容器。
  • 行为:它们本身不产生 Kubernetes 资源,仅用于被其他模板包含和调用。

T3.9.1、define 和 template

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

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

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

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

现在我们可以将该模板嵌入到前面的 ConfigMap 模板中:

{{- 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 文件

# 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

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

可以使用 {/*...*/} 来注释这个命名模板的作用。

现在虽然我们把命名模板放到了 _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 }}

T3.9.2、设置模板范围

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

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

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

$ 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.chartnil,这是因为我们使用的 .Chart.Name 不在定义的这个模板的作用域范围内,当渲染命名模板(使用 define 定义)的时候,它将接收模板调用者传递过来的作用域。在我们这个示例中,我们是这样引用这个模板的:

{{- template "mychart.labels" }}

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

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

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

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

# 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

T3.9.3、include 函数

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

{{- 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 --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 只是一个动作,而不是一个函数,所以无法将命名模板调用的输出传递给其他函数,只是内联插入,相当于渲染的结果是这样的:

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,可以将模板的内容导入到当前的管道中,这样就可以在管道中传递给其他函数进行处理了,如下所示:

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 函数来保证了缩进:

 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 文档的输出格式。

T3.10、访问文件

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

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

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

T3.10.1、基本示例

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

config1.toml:

message = Hello from config 1

config2.toml:

message = This is config 2

config3.toml:

message = Goodbye from config 3

3 个文件都是简单的 TOML 文件,我们知道这些文件的名称,所以我们可以使用 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 . } 获取文件内容。

现在我们渲染这个模板得到如下 ConfigMap:

# 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

T3.10.1、Glob 模式

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

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

T3.10.2、ConfigMap 和 Secrets

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

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

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

T3.10.3、编码

我们也可以导入一个文件并用 base64 编码:

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

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

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

T3.10.4、Lines

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

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

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

T3.11、NOTES.txt 文件

下面我们来了解为 chart 用户提供说明的一个 NOTES.txt 文件,在 chart 安装或者升级结束时,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 {{ .Release.Name }}

现在我们运行 helm install ./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 rude-cardinal

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

T3.12、Subcharts 和 Global Values

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

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

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

T3.12.1、创建子 chart

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

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

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

T3.12.2、添加 values 和模板

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

dessert: cake

下面我们再创建一个新的 ConfigMap 模板 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
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

T3.12.3、父 chart 覆盖 values

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

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

mysubchart:
  dessert: ice cream

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

# 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 值来完成了。

T3.12.4、全局值

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

Values 数据类型有一个名为 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.yamlmysubchart/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/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

T3.12.5、共享模板

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

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

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

{{ include $mytemplate }}

T3.13、模板调试

调试模板可能比较麻烦,因为渲染的模板会发送到 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

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

渲染结果如下:

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

T3.14、Chart Hooks

Helm 提供了一套 Hook 机制,允许开发人员在 Release 生命周期的特定节点执行自定义操作。例如,可以通过 Hook 实现以下功能:

  • 在安装过程中,于加载其他 Chart 资源之前,预先创建 ConfigMap 或 Secret
  • 在升级前执行一个 Job 用于备份数据库,升级后再执行另一个 Job 进行数据恢复
  • 在删除 Release 之前运行 Job,以便在资源清除前完成相关服务的优雅终止

Hook 的编写方式与常规模板类似,但其通过特定的注解(annotation)来标识,使得 Helm 能够以不同的方式处理这些资源。

T3.14.1、可用的 Hook 类型

Helm 支持以下类型的 Hook,分别对应 Release 生命周期的不同阶段:

  • pre-install:在模板渲染完成后、Kubernetes 创建任何资源之前执行
  • post-install:在全部 Kubernetes 资源部署到集群后执行
  • pre-delete:在从 Kubernetes 删除任何资源之前执行
  • post-delete:在所有 Release 资源删除后执行
  • pre-upgrade:在模板渲染后、资源更新之前执行
  • post-upgrade:在所有资源升级完成后执行
  • pre-rollback:在模板渲染后、资源回滚之前执行
  • post-rollback:在所有资源回滚完成后执行
  • test:在执行 helm test 命令时触发(详见测试相关文档)

T3.14.2、Hook 执行生命周期

helm install 为例,正常安装流程如下:

  1. 用户执行 helm install foo
  2. Helm 库调用安装 API
  3. 经过校验后,Helm 渲染 foo 模板
  4. Helm 将生成的资源提交至 Kubernetes
  5. Helm 库向客户端返回 Release 对象及相关数据
  6. Helm 客户端退出

如果在 Chart 中定义了 pre-installpost-install Hook,则安装流程会扩展为:

  1. 用户执行 helm install foo
  2. Helm 库调用安装 API
  3. 安装 crds/ 目录下定义的 CRD 资源
  4. 校验并渲染 foo 模板
  5. Helm 加载 Hook 资源,准备执行 pre-install Hook
  6. Helm 根据权重对 Hook 排序(默认权重为 0,相同权重按名称升序排列)
  7. Helm 依次执行权重最低的 Hook
  8. Helm 等待 Hook 达到就绪状态
  9. Helm 将主要资源加载到 Kubernetes(若使用 --wait 参数,则会等待所有资源就绪后才执行 post-install Hook)
  10. Helm 执行 post-install Hook(加载对应资源)
  11. Helm 等待该 Hook 达到就绪状态
  12. Helm 向客户端返回 Release 对象及相关数据
  13. Helm 客户端退出

Hook 的就绪等待是一个阻塞操作:对于 Job 类资源,Helm 会等待其执行成功;若失败,则整个 Release 失败。在此期间 Helm 客户端处于暂停状态。

对于其他类型资源,一旦 Kubernetes 将其标记为已加载(Added 或 Updated),即视为就绪。若一个 Hook 声明了多个资源,这些资源将按顺序执行。

T3.14.3、重要说明

Hook 创建的资源不会被纳入 Release 的管理范围。一旦 Hook 执行完毕,Helm 将不再跟踪这些资源。因此,若在 Hook 中创建了资源,不能依赖 helm uninstall 自动清理。如需自动删除,需在 Hook 模板中添加 helm.sh/hook-delete-policy 注解,或为 Job 类资源设置 TTL(Time-To-Live)。

T3.14.4、编写 Hook

Hook 本质上是带有特定注解的 Kubernetes 资源清单文件,其本身也是模板,因此支持所有模板功能,包括读取 .Values.Release.Template

以下是一个 post-install Hook 的示例(templates/post-install-job.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
    "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 }}"]

T3.14.4.1、多阶段 Hook

一个资源可通过注解关联多个 Hook:

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

单个 Hook 阶段也可关联多个资源,例如可将一个 Secret 和一个 ConfigMap 均声明为 pre-install Hook。

子 Chart 中声明的 Hook 同样会被执行,上层 Chart 无法将其禁用。

T3.14.4.2、Hook 权重

可通过权重控制同一阶段内多个 Hook 的执行顺序:

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

权重值可为正数或负数,但必须以字符串形式表示。Helm 在执行同一类型的 Hook 时,会按权重升序排列并依次执行。

T3.14.5、Hook 删除策略

通过 helm.sh/hook-delete-policy 注解可定义 Hook 资源的删除时机:

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

可选策略包括:

  • before-hook-creation(默认):执行新 Hook 前删除之前的资源
  • hook-succeeded:Hook 成功后删除资源
  • hook-failed:Hook 失败后删除资源

若未指定删除策略,则默认采用 before-hook-creation

T3.15、案例

前面介绍了 Helm 的基本使用以及 Helm Chart 包开发的相关知识,下面我们通过一个实例来演示如何开发一个真正的 Helm Chart 包。

T3.15.1、应用示例

我们将为 WordPress 应用开发一个 Chart 包。在开发 Chart 包之前,首先需要明确应用的使用方式和部署需求,这是编写对应 Chart 包的基础。

T3.15.1.1、创建 Chart 结构

我们可以使用 helm create 命令创建一个 Chart 包,这里我们将手动创建。首先创建一个名为 wordpress 的文件夹:

$ mkdir wordpress && cd wordpress

T3.15.1.2、定义 Chart.yaml

在目录下创建 Chart.yaml 文件:

apiVersion: v2
name: wordpress
description: A Helm chart for Kubernetes
home: https://wordpress.org/
type: application
# Chart 版本号
version: 0.1.0
# WordPress 应用版本
appVersion: 5.3.2

由于 WordPress 应用依赖 MySQL 数据库,我们需要添加依赖声明。在 Helm 3 中,依赖定义在 Chart.yaml 中:

dependencies:
  - name: mysql
    version: 1.6.2
    repository: http://mirror.azure.cn/kubernetes/charts/
    condition: mysql.enabled
    tags:
      - wordpress-database

可以通过 helm search 命令查找可用 Chart:

$ helm search repo stable/mysql
NAME                    CHART VERSION   APP VERSION     DESCRIPTION                                       
stable/mysql            1.6.2           5.7.28          Fast, reliable, scalable, and easy to use open-...

T3.15.1.3、配置 Values.yaml

依赖定义中的 condition: mysql.enabled 表示只有当 mysql.enabledtrue 时才会安装此子 Chart,这样我们可以选择使用外部数据库。首先创建 values.yaml 文件:

##
## MySQL Chart 配置,参考如下 values.yaml
## https://github.com/helm/charts/blob/master/stable/mysql/values.yaml
##
mysql:
  ## 是否部署 MySQL 服务来满足 WordPress 的需求。
  ## 如果要使用外部数据库,将 enabled 设置为 false 并配置 externalDatabase 参数。
  enabled: true
  ## 其他 MySQL 配置

## 当 mysql.enabled=false 时使用外部数据库
externalDatabase:
  ## 数据库地址
  host: localhost:3306
  ## WordPress 数据库的非 root 用户
  user: wordpress
  ## 数据库密码
  password: wordpress
  ## 数据库名称
  database: wordpress

上述配置逻辑如下:如果 mysql.enabled=true,则使用 MySQL 子 Chart 安装数据库;如果为 false,则使用 externalDatabase 中的外部数据库配置。

T3.15.1.4、获取依赖 Chart

创建 templates 和 charts 目录,并获取 MySQL 子 Chart:

$ mkdir templates && mkdir charts
$ cd charts && helm fetch stable/mysql && cd ..

当前 Chart 包目录结构如下:

$ tree .
.
├── Chart.yaml
├── charts
│   └── mysql-1.6.2.tgz
├── templates
└── values.yaml

2 directories, 4 files

T3.15.2、模板开发

T3.15.2.1、准备模板文件

将 WordPress 资源清单文件复制到 templates 目录中,将 Deployment 和 Service 分别放在不同的 YAML 文件中。移除 MySQL 相关部分,因为我们通过外部数据库或子 Chart 来提供数据库服务。同时移除所有资源对象的命名空间配置,因为可以通过 helm install 命令指定。

当前模板目录结构:

$ tree templates 
templates
├── deployment.yaml
├── pvc.yaml
└── service.yaml

0 directories, 3 files

T3.15.2.2、调试与命名

尝试安装调试这个 Chart 模板:

$ helm install --generate-name --dry-run --debug .

可能会出现警告信息:Warning: Dependencies are handled in Chart.yaml since apiVersion "v2". We recommend migrating dependencies to Chart.yaml.,这意味着在 Helm 3 中依赖项应配置在 Chart.yaml 中,我们已经完成了这一调整。

错误信息通常类似 rendered manifests contain a resource that already exists,这是因为资源对象名称固定,可能与现有集群资源冲突。我们需要为资源清单设置动态命名。

T3.15.2.3、创建命名模板

templates/_helpers.tpl 中创建命名模板:

{{/*
创建默认的应用名称,截取63个字符以符合 Kubernetes 的 DNS 命名规范。
*/}}
{{- define "wordpress.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

将此命名模板放入 _helpers.tpl 文件中,后续所有命名模板都将在此文件中定义。

T3.15.2.4、应用命名模板

将 templates 目录下所有资源对象的 name 属性替换为命名模板。按照惯例,将 Release 和 Chart 名称作为标签:

metadata:
  name: {{ include "wordpress.fullname" . }}
  labels:
    app: {{ include "wordpress.fullname" . }}
    chart: {{ include "wordpress.chart" . }}
    release: {{ .Release.Name | quote }}
    heritage: {{ .Release.Service | quote }}

同时更新 Deployment 的 matchLabelstemplate.labels 以及 Service 的 selector 标签。

T3.15.2.5、优化 PVC 配置

原 PVC 配置使用固定名称,现在需要更灵活地支持多种使用场景:

volumes:
  - name: wordpress-data
  {{- if .Values.persistence.enabled }}
    persistentVolumeClaim:
      claimName: {{ .Values.persistence.existingClaim | default (include "wordpress.fullname" .) }}
  {{- else }}
    emptyDir: {}
  {{- end }}

相应的 PVC 对象模板:

{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: {{ include "wordpress.fullname" . }}
  labels:
    app: {{ include "wordpress.fullname" . }}
    chart: {{ include "wordpress.chart" . }}
    release: {{ .Release.Name | quote }}
    heritage: {{ .Release.Service | quote }}
spec:
  {{- if .Values.persistence.storageClass }}
  storageClassName: {{ .Values.persistence.storageClass | quote }}
  {{- end }}
  accessModes:
    - {{ .Values.persistence.accessMode | quote }}
  resources:
    requests:
      storage: {{ .Values.persistence.size | quote }}
{{- end }}

values.yaml 中添加默认配置:

## 是否使用 PVC 开启数据持久化
persistence:
  enabled: true
  ## 是否使用 storageClass,如果不适用则不配置
  # storageClass: "xxx"
  ## 如果想使用一个已存在的 PVC 对象,则传递给下面的 existingClaim 变量
  # existingClaim: your-claim
  accessMode: ReadWriteMany  # 访问模式
  size: 2Gi  # 存储容量

T3.15.3、高级定制

T3.15.3.1、副本数与更新策略

使副本数可通过 Values 配置:replicas: {{ .Values.replicaCount }}

更新策略使用 toYaml 函数:

{{- if .Values.updateStrategy }}
strategy:
  {{- toYaml .Values.updateStrategy | nindent 2 }}
{{- end }}

T3.15.3.2、节点调度配置

添加 nodeSelector、容忍度和亲和性配置:

{{- if .Values.nodeSelector }}
nodeSelector:
  {{- toYaml .Values.nodeSelector | nindent 2 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
  {{- toYaml . | nindent 2 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
  {{- toYaml . | nindent 2 }}
{{- end }}

T3.15.3.3、镜像配置

配置镜像拉取策略和私有仓库凭据:

{{- if .Values.image.pullSecrets }}
imagePullSecrets:
{{- range .Values.image.pullSecrets }}
  - name: {{ . }}
{{- end }}
{{- end }}
containers:
  - name: wordpress
    image: {{ printf "%s:%s" .Values.image.repository .Values.image.tag }}
    imagePullPolicy: {{ .Values.image.pullPolicy | quote }}

Values 配置:

image:
  repository: wordpress
  tag: 5.3.2-apache
  ## 指定 imagePullPolicy
  pullPolicy: IfNotPresent
  ## 私有仓库凭据配置
  # pullSecrets:
  #   - myRegistryKeySecretName

T3.15.3.4、环境变量配置

根据数据库选择配置环境变量:

env:
  - name: WORDPRESS_DB_HOST
    {{- if .Values.mysql.enabled }}
    value: {{ printf "%s:%d" (include "mysql.fullname" .) (int64 .Values.mysql.service.port) }}
    {{- else }}
    value: {{ .Values.externalDatabase.host | quote }}
    {{- end }}
  - name: WORDPRESS_DATABASE_NAME
    {{- if .Values.mysql.enabled }}
    value: {{ .Values.mysql.mysqlDatabase | quote }}
    {{- else }}
    value: {{ .Values.externalDatabase.database | quote }}
    {{- end }}
  - name: WORDPRESS_DB_USER
    {{- if .Values.mysql.enabled }}
    value: {{ .Values.mysql.mysqlUser | quote }}
    {{- else }}
    value: {{ .Values.externalDatabase.user | quote }}
    {{- end }}
  - name: WORDPRESS_DB_PASSWORD
    valueFrom:
      secretKeyRef:
        {{- if .Values.mysql.enabled }}
        name: {{ include "mysql.fullname" . }}
        key: mysql-password
        {{- else }}
        name: {{ printf "%s-%s" .Release.Name "externaldb" }}
        key: db-password
        {{- end }}

T3.15.3.5、外部数据库 Secret

创建外部数据库的 Secret 资源:

{{- if not .Values.mysql.enabled }}
apiVersion: v1
kind: Secret
metadata:
  name: {{ printf "%s-%s" .Release.Name "externaldb" }}
  labels:
    app: {{ printf "%s-%s" .Release.Name "externaldb" }}
    chart: {{ include "wordpress.chart" . }}
    release: {{ .Release.Name | quote }}
    heritage: {{ .Release.Service | quote }}
type: Opaque
data:
  db-password: {{ .Values.externalDatabase.password | b64enc | quote }}
{{- end }}

T3.15.3.6、资源限制与健康检查

配置资源限制:

resources:
  {{- toYaml .Values.resources | nindent 10 }}

配置健康检查探针:

{{- if .Values.livenessProbe.enabled }}
livenessProbe:
  httpGet:
    path: /wp-login.php
    port: 80
  initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
  periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
  timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
  successThreshold: {{ .Values.livenessProbe.successThreshold }}
  failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
{{- end }}
{{- if .Values.readinessProbe.enabled }}
readinessProbe:
  httpGet:
    path: /wp-login.php
    port: 80
  initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
  periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
  timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
  successThreshold: {{ .Values.readinessProbe.successThreshold }}
  failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
{{- end }}

T3.15.3.7、Service 模板化

使 Service 类型可通过 Values 配置:

apiVersion: v1
kind: Service
metadata:
  name: {{ include "wordpress.fullname" . }}
  labels:
    app: {{ include "wordpress.fullname" . }}
    chart: {{ include "wordpress.chart" . }}
    release: {{ .Release.Name | quote }}
    heritage: {{ .Release.Service | quote }}
spec:
  selector:
    app: {{ include "wordpress.fullname" . }}
  type: {{ .Values.service.type }}
  {{- if or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort") }}
  externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy | quote }}
  {{- end }}
  ports:
    - name: web
      port: {{ .Values.service.port }}
      targetPort: web
      {{- if and (eq .Values.service.type "NodePort") (.Values.service.nodePort) }}
      nodePort: {{ .Values.service.nodePort }}
      {{- end }}

T3.15.3.8、添加使用说明

templates/NOTES.txt 中添加使用说明:

Get the WordPress Manifests Objects:

$ kubectl get all -l app={{ .Release.Name }}

T3.15.4、测试部署

使用 Chart 包进行安装测试:

$ helm install mychart . --set service.type=NodePort

查看安装状态:

$ helm list
NAME    NAMESPACE REVISION UPDATED                                 STATUS    CHART          APP VERSION
mychart default   1        2020-03-08 17:52:04.826185 +0800 CST    deployed  wordpress-0.1.0 5.3.2

查看部署的资源对象:

$ kubectl get all -l app=mychart-wordpress
NAME                                     READY   STATUS    RESTARTS   AGE
pod/mychart-wordpress-5f65786d89-2m45s   1/1     Running   0          70s

NAME                        TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
service/mychart-wordpress   NodePort   10.99.239.29   <none>        80:30427/TCP   70s

NAME                                READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/mychart-wordpress   1/1     1            1           70s

NAME                                           DESIRED   CURRENT   READY   AGE
replicaset.apps/mychart-wordpress-5f65786d89   1         1         1       70s

现在可以通过 NodePort 访问 WordPress 应用。Chart 包的大部分配置都可以通过 Values 进行定制。

标签云