## Helm手记

Helm是一个用于Kubernetes应用的包管理工具。它允许你定义、安装和升级Kubernetes应用。 Helm
使用称为“Charts”的打包格式,每个Chart都包含用于部署一个具体应用程序的相关文件。

### 1. 创建Chart

```shell
$ helm create example-chart

$ tree example-chart                                 
example-chart
├── charts  # 初始为空目录,存放本 Chart 依赖的其他 Charts
├── Chart.yaml # 记录这个Chart的元数据,如名称/描述/版本等
├── templates # 主要。存放k8s部署文件的helm模板,不完全等于k8s模板,扩展go template语法
│   ├── deployment.yaml  # 用于定义 Kubernetes Deployment 对象,描述如何部署你的应用程序。
│   ├── _helpers.tpl # 包含了一些 Helm 模板引擎的辅助函数,可以在其他所有模板文件中使用。
│   ├── hpa.yaml # 用于定义 Horizontal Pod Autoscaler 对象,允许根据 CPU 使用率或其他指标动态调整 Pod 的数量。
│   ├── ingress.yaml # 用于定义 K8s Ingress 对象
│   ├── NOTES.txt #  当执行 helm install 时,Helm 将在安装完成后显示这个文件中的注释。
│   ├── serviceaccount.yaml # 用于定义 Kubernetes ServiceAccount 对象,用于为 Pod 中的进程提供身份验证信息。
│   ├── service.yaml # 用于定义 Kubernetes Service 对象,用于将流量路由到你的 Pod
│   └── tests # 包含用于测试 Chart 的测试文件
│       └── test-connection.yaml
└── values.yaml # 该文件包含了 Helm Chart 的默认值,这些值将用于渲染模板文件; 用户可以通过传递自定义的 values.yaml 文件或通过命令行选项来覆盖这些默认值
```

其中`deployment.yaml`和`service.yaml`
是必须要使用的(即需要修改),其他K8s对象模板文件都是用到时才会改动,包含`hpa.yaml`, `ingress.yaml`, `serviceaccount.yaml`,
在这几个文件的首行包含`if .Values.*.enabled`字样表示动态启用,需要在`values.yaml`文件中的`enabled`字段为`true`时才会启用。

#### 1.1 解释 deployment.yaml

```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "example-chart.fullname" . }}
  labels:
    {{- include "example-chart.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "example-chart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
...
```

其中`{{ ... }}`是Go Template语法。大括号中以

- `.Values`开头的属性值是在`values.yaml`中定义的
- 其他属性是在`Chart.yaml`中定义的
- `.Release`开头的是在发布版本时确定

通过Go Template,可以使模板的具体部署操作和部署参数分离开来,各自单独维护。最关键的是可以多个对象复用同一套Chart模板。

#### 1.2 解释 _helpers.tpl

`_helpers.tpl`与其他模板文件不同,它可以被除了`Chart.yaml`以外的所有模板文件(包括自己)引用。
一般用来定义生成逻辑稍微复杂的变量,比如某项命名/标签等。

> 一般我们可以直接将变量的生成逻辑写入K8s YAML文件中,但这样会使得它们变得臃肿而降低模板可读性,所以会用到`_helpers.tpl`。

这个文件的语法也很简单,主要使用Helm
模板引擎的[各种函数](https://helm.sh/zh/docs/chart_template_guide/functions_and_pipelines/)来组合成具体的逻辑。

```yaml
# 定义一个变量 example-chart.name
# 其值的生成逻辑是:优先取 .Chart.Name,若为空则取.Values.nameOverride
  # 然后,|类似管道符号,继续将值调用trunc函数确保字符长度不超过63,最后去掉后缀-
  { { - define "example-chart.name" - } }
  { { - default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" } }
  { { - end } }
```

#### 1.3 解释 tests 目录

默认这个目录下有个`test-connection.yaml`文件,用于定义【部署完成后需要执行的测试内容】,以便验证应用是否成功部署。

执行`helm test <RELEASE_NAME>`来运行测试,以便验证部署的Helm资源是否正常运行。下面是一个例子:

```yaml
# 默认是一个Pod,测试对Service的访问连通性
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "example-chart.fullname" . }}-test-connection"
  labels:
    { { - include "example-chart.labels" . | nindent 4 } }
  annotations:
    "helm.sh/hook": test # 测试资源都有这个注解,它是helm的一个钩子
spec:
  containers:
    - name: wget
      image: busybox
      command: [ 'wget' ]
      args: [ '{{ include "example-chart.fullname" . }}:{{ .Values.service.port }}' ]
  restartPolicy: Never
```

注意,执行测试的Pod资源在测试完成后应该以(exit 0)成功退出,所以注意`command`部分的编写。

在Helm v3中,支持使用以下测试钩子(`helm.sh/hook`)之一:

- test-failure:这是一个针对【失败】情况的测试用例
- test-success:这是一个针对【成功】情况的测试用例(等同于旧版的`test`)
- test(向后兼容,等同于`test-success`)

#### 1.4 解释 values.yaml

这是最主要的配置文件,用于定义应用部署的各项参数。比如Pod副本数量,镜像名称等。

通过查看`values.yaml`可以知道,默认的配置是一个使用Nginx镜像的Deployment控制器,副本数量为1。并且基于Deployment控制器创建了一个Service,
类型ClusterIP,监听80端口;此外还创建了Pod专属的serviceAccount。Ingress和Hpa配置项默认未启用。

### 2. 验证Chart

发布前需要对Chart配置格式进行验证:

```shell
$ helm lint example-chart
==> Linting example-chart
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed
```

在最终执行`helm install`
进行部署时,会将Chart文件解析为K8s能够识别的各种对象模板以进行部署。
可使用`helm install --dry-run --debug [Chart目录位置]`来提前检查Chart生成的k8s对象模板是否正确。

```shell
# 其中helm-nginx是发布名称,最后才是chart目录作为参数
helm install --dry-run --debug helm-nginx example-chart
...输出计算后的各模板内容
```

新版的Helm必须提供发布名称参数,或者提供`--generate-name`标志使用自动生成的名称。

### 3. 发布和查看

```shell
$ helm install  helm-nginx example-chart
NAME: helm-nginx
LAST DEPLOYED: Mon Dec  4 20:23:48 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
...
```

查看部署:

```shell
$ helm ls
NAME      	NAMESPACE	REVISION	UPDATED                                	STATUS  	CHART              	APP VERSION
helm-nginx	default  	1       	2023-12-04 20:23:48.653998103 +0800 CST	deployed	example-chart-0.1.0	1.16.0

# status可以查看最后部署的时间,namespace,状态,递增版本号
$ helm status helm-nginx       
NAME: helm-nginx
LAST DEPLOYED: Mon Dec  4 20:23:48 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
...

# --show-resources 列出Chart部署的资源
$ helm status helm-nginx --show-resources
NAME: helm-nginx
LAST DEPLOYED: Mon Dec  4 20:23:48 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
RESOURCES:
==> v1/ServiceAccount
NAME                       SECRETS   AGE
helm-nginx-example-chart   0         6m5s

==> v1/Service
NAME                       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
helm-nginx-example-chart   ClusterIP   20.1.80.84   <none>        80/TCP    6m5s

==> v1/Deployment
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
helm-nginx-example-chart   1/1     1            1           6m5s

==> v1/Pod(related)
NAME                                        READY   STATUS    RESTARTS   AGE
helm-nginx-example-chart-5b5b69cb9d-nnrpn   1/1     Running   0          6m5s
```

删除部署(无法回滚):

```shell
helm uninstall helm-nginx
```

### 4. 打包Chart

当Chart编写和验证完成后,你如果有分发给给其他用户使用的需求(像分享镜像那样)或者需要版本化Chart包,则可以打包Chart到仓库中。

```shell
$ helm package example-chart 
Successfully packaged chart and saved it to: /mnt/hgfs/go_dev/k8s-tutorial-cn/helm/example-chart-0.1.0.tgz
```

**升级Chart**

升级表示要对Chart配置进行大或小的修改,并且更新`Chart.yaml`的版本号。在其中会有两个意义不同的版本号:

```yaml
# 打包时增加的版本号
version: 0.1.0

# 发布时增加的版本号
appVersion: "1.16.0"
```

其中`version`是`helm search xxx`输出的Chart Version。`helm search xxx --versions`会输出每个Chart的所有历史版本。

### 5. 发布的升级、回滚和删除

#### 5.1 升级

刚才我们已经发布了`example-chart`,命名为`helm-nginx`,其`Chart.yaml`中的`appVersion`为`1.16.0`。现在我们修改`appVersion`
为`1.16.1`来模拟升级所做的修改
,然后更新发布。

```shell
# 首先修改Chart.yaml中的appVersion为 1.16.1

# 然后更新发布,--description 增加发布说明
$ helm upgrade helm-nginx example-chart
Release "helm-nginx" has been upgraded. Happy Helming!
NAME: helm-nginx
LAST DEPLOYED: Mon Dec  4 20:57:14 2023
NAMESPACE: default
STATUS: deployed
REVISION: 2
...

# APP VERSION 已更新
$ helm ls                              
NAME      	NAMESPACE	REVISION	UPDATED                                	STATUS  	CHART              	APP VERSION
helm-nginx	default  	2       	2023-12-04 20:57:14.959204562 +0800 CST	deployed	example-chart-0.1.0	1.16.1   

$ helm upgrade helm-nginx example-chart
Release "helm-nginx" has been upgraded. Happy Helming!
NAME: helm-nginx
LAST DEPLOYED: Mon Dec  4 20:57:14 2023
NAMESPACE: default
STATUS: deployed
REVISION: 2
...
```

如果最后的Chart参数是引用某个仓库中的Chart(引用形式为`repo/chart_name`
),此时可以使用`helm upgrade helm-nginx example-chart --version x.x.x`来指定Chart版本进行升级。

如果是本地的Chart目录,那`--version`
参数就无效了,会直接使用所引用目录下的Chart配置进行升级。**无论你是否修改了Chart中的任何一个文件**
,Helm都会为发布增加`REVISION`号。
当然,实际的K8s对象如Deployment只会在模板变化时重新部署Pod。

实际环境中,我们通常会使用`-f values.yaml`参数来指定配置文件(或使用`--set`指定某个配置参数)进行升级。使用`helm upgrade -h`
查看全部参数。

> 例如:  
> helm upgrade helm-nginx example-chart -f example-chart/values.yaml --set "serviceType=NodePort"

其中`--set`可以指定多个键值对参数(只用于替换`values.yaml`中的配置),使用`helm show values example-chart`
查看Chart的`values.yaml`配置。
此外,它还有一些细节上的规范(比如如何设置值为数组的字段),可以参考以下文档:

- [安装前自定义chart(官方文档)](https://helm.sh/zh/docs/intro/using_helm/#安装前自定义-chart)
- [“set”参数的高级用法(英)](https://itnext.io/helm-chart-install-advanced-usage-of-the-set-argument-3e214b69c87a)

> 最后,说一点笔者的个人建议。在实际的项目开发中,建议只需要在每个服务目录下保留`values.yaml`
> 即可,而不需要保留`Chart.yaml`来定义其APP VERSION,
> 因为这样就免去了在每个服务目录下维护两个helm配置文件的麻烦。在发布时我们只需要使用`--description`来简述
> 本次发布的具体内容即可,并可以直接将镜像tag作为发布说明。这样也可以为回滚提供帮助。
>
> Helm不支持在Upgrade时设置`appVersion`,这是难以理解的。在 [ #3555](https://github.com/helm/helm/issues/3555) 这个讨论时间长达三年的
> Issue中,官方最终也没有支持这种方式,而是推荐使用`helm package --app-version`的方式来设置`appVersion`
> ,但打包就需要部署Helm仓库,增加了运维成本。
> 社区中的另一种非常规做法则是在更新发布前使用`sed`命令修改了`Chart.yaml`中的`appVersion`。

#### 5.2 回滚

查看helm发布的记录:

```shell
# 在upgrade时使用--description设置的说明会覆盖这里的 DESCRIPTION
$ helm history helm-nginx   
REVISION	UPDATED                 	STATUS    	CHART              	APP VERSION	DESCRIPTION     
1       	Mon Dec  4 20:23:48 2023	superseded	example-chart-0.1.0	1.16.0     	Install complete
2       	Mon Dec  4 20:57:14 2023	superseded	example-chart-0.1.0	1.16.1     	Upgrade complete
```

注意:`REVISION`是永远递增的。

回滚到指定`REVISION`:

```shell
# 1是REVISION,不指定就默认上个REVISION
$ helm rollback helm-nginx 1           
Rollback was a success! Happy Helming!
```

注意,Helm默认最多保留10条发布记录,也就是说,当`REVISION`为11的时候(只能看到2~11的记录),1就被删除了,也不能回滚到1了。

#### 5.3 删除

新版本中,`helm delete RELEASE-NAME`命令已经不再保留发布记录了,而是彻底删除发布涉及的所有K8s对象和Helm中的记录。

`delete`可以使用关键字`uninstall/del/un`进行等价替换。

### 6. 钩子

Helm提供了钩子(Hook)功能,允许在Helm资源的安装前/后、删除前/后等特定时机执行特定的操作。
一般使用钩子来执行以下任务:

- 升级之前检查环境是否具备升级的条件
- 安装前先创建一些基础资源,如ConfigMap、Secret等
- 删除后执行一些清理工作

所有Helm钩子:

| Hook          | 作用                               |
|---------------|----------------------------------|
| pre-install   | 在渲染模板之后,在 Kubernetes 中创建任何资源之前执行 |
| post-install  | 将所有资源加载到 Kubernetes 之后执行         |
| pre-delete    | 在从 Kubernetes 删除任何资源之前对删除请求执行    |
| post-delete   | 删除所有发行版资源后,对删除请求执行               |
| pre-upgrade   | 在呈现模板之后但在更新任何资源之前,对升级请求执行        |
| post-upgrade  | 升级所有资源后执行升级                      |
| pre-rollback  | 在呈现模板之后但在回滚任何资源之前,对回滚请求执行        |
| post-rollback | 修改所有资源后,对回滚请求执行                  |
| test          | 调用Helm test子命令时执行                |

在1.3节【解释tests目录】中我们已经看见过钩子是通过在Pod资源(其他k8s资源也可)中使用注解来使用的。下面是一个例子:

```yaml
# 通常在Pod和Job中使用
annotations:
  "helm.sh/hook": post-install,post-upgrade
```

所有钩子关联的资源都是串行阻塞加载的,当使用钩子的资源达到`Ready`状态时,
Helm会继续加载下一个钩子。如果一个资源加载失败,则不会继续加载后续的资源。

> 针对Pod和Job以外的资源,一旦K8s将资源标记为已加载(已添加或已更新),资源会被认为是`Ready`。

此外,还可以定义:

- 钩子关联资源的权重,这决定了钩子资源加载顺序
- 钩子关联资源的删除策略,这决定了删除钩子资源的时机

示例如下:

```yaml
# 权重是字符串形式的数字,支持负数和正数,按照升序执行
"helm.sh/hook-weight": "-5"
# 删除策略
# before-hook-creation   新钩子启动前删除之前的资源 (默认)
# hook-succeeded	钩子成功执行之后删除资源
# hook-failed	如果钩子执行失败,删除资源
"helm.sh/hook-delete-policy": hook-succeeded
```

你可以通过 [Kibana-templates](helm/kibana/templates) 来进一步学习钩子的使用。

### 推荐的文章

- [Helm官方文档](https://helm.sh/zh/docs/)
- [Helm template快速入门_掘金](https://juejin.cn/post/6844904199818313735)