从基础到高级:Helm 指南
编辑:bot
技术校对:星空下的文仔、小君君
如果你熟悉 apt / yum / Homebrew 及其在不同操作系统中的作用,你应该明白软件包管理器的重要性。随着 Kubernetes 奠定了自己在容器编排上的地位,它也亟需一种包管理器来提高开发人员工作效率、降低应用部署复杂性、加快云原生应用程序的采用。 这就是我们今天的要介绍的工具——Helm。
入门 Helm,首先要了解它是什么,有什么用。
Helm 能帮你管理 Kubernetes 应用程序:Helm Chart 可帮你定义、安装和升级最复杂的 Kubernetes 应用程序。—— https://helm.sh/
2018 年 6 月,云原生计算基金会(CNCF)技术监督委员会(TOC)投票决定接受 Helm 作为孵化级托管项目,标志着 Helm 已经成为 Kubernetes 包管理的 de facto。
Helm 简介
众所周知,Kubernetes 是一个功能强大且流行的容器编排系统,但把应用程序部署到上面并不简单。哪怕只是部署单个应用程序,它也会涉及创建多个相互依赖的 Kubernetes 资源,如 Pod、Service、Deployment 和 ReplicaSet 等。每个资源都要求编写详细的 YAML 清单文件。
这意味着我们在实践中会面临诸多挑战:
如何管理、编辑和更新这些分散的 Kubernetes 应用配置文件;
如何把一套相关的配置文件作为一个应用进行管理;
如何分发和重用 Kubernetes 的应用配置。
Helm 就是为解决这些问题而生的。
Helm 的操作基于两个主要组件的协作:一个名为 helm
的命令行工具和一个名为 tiller
的服务端组件(必须在它管理的集群上运行)。
Helm 部署的基本构建块是 Helm Chart,它负责封装 Kubernetes 原生应用程序的 YAML 文件,可以本地存储,也可以从远程 Chart 仓库中获取。
安装 Helm
要安装命令行工具 helm
,我们可以用受支持的软件包管理器,或者只需下载预编译的二进制文件:
# Brew
$ brew install kubernetes-helm
# Choco
$ choco install kubernetes-helm
# Gofish
$ gofish install helm
安装完成后,输入 helm init
,我们就能在 Kubernetes 群集上安装 Tiller:
# Select the Kubernetes context you want to use
$ kubectl config use-context docker-for-desktop
$ helm init
注:在集群上启用 RBAC 后,我们可能要为 tiller
Pod 设置适当的权限。
Helm repo
安装完 Helm 后,我们不需要本地就有可用的 Chart,因为 Helm 可以进行远程仓库管理,它提供一个默认仓库 stable
,但你也可以视情况添加、删除仓库。
$ helm repo add banzaicloud-stable https://kubernetes-charts.banzaicloud.com/branch/master
$ helm repo list
NAME URL
stable https://kubernetes-charts.storage.googleapis.com
banzaicloud-stable https://kubernetes-charts.banzaicloud.com/branch/master/
注:上面有一个国内不存在的 URL,记得先搭好梯子。
如果要把 Chart 安装到集群,只需指定 Chart 的名称和可选的自定义配置值:
# Install with default values
$ helm install banzaicloud-stable/logging-operator
# Install with custom yaml file
$ helm install banzaicloud-stable/logging-operator -f example.yaml
# Install with value overrides
$ helm install banzaicloud-stable/logging-operator --set rbac.enabled=false
创建新 Chart
使用 Helm 包非常简单,但编写 Helm Chart 也不难,只是稍微复杂一些。
有时,如果找不到要部署软件的现有 Chart,我们可能得自己创建一个。要创建新 Chart,我们应该使用内置的 helm create
命令。如下所示,它包含一些基本模板和示例:
$ helm create my-app
Creating my-app
$ tree -d my-app
my-app
├── Chart.yaml
├── charts
├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── ingress.yaml
│ └── service.yaml
└── values.yaml
在这个名为 my-app
的 Chart 的目录中:
Chart.yaml
包含 Chart 的元数据,包括名称、描述信息以及版本等;templates
是 Kubernetes 清单文件目录,用于描述我们希望在集群上拥有的资源,它默认使用 Go Template 语法;values.yaml
常见于大多数 Chart,提供 Chart 的默认配置值。
有用的功能
Helm 之所以好用,它的核心竞争力在于模板。Golang 模板的阅读体验一开始其实不太好,但它通过一些有用的功能弥补了这一点。
伪随机
刚开始接触 Helm 的工程师可能都有这样的疑问:“为什么 Helm 连生成随机字符串这么简单的事都不会?”
这个问题的关键在于,如果我们为每次安装(升级)都生成一个随机字符串,那么该字符串每次都会覆盖现有值,这会导致不必要的噪声和其他问题。
为了避免这些,我们可以使用 derivePassword
。它基于多个输入源,能为为每次运行生成相同的稳定输出:
sharedKey = "{{ .Values.tls.sharedKey | default (derivePassword 1 "long" (.Release.Time | toString) .Release.Name .Chart.Name ) }}"
生成 TLS 证书
使用加密通道是组件之间通信的标准方法,但是对于测试和开发,我们可能不希望用完整的 PKI 来发布测试证书。对此,一种解决方案是对预先生成的证书进行硬编码,但它既不易维护也不优雅。所以我们可以用 Sprig,它提供开箱即用的自签名证书支持。
注:建议不要在生产环境中使用自签名服务器证书。
以下是我们的 Logging Operator Helm Chart 中的示例:
{{- if and .Values.tls.enabled (not .Values.tls.secretName) }}
{{ $ca := genCA "svc-cat-ca" 3650 }}
{{ $cn := printf "fluentd.%s.svc.cluster.local" .Release.Namespace }}
{{ $server := genSignedCert $cn nil nil 365 $ca }}
{{ $client := genSignedCert "" nil nil 365 $ca }}
apiVersion: v1
kind: Secret
metadata:
name: {{ template "logging-operator.fullname" . }}
labels:
app: {{ template "logging-operator.name" . }}
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
heritage: {{ .Release.Service }}
release: {{ .Release.Name }}
data:
caCert: {{ b64enc $ca.Cert }}
clientCert: {{ b64enc $client.Key }}
clientKey: {{ b64enc $client.Cert }}
serverCert: {{ b64enc $server.Cert }}
serverKey: {{ b64enc $server.Key }}
{{ end }}
标签、注释和其他属性
无论是用过的还是没用过的,我们应该尽可能记住 Kubernetes 的功能,以此辅助自定义部署。而为了更便捷地做到这一点,我们可以用标签,注释等。你可以用自己的属性,但也要确保终端用户能用他们的属性。运行时覆盖对象(如注释和标签)的最佳方法是将值呈现为 YAML 或 JSON 。
空值示例:
nodeSelector: {}
tolerations: []
affinity: {}
Pod 描述文件中的示例模板:
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
唯一的资源名称
_helpers.yaml
文件中包含着一些有用的功能,通过使用这些功能来构建资源名称,我们能轻松创建可以部署在多个实例中的 Chart。
注:但是,对于不同的单独部署,用不同的资源名称还是更好一些。
创建自定义的 Chart
要创建灵活的 Helm Chart,我们需要为用户提供覆盖生成和默认值的机会。这有助于将 Chart 部署到不同的环境,并有助于在 Chart 顶部构建 umbrella Chart。
官方 Chart 仓库中的许多 Chart 都是用于创建更高级应用程序的“构建块”。但 Chart 也可被用于创建大型应用程序的实例。在这种情况下,单个 umbrella Chart 可能有多个子 Chart,每个子 Chart 都是整体的一部分。
示例:用 _configMapOverrideName
值处理自定义配置:
- name: config-volume
configMap:
name: {{ if .Values.configMapOverrideName }}{{ .Release.Name }}-{{ .Values.configMapOverrideName }}{{- else }}{{ template "my-app.fullname" . }}{{- end }}
创建 umbrella Chart
要创建由一个或多个其他 Chart 构建的 Chart,请参阅 requirements.yaml
文件中明确定义的从属 Chart 版本。
例如,如果要在应用程序中安装 MySQL Chart,我们应该创建这样一个requirements.yaml
:
dependencies:
- name: mysql
version: 0.7.1
repository: alias:banzaicloud-stable
condition: mysql.enabled
当 mysql.enabled
值为 TRUE 时,上述代码会把固定版本的 MySQL Chart 安装到自定义仓库。
你也可以覆盖 values.yaml
文件中的默认值。
mysql:
enabled: true
nameOverride: my-example-db
mysqlDatabase: example
使用长文件作为配置映射
有时,我们可能不想用模板,而是希望将长文件用作 Kubernetes configmap。这时我们可以用 Files.Get
,把这些文件包含在一个或多个 configmap 清单中。
{{- if .Values.grafana.dashboard.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "logging-operator.fullname" . }}-grafana-dashboard-logging
labels:
pipeline_grafana_dashboard: "1"
data:
logging.json: |-2
{{.Files.Get "grafana-dashboards/logging-dashboard_rev1.json"| indent 4}}
{{- end }}
注:路径是 Chart 根目录中的相对路径。
设计原则
这一节我们要介绍的不是功能要求,而是设计指南。如果我们不能保证 Helm Chart 的一致和整洁,它们很容易变得既笨拙又复杂。
值
值是任何 Helm 部署的主要定制点,它必须易于定制,且易于部署。所以在实践中,我们通常应该遵循以下规则:
用尽可能少的层次结构,避免冗余;
使用层次结构,而不是前缀/后缀;
命名一致。
用功能切换条件
深入了解 Helm 功能时,我们会发现有很多东西是捆绑在一起的。一个很好的例子就是用 TLS 证书进行集群内通信,这时我们必须改变几个相辅相成的属性:
service:
tls:
enabled: true
...
这一小段代码完全改变了以下这些组件的行为:
Ingress 的前端、后端 Scheme 和 Probe;
ports:
- port: {{ .Values.service.externalPort }}
targetPort: {{ .Values.service.internalPort }}
protocol: TCP
{{- if .Values.service.tls }}
name: "https-{{ .Values.service.name }}"
{{- else }}
Service Scheme 和 Probe;
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.externalPort }}
targetPort: {{ .Values.service.internalPort }}
protocol: TCP
{{- if .Values.service.tls }}
name: "https-{{ .Values.service.name }}"
{{- else }}
name: "{{ .Values.service.name }}"
{{ end }}
Readiness 和 Liveness Probe;
livenessProbe:
httpGet:
path: {{ .Values.pipelineBasepath }}/api
port: {{ .Values.service.internalPort }}
{{- if .Values.service.tls }}
scheme: HTTPS
{{ end }}
initialDelaySeconds: 15
readinessProbe:
httpGet:
path: {{ .Values.global.pipelineBasepath }}/api
port: {{ .Values.service.internalPort }}
{{- if .Values.service.tls }}
scheme: HTTPS
{{ end }}
initialDelaySeconds: 10
Mount Secret。
{{- if .Values.service.tls }}
- name: tls-certificate
mountPath: /tls
{{ end }}
使用 NOTE.txt
NOTES.txt
的作用是为部署 Chart 的用户提供信息,它也是模板化的,所以我们可以为它们提供一些有用的信息,以帮助它们开始使用已部署的应用程序。通常,我们应该以用户最可能需要的格式输出创建的端点。
下面是允许用户访问已创建服务的示例代码:
POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "prometheus.name" . }},component={{ .Values.pushgateway.name }}" -o jsonpath="{.items[0].metadata.name}")
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 9091
README
当我们编写 README 文件的时候,Chart 应该已经创建好了。一篇好的 README 通常包括 Chart 的简要说明、一些示例以及有关安装选项的一些详细信息。
检查你的 Chart
到这里,不要忘了检查 Chart 是否存在语法错误,这时检查比部署后再检查容易得多。
$ helm lint my-chart
==> Linting my-chart
Lint OK
1 chart(s) linted, no failures
调试
最后就是调试。要了解 Helm 渲染模板后会发生什么,我们可以使用 --debug
和 --dry-run
。
$ helm install chart-name --debug --dry-run
我安装了什么
要看自己干了什么,获取正在运行的部署使用的值可能会派上用场。
$ helm get values release-name
到这里,文章就结束了。读罢这篇关于 Helm 的短文,希望你能觉得有用、有趣,并愿意亲自动手去试一试。
参考文献
1.https://banzaicloud.com/blog/creating-helm-charts/
2.http://www.10tiao.com/html/357/201808/2247486154/1.html
3.https://www.digitalocean.com/community/tutorials/an-introduction-to-helm-the-package-manager-for-kubernetes
END
推荐阅读:
K8S 网络插件(CNI)超过 10Gbit/s 的基准测试结果
KubeCon 直击:etcd 正式成为 CNCF 孵化项目
KubeCon 中国首秀|全面解读 7 大 Keynote 带你看穿 K8S 新时代
当当网专家详述如何利用 K8S 构建自主可控的 FaaS 平台