1. 引言
Helm 已成为 Kubernetes 的事实上的包管理器,它简化了在 Kubernetes 集群上应用程序的部署和管理。通过将 Kubernetes 资源打包成图表(Chart),Helm 可以帮助我们快速且一致地部署复杂的应用程序。
在本文中,我们将探讨在部署中使用 Helm 图表的最佳实践。
2. 标签与选择器的有效使用
标签(Labels)和选择器(Selectors)是 Kubernetes 架构中的基础组件,它们允许我们对集群内的资源进行分组、筛选和管理。在 Helm 图表中有效使用标签,有助于资源管理,并提供对部署结构和状态的洞察。
此外,标签还允许我们为 Kubernetes 资源添加元数据,用于标识资源的用途、来源或其他对用户或工具有意义的属性。
2.1 Helm 模板中的标签
在图表中,标签对于组织资源至关重要,有助于从查询资源状态到基于这些标签实施策略等任务。
在 Helm 模板中定义资源时,必须包含有助于识别资源用途、来源或其他特性的标签。 同时,它们必须遵循促进互操作性和简化管理的通用标准。
来看一个在 Helm 模板中定义标签的示例:
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-pod"
labels:
app.kubernetes.io/name: "{{ .Chart.Name }}"
app.kubernetes.io/instance: "{{ .Release.Name }}"
app.kubernetes.io/version: "{{ .Chart.Version }}"
app.kubernetes.io/managed-by: "{{ .Release.Service }}"
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
可以看到,在 Helm 图表中使用一致的标签方案不仅有助于资源管理,也符合 Kubernetes 社区推荐的最佳实践。这使得我们的图表更直观、更容易协作。
2.2 使用选择器
选择器允许我们根据标签过滤和操作资源。
在 Helm 图表中,我们可以在各种 Kubernetes 对象中使用选择器来选择资源子集。
例如,Kubernetes 中的 Service 使用选择器来决定将流量路由到哪些 Pod:
apiVersion: v1
kind: Service
metadata:
name: "{{ .Release.Name }}-service"
spec:
selector:
app.kubernetes.io/name: "{{ .Chart.Name }}"
app.kubernetes.io/instance: "{{ .Release.Name }}"
ports:
- protocol: TCP
port: 80
targetPort: 9376
根据其标签,selector
字段指定了该 Service 应该将流量路由到哪些 Pod。这个 Service 现在将流量路由到标签匹配 app.kubernetes.io/name
和 app.kubernetes.io/instance
的 Pod,这些标签是在 Helm 模板中定义的。
2.3 标签管理的集中化
另一个最佳实践是将通用标签定义在 helpers.tpl
文件中。
通过这个单一文件,我们为整个 Helm 图表的标签定义了一个“唯一真实来源”(single source of truth)。 这种集中化方式使我们在应用程序演进和扩展时更容易更新和维护标签,确保所有资源的一致性。
来看一个 helpers.tpl
文件中定义标准标签的示例:
{{/*
标准应用标签
*/}}
{{- define "mychart.labels.standard" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/version: {{ .Chart.Version }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
这里的模板 mychart.labels.standard
包含了在 Kubernetes 部署中常见的基本标签。我们可以将 mychart
替换为我们图表的名称。
2.4 在资源模板中使用通用标签
在定义 Helm 图表中的 Kubernetes 资源时,我们现在可以使用 include
函数来包含这些通用标签。然后通过 indent
函数确保格式对齐。
来看一个典型示例:
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-service
labels:
{{ include "mychart.labels.standard" . | indent 4 }}
spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
selector:
{{ include "mychart.labels.standard" . | indent 4 }}
这里,include
函数从 helpers.tpl
中提取通用标签,并将其应用到 Service 的 metadata
和 selector
中。indent
函数确保其与 YAML 结构对齐。
3. 安全地管理 Helm Secret
Secret 管理对于应用程序部署至关重要,尤其是在我们使用 Helm 管理 Kubernetes 部署时。我们需要小心处理诸如密码、OAuth 令牌和 SSH 密钥 等敏感信息,以避免意外暴露。
然而,即使在 Helm 图表中,以明文形式存储 Secret 也是危险且不推荐的做法。 Kubernetes Secrets 提供了一种存储和管理敏感信息的方式。
但在使用 Helm 模板时,我们还需要额外措施来确保不会暴露 Secret。
3.1 使用 helm-secrets
和 SOPS
helm-secrets
插件结合 Mozilla 的 Secrets OPerationS (SOPS) 提供了一种安全地加密、解密和管理 Helm 图表中 Secret 的方法。
首先,我们需要安装 helm-secrets 插件:
$ helm plugin install https://github.com/jkroepke/helm-secrets
Downloading and installing helm-secrets v4.6.0 ...
https://github.com/jkroepke/helm-secrets/releases/download/v4.6.0/helm-secrets.tar.gz
Installed plugin: secrets
这表示我们成功将 helm-secrets
插件(版本 4.6.0)安装到了 Helm 环境中。
3.2 使用 SOPS 加密 Secret
Mozilla 的 SOPS 是一个用于安全管理包含 Secret 的文件的工具。
在使用 SOPS 与 Helm 之前,我们也需要安装它:
$ wget https://github.com/mozilla/sops/releases/download/v3.8.1/sops-v3.8.1.linux
$ chmod +x sops-v3.8.1.linux
$ sudo mv sops-v3.8.1.linux /usr/local/bin/sops
我们可以将 v3.8.1
替换为 SOPS 发布页面 上的最新版本。
安装完成后,我们现在可以加密我们的 secret 文件,例如一个 secrets.yaml
文件:
$ sops -e secrets.yaml > secrets.enc.yaml
这里,-e
标志告诉 SOPS 要加密内容。它读取我们的 secrets.yaml
文件,对其进行加密,并将加密后的数据输出到 secrets.enc.yaml
。我们现在可以安全地将这个加密文件保存在版本控制系统中。
3.3 使用 Helm 解密并应用 Secret
当我们部署 Helm 图表时,可以使用 helm-secrets
在运行时解密我们的 Secret。
然后,我们可以将其直接应用到 Kubernetes 集群中:
$ helm secrets upgrade -f secrets.enc.yaml myrelease mychart
Release "myrelease" has been upgraded. Happy Helming!
NAME: myrelease
LAST DEPLOYED: Thu Apr 6 14:35:49 2023
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None
输出确认了 release 的成功升级,而没有暴露任何敏感信息。
这种最佳实践确保了 Secret 在版本控制系统中以加密形式存储。仅在部署时必要时才进行解密。
4. 使用子图表管理依赖项
我们在 Kubernetes 上部署的应用程序通常由多个相互依赖的组件组成。Helm 图表可以封装这些复杂的应用程序,通过子图表(Subcharts)干净地管理其组件和依赖关系。这确保了所有组件能够协调一致地部署。
具体来说,Helm 通过 Chart.yaml
文件和 charts/
目录的组合来管理依赖项。 这允许我们将外部图表作为子图表打包,供我们的应用程序依赖。
例如,假设我们的应用程序需要一个后端服务和一个数据库,结构如下:
my-application-chart/
├── Chart.yaml
├── values.yaml
└── charts/
├── backend-service/
│ ├── Chart.yaml
│ └── values.yaml
└── database/
├── Chart.yaml
└── values.yaml
其中,my-application-chart/
是我们应用程序的主图表。它有两个依赖项(backend-service
和 database
)。charts/
目录中包含依赖于主应用程序的子图表(backend-service
和 database
)。
现在,要将这些子图表作为依赖项包含进来,我们需要在主图表的 Chart.yaml
文件中指定它们:
dependencies:
- name: backend-service
version: 1.2.3
repository: "@local" # 因为子图表位于本地 charts/ 目录中
- name: database
version: 4.5.6
repository: "@local"
在此配置中,dependencies
列出了主图表所依赖的所有外部图表,而子图表的详细信息则依次列出。repository
表示子图表的位置。值 @local
表示子图表存储在主图表的 charts/
目录中。但是,如果我们将子图表托管在远程仓库中,则应使用该仓库的 URL。
5. 资源策略管理
在 Helm 图表部署的生命周期中,我们需要管理资源的创建、更新和删除方式,以保持状态并确保数据持久性。作为最佳实践,Helm 引入了资源策略(Resource Policies),允许我们更细粒度地控制 Kubernetes 资源的生命周期。
值得注意的是,资源策略是我们可以添加到 Kubernetes 资源上的注解(Annotations),用于影响 Helm 在安装、升级和删除操作期间如何处理它们。最常见的用例是防止某些资源(如 PersistentVolumeClaims(PVCs))在卸载 Helm release 时被删除,从而保留关键数据。
例如,假设我们在 Helm 图表的 templates/pvc.yaml
文件中有一个 PVC 定义:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: {{ .Release.Name }}-pvc
annotations:
"helm.sh/resource-policy": keep
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
这里的 helm.sh/resource-policy
注解值为 keep
,告诉 Helm 在卸载 release 时不删除该资源。
然而,卸载后,由于 Helm 不再管理该 PVC,我们只能通过 kubectl 或其他 Kubernetes 管理工具手动处理与该 PVC 的未来交互(如删除或修改)。
6. 模板函数与值的使用
Helm 图表的强大和灵活性很大程度上来自于 Helm 提供的模板语言(基于 Go 模板构建)。这种模板机制允许我们创建可配置的部署,在不同环境和需求下只需最小的更改即可适应。
6.1 使用函数简化 Helm 图表
在 Helm 图表中使用模板函数是一种最佳实践,可以简化复杂逻辑、减少冗余并增强图表的可读性。
Helm 包含大量内置函数,并支持 Sprig 库 提供的所有函数。它还提供了超过 100 个额外的函数,用于字符串操作、数据转换、数学运算等。
例如,假设我们有如下 values.yaml
文件:
# values.yaml 文件
...
# ConfigMap 示例中使用的值
myValue: customValue
# ConfigMap 示例中用于相乘的数字
myNumber: 5
# 动态 Service 生成示例中定义的服务
services:
- name: web-service
type: LoadBalancer
port: 80
targetPort: 8080
# nodePort 是可选的,演示条件包含
- name: internal-service
type: ClusterIP
port: 8080
targetPort: 8080
nodePort: 30007
我们可以使用模板函数为 values.yaml
中的值设置默认值并对值进行操作:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
data:
myValue: {{ default "defaultValue" .Values.myValue | quote }}
myNumber: {{ .Values.myNumber | mul 2 | toString }}
这里,如果 values.yaml
中没有指定 myValue
,则默认为 defaultValue
。然后 myNumber
的值乘以 2
,展示了如何在使用前对数据进行操作。
6.2 逻辑操作
模板函数不仅限于简单操作。它们在执行复杂逻辑时也非常有用,例如根据值有条件地包含资源、遍历列表以及根据用户输入生成动态内容。
来看一个在模板中使用 range
循环和条件语句的示例:
{{- range .Values.services }}
apiVersion: v1
kind: Service
metadata:
name: {{ .name }}
spec:
type: {{ .type | default "ClusterIP" }}
ports:
- port: {{ .port }}
targetPort: {{ .targetPort }}
protocol: TCP
{{- if .nodePort }}
nodePort: {{ .nodePort }}
{{- end }}
{{- end }}
在这里,我们的模板为 values.yaml
中 services
列表中的每个条目生成一个 Service
资源。该列表定义了两个不同的 Kubernetes Service
资源。第一个服务(web-service
)类型为 LoadBalancer
,第二个(internal-service
)包含一个可选的 nodePort
。
这种最佳实践展示了 Helm 模板如何根据 values.yaml
文件中值的存在与否有条件地包含属性。通过这种方式,我们可以灵活地编写 Helm 模板,以生成动态的 Kubernetes 资源定义。
7. 使用 ConfigMap 和 Secret 实现自动更新
Kubernetes 部署中的一个常见挑战是,当配置(存储在 ConfigMap 或 Secret 中)发生变化时,自动更新应用程序。
如果没有适当的机制,我们对 ConfigMap 或 Secret 的更新不会自动触发依赖 Pod 的滚动更新。
实现自动更新的最佳实践是使用 ConfigMap 或 Secret 的校验和作为部署模板中的注解。这种方法确保了 ConfigMap 或 Secret 的任何更改都会触发 Pod 的滚动更新。
来看如何在 Helm 模板中实现这一点。
假设我们有一个 ConfigMap
(configmap.yaml
)位于 Helm 图表的 templates
目录中,并被我们的应用程序使用:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
data:
my-config-value: "Hello, World!"
接下来,我们可以修改我们的 Deployment 模板,添加一个注解,该注解是 ConfigMap 的校验和:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
spec:
replicas: 1
selector:
matchLabels:
app: my-application
template:
metadata:
labels:
app: my-application
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
containers:
- name: my-container
image: "my-image:latest"
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: {{ .Release.Name }}-config
在这个模板中,checksum/config
是 Deployment Pod 模板上的一个注解,其值是 ConfigMap 内容的 SHA-256 校验和。这是通过 Helm 的 include
函数完成的:它将 ConfigMap
模板渲染为字符串,sha256sum
函数计算校验和。
现在,当我们修改 ConfigMap
(例如更改 configmap.yaml
中的 my-config-value
)时,Deployment 注解中的校验和值将发生变化,因为 ConfigMap 的内容发生了变化。
然后,Kubernetes 会注意到 Deployment 的 Pod 模板发生了变化,并执行 Pod 的 滚动更新,以匹配最新的配置。
8. 使用 lookup
函数避免 Secret 重复生成
Helm 图表开发中的一个常见问题是管理那些一旦创建就不应被随意更改或重新生成的资源。Secret(尤其是包含密码或令牌的)就是典型例子。重新生成这些 Secret 可能会中断依赖于一致凭据的应用程序。
幸运的是,**lookup
函数允许我们在创建之前检查 Secret 是否已存在,从而有效避免不必要的重新生成。** lookup
函数还允许我们从图表模板中动态查询 Kubernetes 资源。
假设我们正在部署一个需要数据库密码(存储在 Kubernetes Secret 中)的应用程序。该应用程序的 Helm 图表应确保数据库密码在更新期间保持一致,除非我们显式更改或删除该 Secret。
为此,一个最佳实践是为我们的 Secret 创建一个模板文件(如 templates/secret.yaml
),并在其中使用 lookup
逻辑以防止重新生成:
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace "my-app-db-secret" }}
{{- if not $existingSecret }}
apiVersion: v1
kind: Secret
metadata:
name: my-app-db-secret
type: Opaque
data:
db-password: {{ randAlphaNum 12 | b64enc | quote }}
{{- end }}
在这里,我们检查是否已经存在名为 my-app-db-secret
的 Secret。如果不存在,则创建一个带有随机生成密码的 Secret。
在首次部署应用程序时,Helm 会创建该 Secret。
但有时我们可能需要升级应用程序、修改部署配置,或更新 Helm 图表中的应用程序版本。但由于 lookup
函数的存在,如果该 Secret 已经存在,Helm 将不会重新生成 my-app-db-secret
。这确保了我们的应用程序继续使用原始密码。
9. 测试 Helm 图表
Helm 提供了用于测试图表的最佳实践,最著名的是 helm test 命令。该命令允许我们将测试用例定义为 Kubernetes Pod 定义,这些 Pod 会对已部署的应用程序运行一组测试,然后报告结果。
具体来说,Helm 中测试用例的定义是一个 Kubernetes Pod,它执行一组特定操作,然后以成功或失败状态退出。我们使用 helm test
命令执行这些测试。它运行带有特定 Helm Hook(helm.sh/hook: test-success
)注解的 Pod。
来看一个定义并运行测试的示例,该测试检查我们 Helm 图表部署的服务的连通性:
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-test-connection"
labels:
purpose: "helm-test"
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: curl-container
image: curlimages/curl:latest
command: ['curl']
args: ['-s', '{{ include "mychart.serviceUrl" . }}']
restartPolicy: Never
在这里,该测试 Pod 试图连接到由 mychart.serviceUrl
模板定义的服务 URL。然后容器使用 curlimages/curl
镜像对该服务执行 curl 命令。如果 Pod 以状态码 0
退出(表示连接成功),则测试成功。
部署 Helm 图表后,我们现在可以使用 helm test [RELEASE_NAME]
运行测试:
$ helm test myapp
Pod myapp-test-connection pending
Pod myapp-test-connection running
Pod myapp-test-connection succeeded
NAME: myapp
LAST DEPLOYED: Thu Apr 6 14:22:35 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: myapp-test-connection
Last Started: Thu Apr 6 14:25:47 2023
Last Completed: Thu Apr 6 14:25:49 2023
Phase: Succeeded
NOTES:
...
在这个示例中,Helm 执行了 myapp
release 中定义的所有测试 Pod,这些 Pod 是我们在 Helm 图表中为 myapp
定义的,并带有特定的 Helm 测试 Hook。
正如我们所见,Phase: Succeeded
表示测试成功执行。
否则,我们通常会看到 Phase: Failed
。
最后,为了避免可能导致图表失败的常见陷阱,我们可能希望利用一个广为人知的图表仓库,例如 Bitnami,它托管了各种生产就绪的图表:
$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories
这个仓库由 Bitnami 维护,为各种应用程序提供可靠且最新的图表。
添加仓库后,我们可以 搜索一个现有图表,以满足我们的需求。
10. 总结
在本文中,我们探讨了使用 Helm 图表的最佳实践。我们涵盖了有效 Kubernetes 应用程序部署和管理所需的各种主题。
从利用 Helm 生态系统和使用子图表管理依赖项,到安全地管理 Secret 并确保幂等部署,这些最佳实践将提高我们 Helm 图表部署的可维护性、安全性和效率。