Helm是Kubernetes的包管理工具,号称是K8s的apt-get,听起来很美好,用起来却不是那么回事。

我从最开始满怀期待地入门,觉得找到了管理K8s应用的神器,到中间踩了无数的坑,被各种问题折磨得死去活来,再到最后差点放弃,改用其他方案,经历了一段跌宕起伏的历程。

今天这篇文章,就来吐槽一下我用Helm踩过的那些坑,从入门到放弃,我到底经历了什么,也给准备用Helm的朋友,提个醒,少走点弯路。当然,最后我没有真的放弃,还是坚持用下来了,但中间的辛酸,真的是一言难尽。

一、入门:觉得Helm真是个好东西

最开始接触Helm的时候,觉得这真是个好东西。

那时候,我们刚把应用迁移到K8s上,部署应用很麻烦,要写一大堆YAML文件,Deployment、Service、Ingress、ConfigMap,一个应用就要写好几个文件,而且很多内容都是重复的,改个名字,改个镜像地址,就要改好几个地方,很容易出错,也很难维护。

然后就有人推荐了Helm,说这是K8s的包管理工具,能把应用打包成Chart,用模板来管理YAML,一键部署,一键升级,特别方便。

我一听,这不就是我想要的吗?赶紧去学,花了一个周末,看了官方文档,跟着教程,做了一个简单的Chart,部署了一个应用,一键就上去了,当时觉得,哇,太方便了,这东西真是神器,以后部署应用,再也不用写一堆YAML了。

那时候的我,对Helm充满了期待,觉得找到了管理K8s应用的终极方案,甚至还在团队里分享,推荐大家都用Helm。

现在想想,那时候真是太年轻了,不知道后面有多少坑在等着我。

二、第一个坑:Tiller,让人又爱又恨

用了Helm 2之后,第一个遇到的大坑,就是Tiller。

Helm 2的架构,是客户端+服务端,客户端叫Helm,服务端叫Tiller,部署在K8s集群里,Helm客户端通过gRPC和Tiller通信,Tiller再去操作K8s API。

这个架构,听起来没问题,但实际用起来,问题一大堆。

问题1:Tiller的权限太大,有安全风险

Tiller要管理整个集群的资源,所以需要很大的权限,一般都会给它集群管理员的权限。这就意味着,只要能访问Tiller,就能控制整个集群,安全风险很大。

我们那时候,就因为Tiller的权限配置不当,导致一个普通用户,通过Helm就能删除整个命名空间的资源,差点出了大事。后来花了很多时间,才把Tiller的权限收紧,做了细粒度的控制。

问题2:Tiller很容易挂

Tiller是部署在K8s里的一个Pod,它也会挂,也会出问题。我们就遇到过好几次,Tiller的Pod因为内存不足或者其他原因,挂掉了,结果整个Helm都用不了,所有的部署和升级都停了,只能先去修Tiller。

而且,Tiller挂了之后,它管理的Release状态,可能会丢失或者错乱,导致你不知道当前部署的是什么版本,升级也升不了,回滚也回不了,非常头疼。

问题3:Tiller和Helm客户端的版本要对应

Helm 2还有一个很坑的地方,就是客户端和服务端的版本,必须严格对应,大版本小版本都要一致,不然就会报错,用不了。我们就遇到过,有人升级了本地的Helm客户端,结果连不上集群里的Tiller,因为版本不匹配,又要降级客户端,或者升级Tiller,很麻烦。

因为这些问题,我们那时候,对Tiller真是又爱又恨,用Helm带来的便利,有一半都被Tiller的问题抵消了。后来Helm 3去掉了Tiller,变成纯客户端架构,我们第一时间就升级了,终于摆脱了Tiller的噩梦。

三、第二个坑:模板语法,反人类的Go Template

Helm用的是Go Template模板语法,这个语法,真的是反人类,写起来很痛苦,调试起来更痛苦。

问题1:语法晦涩,不好写

Go Template的语法,和我们平时写的代码,差别很大,什么{{ .Values.name }},什么{{- if .Values.enabled }},什么{{ include "xxx" . }},刚开始写的时候,经常写错,不是少了个点,就是少了个横杠,或者缩进不对,导致渲染出来的YAML是错的。

尤其是循环和条件判断,写起来很绕,比如要遍历一个列表,生成多个资源,要写{{- range .Values.items }},然后里面还要用{{ . }}来引用当前元素,很容易搞混,不知道这个.指的是什么。

问题2:调试困难,不知道哪里错了

模板写错了,Helm会报错,但报错信息经常很模糊,只告诉你渲染失败了,不告诉你具体是哪一行错了,为什么错了。你只能一行一行地去看模板,去猜哪里写错了,非常耗时。

我们那时候,经常为了一个模板错误,调试半天,最后发现就是少了个横杠,或者多了个空格,真的是欲哭无泪。

后来学会了用helm template命令,先把模板渲染出来,看渲染后的YAML对不对,才稍微好一点,但还是很麻烦。

问题3:YAML的缩进,噩梦

Helm模板,最终要渲染成YAML,而YAML对缩进非常敏感,多一个空格,少一个空格,就会导致YAML格式错误。

在模板里控制缩进,真的是噩梦,尤其是用include或者range的时候,缩进很容易乱,渲染出来的YAML,要么缩进不对,要么格式错了,要反复调试,才能把缩进调对。

我们那时候,有个Chart,因为缩进的问题,调试了整整一天,最后发现就是一个nindent函数用错了,应该用nindent 4,写成了indent 4,就差了一个字母,折腾了一天。

四、第三个坑:版本管理,混乱不堪

Helm的版本管理,也是一个大坑。

问题1:Release名字不能重复,也不能复用

Helm的Release名字,在一个命名空间里,不能重复,而且,即使你删除了一个Release,这个名字也不能马上复用,因为Helm会保留删除的记录,要过一段时间,或者手动清理,才能复用。

我们就遇到过,删除了一个Release,想重新用同一个名字部署,结果报错说名字已经存在,查了半天,才发现是删除的记录还在,要手动用helm uninstall --purge才能彻底删除,真的是坑。

问题2:版本号混乱,不知道当前是什么版本

Helm的Release,每次升级都会生成一个新的版本号,从1开始,依次递增。但这个版本号,和我们应用的版本号,是两回事,经常搞混。

比如,我们的应用版本是v1.2.3,但Helm的Release版本可能是第5次升级,版本号是5,这两个版本号,完全对不上,管理起来很混乱。有时候想回滚到某个应用版本,还要去查对应的Helm版本号是多少,很麻烦。

问题3:回滚不靠谱

Helm支持回滚,用helm rollback命令,就能回滚到之前的版本。但这个回滚,有时候不靠谱,尤其是当Chart里有不可变的资源,或者有外部依赖的时候,回滚可能会失败,或者回滚之后,状态不对。

我们就遇到过,升级的时候,改了数据库的结构,后来想回滚,结果数据库结构改不回去了,回滚之后,应用启动失败,只能手动去修数据库,非常狼狈。

所以,现在我们用Helm回滚,都很谨慎,尤其是涉及到数据库和有状态服务的时候,不敢随便回滚。

五、第四个坑:依赖管理,剪不断理还乱

Helm支持Chart依赖,一个Chart可以依赖其他的Chart,这个功能,听起来很好,但实际用起来,也是问题一堆。

问题1:依赖版本冲突

当一个Chart依赖多个子Chart,而这些子Chart又依赖了同一个Chart的不同版本,就会出现版本冲突,Helm不知道该用哪个版本,会报错,或者用错版本。

我们就遇到过,一个项目依赖了A和B两个Chart,A依赖了redis的3.0版本,B依赖了redis的4.0版本,结果部署的时候,redis的版本混乱,不知道用的是哪个,出了问题,查了半天才发现是依赖冲突。

问题2:依赖更新不及时

子Chart更新了,父Chart不会自动更新,要手动去更新依赖,而且更新的时候,还要测试兼容性,很麻烦。很多时候,子Chart已经修复了bug,但父Chart还在用旧版本,导致bug一直存在。

问题3:依赖的配置传递复杂

父Chart要给子Chart传配置,要通过global或者子Chart的values来传,这个传递机制,很复杂,也很容易出错。经常出现,配置传不过去,或者传错了,导致子Chart用了默认配置,不是我们想要的。

因为这些问题,我们后来尽量减少Chart依赖,能合并的就合并,能不用依赖就不用,省得麻烦。

六、第五个坑:升级和迁移,惊心动魄

用Helm,最惊心动魄的,就是大版本升级,比如从Helm 2升级到Helm 3,这个过程,真的是一把辛酸泪。

Helm 2到Helm 3的迁移

Helm 3去掉了Tiller,架构变了,数据格式也变了,从Helm 2升级到Helm 3,不是直接升级就行,要做数据迁移,把Helm 2的Release数据,转换成Helm 3的格式。

官方提供了一个迁移插件,但这个插件,有很多坑,比如有些特殊的Release迁移失败,有些数据丢失,有些状态错乱。我们迁移的时候,就遇到了好几个Release迁移失败,只能手动去处理,花了整整一周,才把所有的Release迁移完,中间还差点出了生产事故。

而且,迁移之后,还要测试每个应用是不是正常,有没有配置丢失,有没有状态错误,整个过程,提心吊胆,生怕出问题。

Chart版本升级

除了Helm本身的升级,Chart的升级,也很麻烦。很多开源的Chart,大版本升级的时候,会有不兼容的变化,比如values的结构变了,模板变了,直接升级会失败,或者部署出来的应用不对。

我们就遇到过,升级一个开源Chart的大版本,结果values的结构完全变了,我们之前的配置都用不了了,只能重新写配置,重新测试,花了很多时间。

所以,现在我们升级Chart,都很谨慎,先看更新日志,看有没有不兼容的变化,先在测试环境试,没问题了,再在生产环境升级。

七、差点放弃:考虑过的替代方案

踩了这么多坑,中间有一段时间,我真的想放弃Helm,改用其他方案。

那时候,研究了几个替代方案:

1. 原生K8s YAML + Kustomize

不用Helm,直接写YAML,用Kustomize来管理配置和多环境。这个方案,简单直接,没有模板的那些坑,Kustomize是K8s原生的,不用额外装工具。

但缺点是,复用性差,很多重复的内容,要复制粘贴,管理大量应用的时候,还是很麻烦。

2. 云厂商的部署工具

比如阿里云的ROS,腾讯云的TKE,都有自己的应用部署工具,和云平台集成得很好,用起来也方便。但缺点是,绑定了云厂商,不能跨平台用,而且灵活性差一些。

3. 其他包管理工具

比如Ksonnet、Pulumi、Terraform,这些工具,也能管理K8s应用,各有优缺点,但生态和成熟度,都不如Helm,用的人也少,遇到问题不好解决。

研究了一圈,发现还是Helm最成熟,生态最好,用的人最多,虽然坑多,但至少有大量的文档和社区支持,遇到问题能找到解决方案。其他方案,要么不成熟,要么有其他问题,还不如Helm。

所以,最后还是没有放弃,继续用Helm,只是在使用的过程中,总结了很多经验,避开了很多坑,用得也越来越顺手了。

八、现在的使用经验和建议

虽然踩了很多坑,但用了这么久,也总结了一些经验,分享给大家,让大家少走点弯路。

1. 直接用Helm 3,不要用Helm 2

Helm 3去掉了Tiller,架构更简单,更安全,性能也更好,现在已经很成熟了,新用的话,直接上Helm 3,不要用Helm 2,免得以后还要迁移。

2. 模板尽量简单,不要写太复杂

模板越简单,越不容易出错,也越容易维护。不要为了复用,写很复杂的模板和嵌套,能简单就简单,哪怕有点重复,也比复杂的模板好维护。

3. 多用helm template和helm lint

部署之前,先用helm template渲染一下,看渲染后的YAML对不对,再用helm lint检查一下有没有语法错误,这样能提前发现很多问题,避免部署失败。

4. 做好版本管理,记录每次变更

每次部署和升级,都要记录变更内容,包括Chart版本、配置变更、应用版本,方便出问题的时候排查,也方便回滚。

5. 有状态服务和数据库,谨慎用Helm管理

数据库、消息队列这些有状态服务,用Helm部署可以,但升级和回滚要特别谨慎,最好配合备份和恢复机制,不要随便回滚,不然可能丢数据。

6. 不要过度依赖开源Chart,能自己写就自己写

开源Chart虽然方便,但很多都很复杂,有很多你用不到的功能,而且升级的时候容易有不兼容的变化。对于核心应用,建议自己写Chart,简单可控,维护起来也方便。

7. 做好测试,先在测试环境验证

每次升级Chart或者配置,都要先在测试环境验证,没问题了,再上生产环境,不要直接在生产环境改,不然出了问题,影响很大。

九、写在最后

从入门到放弃,再到坚持用下来,Helm给我带来了很多便利,也带来了很多烦恼。

它不是一个完美的工具,有很多坑,很多反人类的设计,很多让人抓狂的问题,但它也是目前K8s生态里,最成熟、最流行的包管理工具,没有之一。如果你在用K8s,需要管理大量的应用,Helm还是一个值得学习和使用的工具,只是要做好踩坑的准备。

最后,用一句话总结我对Helm的感情:"虐我千百遍,我待你如初恋"。虽然踩了很多坑,但还是离不开它,只能一边吐槽,一边继续用。

希望这篇吐槽,能给准备用Helm的朋友,打个预防针,让你们少走点弯路,少踩点坑。也欢迎大家在评论区,分享自己用Helm踩过的坑,一起吐槽,一起交流。