helm的安装/使用及其运行原理

如果你问我:在使用k8s的过程中,最痛苦的是什么?
我可能会说:最痛苦的莫过于编写各种各样的yaml资源文件,并且稍有不慎就会出错。
如果,你也遇到了这个问题,那么helm将拯救你。

认识helm

Helm 是 Kubernetes 应用程序的包管理器,属于 Kubernetes 的一个子项目,主要用来对 Kubernetes 进行包管理,类似Linux系统常用的 apt、yum等包管理工具。它可以帮助您在 Kubernetes 中无缝安装、配置和管理复杂的应用程序。

Helm Chart 是描述一组 Kubernetes 资源(如部署、服务和配置映射)以及必要配置值的文件集合。

三大概念

  • Chart 代表着 Helm 包

它包含在 Kubernetes 集群内部运行应用程序,工具或服务所需的所有资源定义。你可以把它看作是 Homebrew formula,Apt dpkg,或 Yum RPM 在Kubernetes 中的等价物。

  • Repository(仓库) 是用来存放和共享 charts 的地方

它就像 Perl 的 CPAN 档案库网络 或是 Fedora 的 软件包仓库,只不过它是供 Kubernetes 包所使用的。

  • Release 是运行在 Kubernetes 集群中的 chart 的实例

一个 chart 通常可以在同一个集群中安装多次。每一次安装都会创建一个新的 release。以 MySQL chart为例,如果你想在你的集群中运行两个数据库,你可以安装该chart两次。每一个数据库都会拥有它自己的 release 和 release name。

Helm的目标

Helm管理名为chart的Kubernetes包的工具。Helm可以做以下的事情:

  • 从头开始创建新的chart
  • 将chart打包成归档(tgz)文件
  • 与存储chart的仓库进行交互
  • 在现有的Kubernetes集群中安装和卸载chart
  • 管理与Helm一起安装的chart的发布周期

组件

Helm是一个可执行文件,执行时分成两个不同的部分。Helm客户端 是终端用户的命令行客户端。负责以下内容:

  • 本地chart开发
  • 管理仓库
  • 管理发布
  • 与Helm库建立接口
    • 发送安装的chart
    • 发送升级或卸载现有发布的请求

Helm库 提供执行所有Helm操作的逻辑。与Kubernetes API服务交互并提供以下功能:

  • 结合chart和配置来构建版本
  • 将chart安装到Kubernetes中,并提供后续发布对象
  • 与Kubernetes交互升级和卸载chart

独立的Helm库封装了Helm逻辑以便不同的客户端可以使用它。

特点 操作示例
简化部署 helm install myapp ./myapp-chart
自定义配置 helm install myapp ./myapp-chart --set environment=production
helm install myapp ./myapp-chart --set environment=development
简化更新 helm upgrade myapp ./myapp-chart
查询与快速部署 helm search hub mysql
helm install mymysql stable/mysql

执行

Helm客户端和库是使用Go编程语言编写的,这个库使用Kubernetes客户端库(k8s.io/client-go)与Kubernetes通信。现在,这个库使用REST+JSON。它将信息存储在Kubernetes的密钥中。 不需要自己的数据库。(如果可能,配置文件是用YAML编写的)

源码与文档

github地址: https://github.com/helm/helm
官方文档:https://helm.sh/zh/docs/intro/quickstart/

源码主要的目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
➜ tree -L 3 helm

helm
├── cmd
│   ├── helm # 目录中几乎每一个文件都是helm的一个子命令
│   │   ├── completion.go
│   │   ├── create.go
│   │   ├── dependency.go
│   │   ├── dependency_build.go
│   │   ├── dependency_update.go
│   │   ├── docs.go
│   │   ├── env.go
│   │   ├── flags.go
│   │   ├── get.go
│   │   ├── get_all.go
│   │   ├── get_hooks.go
│   │   ├── get_manifest.go
│   │   ├── get_metadata.go
│   │   ├── get_notes.go
│   │   ├── get_values.go
│   │   ├── helm.go
│   │   ├── history.go
│   │   ├── install.go
│   │   ├── lint.go
│   │   ├── list.go
│   │   ├── load_plugins.go
│   │   ├── package.go
│   │   ├── plugin.go
│   │   ├── plugin_install.go
│   │   ├── plugin_list.go
│   │   ├── plugin_uninstall.go
│   │   ├── plugin_update.go
│   │   ├── printer.go
│   │   ├── pull.go
│   │   ├── push.go
│   │   ├── registry.go
│   │   ├── registry_login.go
│   │   ├── registry_logout.go
│   │   ├── repo.go
│   │   ├── repo_add.go
│   │   ├── repo_index.go
│   │   ├── repo_list.go
│   │   ├── repo_remove.go
│   │   ├── repo_update.go
│   │   ├── require
│   │   ├── rollback.go
│   │   ├── root.go
│   │   ├── root_unix.go
│   │   ├── root_windows.go
│   │   ├── search
│   │   ├── search.go
│   │   ├── search_hub.go
│   │   ├── search_repo.go
│   │   ├── show.go
│   │   ├── status.go
│   │   ├── template.go
│   │   ├── testdata
│   │   ├── uninstall.go
│   │   ├── upgrade.go
│   │   ├── verify.go
│      └── version.go
├── go.mod
├── go.sum
├── internal
├── pkg
├── scripts
└── testdata

helm 使用前注意事项

先决条件

想成功和正确地使用Helm,需要以下前置条件。

  • 一个 Kubernetes 集群
  • 确定你安装版本的安全配置
  • 安装和配置Helm

安装或使用现有Kubernetes集群

  • 使用Helm,需要一个Kubernetes集群。对于Helm的最新版本,我们建议使用Kubernetes的最新稳定版, 在大多数情况下,它是倒数第二个次版本。
  • 应该有一个本地的 kubectl

helm的安装与使用

安装可以参考官方文档 https://helm.sh/zh/docs/intro/install/,笔者这里使用 gox 对helm源码进行编译安装:

1
2
3
4
5
6
7
8
9
10
11
➜ git clone https://github.com/helm/helm.git
➜ cd helm/cmd/helm

# 这里使用的是mac,所以编译为 darwin/amd64。得到可执行文件 helm_darwin_amd64
➜ gox -osarch="darwin/amd64" ./
➜ mv helm_darwin_amd64 helm
➜ cp helm /usr/local/bin/

# 查看安装的helm的版本号
➜ helm version
version.BuildInfo{Version:"v3.15.2", GitCommit:"1a500d5625419a524fdae4b33de351cc4f58ec35", GitTreeState:"clean", GoVersion:"go1.22.4"}

使用 helm 创建 helm-demo chart 模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
➜ helm create helm-demo
Creating helm-demo

➜ tree helm-demo
helm-demo
├── Chart.yaml - chart定义,可以定义chart的名字,版本号信息。
├── charts - 依赖的子包目录,里面可以包含多个依赖的chart包
├── templates - k8s配置模版目录,除NOTES.txt和下划线开头命名的文件,其他可任意命名
│   ├── NOTES.txt - chart 包的帮助信息文件,执行helm install命令安装成功后会输出这个文件的内容
│   ├── _helpers.tpl - 下划线开头的文件被视为公共库定义文件,定义通用的子模版、函数等,不会将这些文件的渲染结果提交给k8s处理
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│   └── test-connection.yaml
└── values.yaml - 包的参数配置文件,模版可引用这里参数

4 directories, 10 files

以 模板 templates/service.yaml 文件为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
name: {{ include "helm-demo.fullname" . }}
labels:
{{- include "helm-demo.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "helm-demo.selectorLabels" . | nindent 4 }}

可以看到被 {{}} 包裹的都是在渲染时需要填充的数据,其中 include 意味着此处嵌入其他模板,其他的则为 渲染时填充的字段值,.Values 开头的值在 values.yaml 文件中。例如.Values.service.port为values.yaml文件中 service下port 的值,其他的类似(当然用法跟 go 的 text/template package的用法一样)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#===== values.yaml文件

# Default values for helm-demo.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
repository: nginx
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
# Specifies whether a service account should be created
create: true
# Automatically mount a ServiceAccount's API credentials?
automount: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""

podAnnotations: {}
podLabels: {}

podSecurityContext: {}
# fsGroup: 2000

securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000

service:
type: ClusterIP
port: 80

ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local

resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi

livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http

autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80

# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false

# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true

nodeSelector: {}

tolerations: []

affinity: {}

在helm-demo chart各个文件中,所引用或使用的顶层变量大致为:

1
2
3
4
5
6
7
example-helm/templates/tests/test-connection.yaml ->  [Chart] [Files] [Release] [Capabilities] [Values] [Subcharts] [Template]
example-helm/templates/serviceaccount.yaml -> [Chart] [Files] [Release] [Capabilities] [Values] [Subcharts] [Template]
example-helm/templates/service.yaml -> [Files] [Release] [Capabilities] [Values] [Subcharts] [Template] [Chart]
example-helm/templates/ingress.yaml -> [Template] [Chart] [Files] [Release] [Capabilities] [Values] [Subcharts]
example-helm/templates/hpa.yaml -> [Values] [Subcharts] [Template] [Chart] [Files] [Release] [Capabilities]
example-helm/templates/deployment.yaml -> [Files] [Release] [Capabilities] [Values] [Subcharts] [Template] [Chart]
example-helm/templates/NOTES.txt -> [Chart] [Files] [Release] [Capabilities] [Values] [Subcharts] [Template]

Helm install命令执行流程

helm的大部分子命令执行逻辑相似,这里以 helm 的子命令install为例,简单追溯其执行过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
cmd/helm/install.go
newInstallCmd()
runInstall()
vals, err := valueOpts.MergeValues(p) // 获取helm模板需要的参数信息
RunWithContext()
i.cfg.KubeClient.IsReachable() // 检查是否能够连接到k8s集群
i.cfg.renderResources() // 渲染helm的模板文件
e.Render()
e.render() // 开始渲染
t := template.New("gotpl") // 使用 go语言 text/template 渲染模板
t.ExecuteTemplate()
i.cfg.KubeClient.Build() // 把渲染得到的yaml文件构建成k8s所需的资源格式
i.cfg.KubeClient.Create() // 调用k8s服务创建资源

可以看出,在执行install的过程中会检查k8s集群是否可用,这也是使用前注意事项的先决条件中要求一个Kubernetes集群的原因。

假设没有可用k8s集群,则在执行helm指令时可能会报错,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
➜ helm install demo ./helm-demo --debug --dry-run

install.go:222: [debug] Original chart version: ""
install.go:239: [debug] CHART PATH: /Users/xxxxxx/code/k8s/helm-demo

Error: INSTALLATION FAILED: Kubernetes cluster unreachable: Get "https://127.0.0.1:26443/version": dial tcp 127.0.0.1:26443: connect: connection refused
helm.go:84: [debug] Get "https://127.0.0.1:26443/version": dial tcp 127.0.0.1:26443: connect: connection refused
Kubernetes cluster unreachable

helm.sh/helm/v3/pkg/kube.(*Client).IsReachable
helm.sh/helm/v3/pkg/kube/client.go:135
helm.sh/helm/v3/pkg/action.(*Install).RunWithContext
helm.sh/helm/v3/pkg/action/install.go:231
main.runInstall
helm.sh/helm/v3/cmd/helm/install.go:314
main.newInstallCmd.func2
helm.sh/helm/v3/cmd/helm/install.go:156
github.com/spf13/cobra.(*Command).execute
github.com/spf13/cobra@v1.8.0/command.go:983
github.com/spf13/cobra.(*Command).ExecuteC
github.com/spf13/cobra@v1.8.0/command.go:1115
github.com/spf13/cobra.(*Command).Execute
github.com/spf13/cobra@v1.8.0/command.go:1039
main.main
helm.sh/helm/v3/cmd/helm/helm.go:83
runtime.main
runtime/proc.go:271
runtime.goexit
runtime/asm_amd64.s:1695
INSTALLATION FAILED
main.newInstallCmd.func2
helm.sh/helm/v3/cmd/helm/install.go:158
github.com/spf13/cobra.(*Command).execute
github.com/spf13/cobra@v1.8.0/command.go:983
github.com/spf13/cobra.(*Command).ExecuteC
github.com/spf13/cobra@v1.8.0/command.go:1115
github.com/spf13/cobra.(*Command).Execute
github.com/spf13/cobra@v1.8.0/command.go:1039
main.main
helm.sh/helm/v3/cmd/helm/helm.go:83
runtime.main
runtime/proc.go:271
runtime.goexit
runtime/asm_amd64.s:1695

调试模式下渲染模板

helm命令加上–debug –dry-run 两个参数,只输出模版渲染结果,不交给k8s处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
➜  k8s helm install demo ./helm-demo --debug --dry-run

install.go:222: [debug] Original chart version: ""
install.go:239: [debug] CHART PATH: /Users/ivanli/code/k8s/helm-demo

#=== chart信息,STATUS为执行install后当前chart的状态 ===#

NAME: demo
LAST DEPLOYED: Mon Jul 8 11:40:27 2024
NAMESPACE: default
STATUS: pending-install
REVISION: 1
USER-SUPPLIED VALUES:
{}

#=== 渲染模板时使用的数据 ===#

COMPUTED VALUES:
affinity: {}
autoscaling:
enabled: false
maxReplicas: 100
minReplicas: 1
targetCPUUtilizationPercentage: 80
fullnameOverride: ""
image:
pullPolicy: IfNotPresent
repository: nginx
tag: ""
imagePullSecrets: []
ingress:
annotations: {}
className: ""
enabled: false
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
livenessProbe:
httpGet:
path: /
port: http
nameOverride: ""
nodeSelector: {}
podAnnotations: {}
podLabels: {}
podSecurityContext: {}
readinessProbe:
httpGet:
path: /
port: http
replicaCount: 1
resources: {}
securityContext: {}
service:
port: 80
type: ClusterIP
serviceAccount:
annotations: {}
automount: true
create: true
name: ""
tolerations: []
volumeMounts: []
volumes: []

HOOKS:
---
# Source: helm-demo/templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "demo-helm-demo-test-connection"
labels:
helm.sh/chart: helm-demo-0.1.0
app.kubernetes.io/name: helm-demo
app.kubernetes.io/instance: demo
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['demo-helm-demo:80']
restartPolicy: Never


#=== 渲染后所有的清单 ===#

MANIFEST:
---
# Source: helm-demo/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: demo-helm-demo
labels:
helm.sh/chart: helm-demo-0.1.0
app.kubernetes.io/name: helm-demo
app.kubernetes.io/instance: demo
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
automountServiceAccountToken: true
---
# Source: helm-demo/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: demo-helm-demo
labels:
helm.sh/chart: helm-demo-0.1.0
app.kubernetes.io/name: helm-demo
app.kubernetes.io/instance: demo
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: helm-demo
app.kubernetes.io/instance: demo
---
# Source: helm-demo/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-helm-demo
labels:
helm.sh/chart: helm-demo-0.1.0
app.kubernetes.io/name: helm-demo
app.kubernetes.io/instance: demo
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: helm-demo
app.kubernetes.io/instance: demo
template:
metadata:
labels:
helm.sh/chart: helm-demo-0.1.0
app.kubernetes.io/name: helm-demo
app.kubernetes.io/instance: demo
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
spec:
serviceAccountName: demo-helm-demo
securityContext:
{}
containers:
- name: helm-demo
securityContext:
{}
image: "nginx:1.16.0"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{}

#=== template/NOTES.txt 文件内容 ===#
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=helm-demo,app.kubernetes.io/instance=demo" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT

总之,helm简化对 k8s 的操作与管理,非常值得一试。