时间救星
Helm将一个应用所需要声明的所有Kubernetes资源,以模板的形式打包到一个文件夹下,然后通过预定义的或用户自定义的K-V文件进行渲染,最后排序后一并提交到Kubernetes执行。Helm还提供一个额外的抽象release,它存在的形式是ConfigMap
,记录每一次应用的发布。如果你对如何利用Kubernetes部署不同类型的应用没有经验,那么可以先行阅读官方文档。如果你无需了解如何使用Helm部署你的复杂应用,那么了解上述内容已经足够。
Helm & Kubernetes
Helm
是一款服务于Kubernetes生态的工具。根据官方文档的解释,Helm是一个管理Kubernetes Chart
的工具。而Chart是预配置Kubernetes资源的包。从功能上来说,Helm是一个生成预定义全栈应用
配置包,根据自定义的参数来渲染配置包,并在Kubernetes集群中声明对应的资源对象的工具。Chart全栈地定义了一个应用,不仅仅是应用本身,包括它依赖的DB、消息组件、一致性组件等其他应用,结合自定义的参数,Chart可以渲染为Kubernetes认识的资源对象描述文件,例如deployment.yaml。
熟悉Kubernetes的同学应该清楚,Kubernetes没有定义一个类型叫做Application
。Kubernetes倡导微服务架构,一个复杂应用应当是一个抽象的概念,由若干个松散耦合的微服务共同组成。虽然Kubernetes在DevOps上做得非常好,但对于用户来说,手动地定义、部署、运维若干个微服务,而不是一个应用,代价是相当高昂的。Helm的出现就是为了解决微服务架构的这个痛点,通过预定义文件
与自定义配置文件
的组合,来定义一个复杂应用,并在Kubernetes中声明它。
有了Helm、Chart、Kubernetes,我们可以做到类似一键建站
的敏捷性,并且在拆分应用、实践微服务架构的过程中,避开松散耦合带来的管理痛点。
使用Helm
先前条件:
安装Helm命令行工具,并在Kubernetes集群中安装Tiller。
-
编写或获取Charts
我们可以从Repository获取Charts,也可以从零开始或基于Repository的成品或半成品自定义自己需要的Charts。
自定义:➜ ~ helm create test-andy Creating test-andy ➜ ~ cd test-andy ➜ test-andy ls Chart.yaml charts templates values.yaml ➜ test-andy cat Chart.yaml apiVersion: v1 description: A Helm chart for Kubernetes name: test-andy version: 0.1.0 ➜ test-andy ls templates NOTES.txt _helpers.tpl deployment.yaml ingress.yaml service.yaml ➜ test-andy cat values.yaml # Default values for test-andy. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 ...
获取:
➜ ~ helm search wordpress NAME VERSION DESCRIPTION stable/wordpress 2.0.0 Web publishing platform for building blogs and ... ➜ ~ helm fetch stable/wordpress --untar=true # untar=true 可以解开 ➜ ~ ls wordpress Chart.yaml charts requirements.yaml values.yaml README.md requirements.lock templates
-
安装
helm可以通过命令
helm install
安装Charts或Charts压缩包.tgz
。可以在参数中使用自定义的values.yaml
文件或自定义的k-v参数,它们总是会覆盖Charts或.tgz
文件中的值。可以通过命令helm list
查看已经安装的release。➜ ~ helm install --name my-test-andy --set "key1=value1,key2=value2" ./test-andy
- 更新或卸载release
helm可以通过命令helm upgrade
和helm uninstall
来更新或卸载已经安装的release。➜ ~ helm upgrade my-test-andy test-andy --set "key1=value1,key2=value2" ➜ ~ helm uninstall my-test-andy
Charts与模板
Helm使用Chart模板加上预定义或自定义的值来渲染出所需要的.yaml
文件,通过Kubernetes创建对应的资源。模板与参数共同决定了Helm应用的行为。Helm支持标准Go模板语言,支持约60个来自Go Template Language
以及Sprig template library
的自定义函数,同样支持管道、条件语句等特性。本节将在介绍WordPress
模板的过程中,简要介绍Helm使用的模板语言,并介绍Chart的一些特性。
➜ wordpress tree
.
|____Chart.yaml # 该Chart的描述文件
|____LICENSE
|____README.md
|____charts # 依赖的Chart的集合文件夹
|____requirements.yaml # 描述当前Chart依赖的其他Chart
|____templates # 该Chart所需的Kubernetes对象模板
| |____deployment.yaml # deployment模板
| |____NOTES.txt # 该模板在release安装完成后会打印到控制台
| |____secrets.yaml # 储存secrect敏感配置信息的对象模板
| |____svc.yaml # service对象模板
| |____tests # 用于测试的pod对象描述集合
| | |____test-mariadb-connection.yaml # 用于测试的pod对象描述
|____values.yaml # 预定义的值,它们将被加入Value对象
如上所示,Chart目录实际上包含了定义以及安装WordPress应用所需要的所有元素。上述第一层文件目录中的的所有元素皆为Chart保留的文件及文件夹。其他的文件将被保留,不作处理。
除了应用运行时Kubernetes资源以外,Helm还额外增加了一些hook
、test
对象,在应用安装的过程中,以及应用运行时,执行任务,并验证、定位问题。这些对象实际上是打上annotations: "helm.sh/hook"
的KubernetesPod
或Job
,如下所示:
# 这是wordpress中的一个test,test-mariadb-connection.yaml
{{- if .Values.mariadb.enabled }}
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-credentials-test"
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: {{ .Release.Name }}-credentials-test
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}"
restartPolicy: Never
...
{{- end }}
hook会在Release生命周期中被执行,而test会在helm客户端执行命令helm test <ReleaseName>
被执行。
上述test中,多处使用到了Helm模板命令,如{{ .Release.Name }}
。它们都被{{
与}}
包裹。对.Release.Name
,开头的.
表示使用当前的上下文,Release
与Name
皆为对象名。Release为helm内置对象,类似的命名还有Template
,Capabilities
等,详见内置对象简介。其他的引用方式,例如.Value.image.registry
,则表示Values对象下的image对象的registry对象。Value对象来自value.yaml
以及命令行的--set
以及--values
定义的值,它们生效的优先级遵循顶层优先原则,后申明的值会覆盖先声明的值。
更加复杂一点的语法还有类似Unix系统的管道,函数,以及IF/ELSE
、RANGE
控制的模板块。详见文档。
实际上,从应用的角度,我们需要设计并实现两个Helm应用,WordPress与数据库。你当然可以在一个Chart中完整地描述这两个组件,不过在所有的应用中都描述一次数据库显然显得过于冗杂。Helm提供一种更优雅的方式来解决这个问题:专注设计WordPress,以依赖的方式在WordPress中引用数据库Helm。
➜ wordpress cat requirements.yaml
dependencies:
- name: mariadb
version: 4.x.x
repository: https://kubernetes-charts.storage.googleapis.com/
condition: mariadb.enabled
tags:
- wordpress-database
如上所示,在requirements.yaml
中声明依赖,helm将在repository
中下载对应的chart到./charts
下。
➜ wordpress cd charts
➜ charts tree
.
|____mariadb
| |____.helmignore
| |____Chart.yaml
| |____OWNERS
| |____README.md
| |____templates
| | |_____helpers.tpl
| | |____master-configmap.yaml
| | |____master-statefulset.yaml
| | |____master-svc.yaml
| | |____NOTES.txt
| | |____secrets.yaml
| | |____slave-configmap.yaml
| | |____slave-statefulset.yaml
| | |____slave-svc.yaml
| | |____test-runner.yaml
| | |____tests.yaml
| |____values-production.yaml
| |____values.yaml
最后,这里给出一个在阿里云环境安装WordPress的解决方案。在阿里云环境,容器服务打通了自动购买SLB的流程,但没有打通购买持久化储存的流程,因此,首先需要购买NAS并在集群中声明PersistenceVolume对象,如下所示。
➜ helm-test cat wp-nas.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: wp-nas
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 40Gi
flexVolume:
driver: alicloud/nas
options:
path: /wp
server: {{ mountDomain }} # 挂载到交换机上的域名
vers: "4.0"
storageClassName: nas
➜ helm-test kubectl apply -f wp-nas.yaml
然后执行下面的命令部署helm。
➜ helm-test helm fetch --untar stable/wordpress
➜ helm-test helm install --name wordpress-test --debug --set "persistence.enabled=false,mariadb.master.persistence.storageClass=nas" wordpress
➜ helm-test kubectl get deployment wordpress-test-wordpress
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
wordpress-test-wordpress 1 1 1 1 1m
至此,在阿里云容器服务下创建你的WordPress应用圆满成功。
Helm整体架构与实现分析
跟大部分其他的Kubernetes生态工具一样,Helm也将自身的服务端沉入Kubernetes中,成为一个命名空间为kube-system的应用(Tiller),并提供外部的命令行工具与Tiller进行交互。因此,Helm从架构上来说,分为两大块:Helm-client
,Tiller-server
。在介绍这两块之前,先明确几个概念。
Chart: Chart是定义一个Kubernetes应用的信息的集合。
Config: Config包含了一系列配置信息。将这些配置信息与Chart组合,可以用来在Kubernetes中创建一个Release。
Release: Release上述Chart与Config组合的运行时实例,可以理解为Kubernetes中一个抽象的应用。Release包含Helm创建的应用的元数据信息。
Repository: Repository是一个存放发布Chart的仓库,开发者可以将自制的Chart发布到仓库分享。
Helm-client是一个命令行工具,它负责如下工作:
- 在本地实现Chart的研发。
- 配置管理Chart仓库Repository。
- 与Tiller进行交互,包括上传需要安装的Chart,获取Release运行时信息,更新或卸载已经存在的Release。
Tiller-server是一个Kubernetes集群中的应用,它负责如下工作:
- 与Helm-client交互,响应client的请求。
- 将Chart于Config融合为一个可以创建运行在Kubernetes集群中的Release的资源对象。
- 创建相关的Release。
- 对Release进行更新或卸载。
Helm的实现完全使用了与Kubernetes相同的技术栈,使用Go语言实现客户端与服务端,采用gRPC作为客户端服务端交互的协议,服务端与Kubernetes集×××互采用官方提供的REST API,没有采用hack的方式,越过Kubernetes API Server直接操作Kubernetes集群。除了Kubernetes,Tiller-server是一个无依赖无状态的应用,它的状态依靠Kubernetes中的ConfigMap来实现持久化,储存release的元数据与状态,不需要额外的DB。
从上述的介绍来看,Helm是一个轻量级的,标准化的,完全依附于并服务于Kubernetes生态的一个工具。我们甚至可以在短时间之内重新设计一个类似的产品,在Kubernetes外部来实现相同的能力。
下面以一次创建为例,说明Tiller的实现(源码可以在pkg/tiller/release_install.go#InstallRelease
中看到):
-
- prepare
-
- validation: 验证命名唯一以及资源用量。
... name, err := s.uniqName(req.Name, req.ReuseName) ... caps, err := capabilities(s.clientset.Discovery()) ...
- validation: 验证命名唯一以及资源用量。
-
- renderResources: 解析依赖,利用values.yaml文件生成的k-v来渲染Charts,生成hooks以及manifest对象,最后结合为Release对象。hooks、manifest中Kubernetes资源编排顺序由两个对象来确定:
pkg/tiller/kind_sorter.go#UninstallOrder
、pkg/tiller/kind_sorter.go#InstallOrder
,相同类型的资源按命名排序(这个顺序对于Kubernetes系统来说其实并不太敏感)valuesToRender, err := chartutil.ToRenderValuesCaps(req.Chart, req.Values, options, caps) ... // 将chart与values融合为manifest,也就是可以被k8s认识的一系列k8s资源的排列 hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender, caps.APIVersions) //生成release对象,跟踪它的版本、行为、状态等信息 rel := &release.Release{ ... }
- renderResources: 解析依赖,利用values.yaml文件生成的k-v来渲染Charts,生成hooks以及manifest对象,最后结合为Release对象。hooks、manifest中Kubernetes资源编排顺序由两个对象来确定:
-
- prepare
-
- performRelease
-
- 执行hooks。包括自定义资源的install-hooks以及pre-install-hooks,并在过程中再次验证manifest是否正确。
if !req.DisableHooks && !req.DisableCrdHook { if err := s.execHook(r.Hooks, r.Name, r.Namespace, hooks.CRDInstall, req.Timeout); err != nil { fmt.Printf("Finished installing CRD: %s", err) return res, err } } ... if err := validateManifest(s.env.KubeClient, req.Namespace, manifestDoc); err != nil { return res, fmt.Errorf("validation failed: %s", err) } ... if !req.DisableHooks { if err := s.execHook(r.Hooks, r.Name, r.Namespace, hooks.PreInstall, req.Timeout); err != nil { return res, err } }
- 执行hooks。包括自定义资源的install-hooks以及pre-install-hooks,并在过程中再次验证manifest是否正确。
-
- 2. 判断该次操作是创建或更新,调用相应的Kubernetes-client端接口,记录release信息。 ```go switch h, err := s.env.Releases.History(req.Name); // 资源存在,更新操作,记录release case: case req.ReuseName && err == nil && len(h) >= 1: if err := s.ReleaseModule.Update(old, r, updateReq, s.env); err != nil { ... s.recordRelease(old, true) s.recordRelease(r, true) ... } // 资源不存在,创建,记录release default: if err := s.ReleaseModule.Create(r, req, s.env); err != nil { ... s.recordRelease(r, true) ... }
-
- 执行post-install-hooks。
if err := s.execHook(r.Hooks, r.Name, r.Namespace, hooks.PostInstall, req.Timeout); err != nil { ... s.recordRelease(r, true) ... }
- 执行post-install-hooks。
- performRelease
其他的Tiller能力实现与install大同小异,基本都为渲染hooks、manifest、release对象,然后记录并执行release,通过调用Kubernetes REST API,在Kubernetes中声明相应的资源即可。
Helm实现的缺陷
Helm这款工具非常简单优雅,在社区内几乎没有对它的诟病。但是作者个人认为,它的依赖部分的设计存在一些不足之处。由于水平有限,欢迎各位读者指正。
Helm对依赖的理解,事实上是将依赖的组件,例如数据库、中间件、鉴权组件等,打包到应用的整个描述文件中,部署一个由当前应用独占的资源。然而在现实中大部分场景下,这类组件一般不是应用独占的,这会造成很大的浪费。Helm的设计,应当允许用户选择另一种依赖形式:以非独占的方式,依赖这类型可以复用的Kubernetes的资源。
举个例子:A应用依赖并部署了一个MQ组件,MQ组件暴露出服务service-mq,那么B应用完全可以复用该MQ组件,使用service-mq暴露的服务。
我们当然可以采用一些tricks来解决这个问题,例如将共同的组件抽取出来,单独部署,然后在依赖它的应用中通过硬编码或者动态配置的方式注入组件的信息。然而这种方式从一定程度上违背了Helm一键部署全栈应用的思路。公用组件应该是作为应用的依赖部署的,而不是单独部署的,否则Helm就失去了定义一个全栈应用的意义,不断拆分的服务将重新令Kubernetes应用的部署运维变得复杂。在上述场景中,可以在B应用部署时,通过依赖检查,可以发现存在一个符合要求(镜像名+版本+允许共享+...)的MQ组件,并且它暴露了服务,那么B应用就不会去创建一个新的MQ组件,从而将资源节约出来。当然,这种设计方式,有很多问题需要解决,例如如何在两个应用之间共享公共组件的描述信息以及配置信息。
上述设计,既可以满足用户一键部署全栈应用的需求,也可以满足用户希望复用组件,节约成本的诉求。
总结
在Kubernetes生态中,通过Helm完成对全栈应用的定义,可以很容易做到一键建站。
几乎每一个开源产品都有值得我们学习的点,发现这些点其实并不难,将开源产品优秀的思想、设计与自身从事的产品研发工作相结合,找到可以真正实现的改进的点,才是最困难的。希望在以后的工作与学习中,可以不忘初心,尽可能跟上社区的脚步。
文章的部分观点限于作者水平,可能存在客观错误或不足之处,尚希各位读者指正。
转载,请联系:微信(zjjxg2018)
有疑问加站长微信联系(非本文作者)