尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Helm实战指南:从Chart模板到CI/CD,解决K8s应用交付痛点

Helm实战指南:从Chart模板到CI/CD,解决K8s应用交付痛点 1. Helm到底解决了什么问题K8s世界里“搬应用”的终极痛点先从一个真实的场景说起。早些年我在公司维护一套微服务架构服务数量从十几个一路涨到四五十个。每个服务在Kubernetes里至少要有Deployment、Service、ConfigMap三件套复杂一点的还得配上Ingress、HPA、ServiceAccount、NetworkPolicy。麻烦的不是写这些YAML本身而是“几乎一样的YAML要复制粘贴几十遍”更麻烦的是“每次环境不一样副本数、镜像tag、域名、资源配置全都不一样”。那时候团队里维护K8s配置的方式基本靠两招一是直接复制粘贴YAML文件然后全局替换二是写一堆Shell脚本去sed替换。说实话这两种方式在最开始确实能跑但一旦到了多环境、多服务、频繁发布的阶段就是灾难。有次线上环境更新镜像tag脚本里正则写错了直接把三个服务的镜像都替换成了同一个版本灰度发布变成了全量发布好在发现得早不然后果挺严重。Helm就是在这样的背景下被设计出来的。它的官方定位是“Kubernetes的包管理器”但如果你用过apt、yum、npm、pip这些工具你会发现Helm做的事情跟它们高度相似把一组相关的K8s资源定义打包成一个标准单元带上版本号塞进一个仓库里然后通过简单的命令安装、升级、回滚、卸载。这个“标准单元”就是Chart类似于npm里的package或者apt里的deb包。一个Chart里可以包含Deployment、Service、Ingress等所有资源模板也可以包含README、默认配置、依赖声明。安装Chart的时候Helm会读取你传入的配置值把模板渲染成最终的YAML并应用到集群里。如果说Kubernetes解决的是“容器怎么编排”的问题那Helm解决的就是“应用怎么交付”的问题。K8s给你的是原子能力你得自己组合Helm给你的是封装好的应用单元拿来就能跑。这个区别非常重要也是我在实际团队里推动Helm落地时最常强调的一点Helm不是K8s的替代品而是K8s复杂性的“减震器”。从团队协作的角度看Helm带来的价值更加直接。以前交付一个应用你需要写一份部署文档把十几个YAML文件打个压缩包发给运维运维再按文档一步步操作。用上Helm之后交付物变成了一个Chart包安装、升级、回滚都是命令行操作版本信息自包含文档可以大幅缩减。还有一点让我觉得Helm真正“香”的地方是它的生态。Helm Hub和Artifact Hub上有大量现成的Chart从数据库到消息队列从监控组件到CI/CD工具基本都能一键安装。我搭建测试环境的时候装一个Redis集群、装一个MinIO、装一个Kafka每样都是几条helm命令搞定省下来的时间非常可观。2. Chart内部到底长什么样templates、values与渲染原理要用好Helm理解Chart的结构是第一步。很多人学Helm只记住了几条命令行结果一遇到自定义Chart就懵了说到底还是没搞明白Chart的内部组织方式。一个典型的Chart目录结构大概是这样的mychart/ ├── Chart.yaml ├── values.yaml ├── values.schema.json ├── charts/ ├── templates/ │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── service.yaml │ ├── ingress.yaml │ └── tests/ │ └── test-connection.yaml └── README.md每个文件都有明确的职责。Chart.yaml是Chart的“身份证”记录了name、version、appVersion、description、dependencies这些元信息。values.yaml是默认配置相当于npm包里的默认参数。templates/目录放的是Go template格式的资源模板这是Helm的灵魂所在。charts/目录放的是依赖的子Chart_helpers.tpl存放可复用的模板片段NOTES.txt是安装完成后打印给用户的提示信息。理解Helm的模板渲染机制可以从一个经典的类比入手把Chart想象成一张“申请表”values.yaml是你在表上填的内容templates是表本身的格式定义而Helm就是那个帮你把表提交给K8s的办事员。你只需要在values里改几个字段Helm会自动帮你把Deployment的副本数、镜像地址、端口这些所有关联资源全部统一修改并部署上去。_template渲染的核心概念有三个值注入、管道函数和流程控制。值注入用的是{{ .Values.replicaCount }}这种语法Helm在渲染时会在当前目录的values.yaml和你通过--set传入的值之间做合并。管道函数类似Linux命令行里的管道符比如{{ .Values.image.tag | default latest | quote }}表示取tag值如果没设置就用latest再用quote包一层引号。流程控制是if-else、with、range这些逻辑结构。最常用的场景是根据条件决定是否生成某个资源。比如只在开启了ingress的情况下才渲染Ingress模板{{- if .Values.ingress.enabled -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ include mychart.fullname . }} spec: rules: - host: {{ .Values.ingress.host | quote }} http: paths: - path: {{ .Values.ingress.path }} pathType: Prefix backend: service: name: {{ include mychart.fullname . }} port: number: {{ .Values.service.port }} {{- end }}注意{{-和-}}这里的横线作用是去除模板标记前后的空白字符。这个细节挺重要因为如果不处理缩进和换行渲染出来的YAML很容易因为格式问题报错。_helpers.tpl里的命名模板是另外一个容易被忽视的重点。它相当于代码里的公共函数用来生成统一的资源名称、标签、选择器等。比如{{ include mychart.fullname . }}这个调用在_helpers.tpl里通常会这样定义{{- define mychart.fullname -}} {{- if .Values.fullnameOverride -}} {{- .Values.fullnameOverride | trunc 63 | trimSuffix - -}} {{- else -}} {{- $name : default .Chart.Name .Values.nameOverride -}} {{- if contains $name .Release.Name -}} {{- .Release.Name | trunc 63 | trimSuffix - -}} {{- else -}} {{- printf %s-%s .Release.Name $name | trunc 63 | trimSuffix - -}} {{- end -}} {{- end -}} {{- end -}}这段逻辑说白了就是如果有手动指定的fullnameOverride就用它否则用release名加Chart名的组合并且保证不超过63个字符这是K8s对资源名称的长度限制。用这种公共模板的好处是所有资源的命名规则保持一致不会出现Deployment叫一个名字、Service叫另一个名字的情况。我在带团队做Chart规范化的时候会要求每个Chart必须有values.schema.json。这个文件用JSON Schema格式定义values里每个字段的类型、取值范围、是否必填。Helm在安装和升级时会对values做校验不合法就直接报错。这相当于给Chart加了一层“编译期检查”很多低级错误在提交到集群之前就被拦截了。3. 从零开始发布一个Charthelm create、dependency与私有仓库配置理解了Chart结构之后接下来就是实际操作的部分了。Helm的入门操作其实不复杂我建议所有新手都从helm create命令开始先看看官方脚手架生成了什么再逐步改成自己的Chart。helm create会生成一个完整的示例Chart包含Deployment、Service、Ingress、HPA、ServiceAccount这些资源模板注释也写得很完整。我一般会让学生先跑一次这个命令然后仔细读一遍生成的模板再对照helm template的输出看渲染结果。这个过程比看十篇教程都管用。实际项目的Chart开发流程我们团队基本是这么走的helm create生成基础骨架按业务需求精简模板去掉不需要的资源比如没开启HPA就不保留HorizontalPodAutoscaler整理values.yaml把环境相关的参数全部提取出来并加上注释说明把资源名称统一改成{{ include chartname.fullname . }}检查labels和selector的一致性本地跑helm template . --debug检查渲染结果用helm lint做静态检查推送到测试环境安装验证其中第5点特别容易踩坑。K8s要求Deployment的selector必须匹配Pod模板的labels而且selector一旦创建就不能改所以selectors里的标签必须用Chart名称和release名称这种稳定的值不能用版本号这种会变的字段。多Chart管理的时候dependency和私有仓库是绕不开的话题。微服务架构下一个完整的应用栈往往由多个Chart组成比如一个业务应用依赖Redis和PostgreSQL那就可以用dependencies来声明这种关系# Chart.yaml dependencies: - name: redis version: 17.3.0 repository: https://charts.bitnami.com/bitnami condition: redis.enabled这里的condition字段很实用它表示只有在values.yaml里设置了redis.enabled: true时才拉取这个依赖。这样一套Chart既可以整体部署也可以拆开来只装核心服务。有了依赖声明之后helm dependency update会把依赖Chart下载到charts/目录helm install的时候会自动一起安装。私有仓库的配置值得单独说。公司内部一般会有自己封装的Chart需要推到自己的Chart仓库里。Helm 3时代常见的方案是用Harbor或者ChartMuseum搭一个OCI Registry或者普通HTTP仓库。我个人更推荐OCI因为OCI Registry就是标准的容器镜像仓库不需要额外维护ChartMuseum实例Harbor本身就支持。推送和拉取私有仓库里的Chart操作大概是这样的# 添加仓库注意Helm 3.8默认支持OCI但OCI不需要helm repo add helm repo add myrepo https://harbor.example.com/chartrepo/library # 打包Chart helm package ./mychart -d ./dist # 推送HTTP仓库方式 helm push ./dist/mychart-0.1.0.tgz myrepo --username admin --password xxxx # 搜索仓库中的Chart helm search repo myrepo # 安装 helm upgrade --install myapp myrepo/mychart -n production --values production-values.yamlhelm upgrade --install这个组合命令是我日常最常用的它实现了“没有就装、有就升级”的幂等逻辑在CI/CD里直接跑这条命令就行不用先判断Chart是不是已经存在了。还有一个容易被忽略的细节是Chart版本和App版本的区分。Chart.yaml里的version表示Chart自身的版本appVersion表示它部署出来的应用版本。升级任何一方都需要修改对应的字段。我们团队的规定是只要模板或values结构有变化就必须升version只有镜像tag变化只升appVersion。这样做的好处是通过helm list和helm history就能清楚知道某次升级改的是Chart逻辑还是应用镜像。4. 踩坑实录版本不一致、hook幂等性与大Chart发布超时Helm用久了之后你会发现真正的挑战不在“怎么用”而在“出问题了怎么查”。这里整理几个我在实际项目中反复遇到的坑每一个都花了不少时间才定位到根因。第一个坑客户端版本与服务端API版本不一致导致的渲染报错。Kubernetes的API版本一直在演进比如Ingress从extensions/v1beta1演进到networking.k8s.io/v1Deployment从apps/v1beta1演进到apps/v1。如果你用的Helm Chart模板里写的是老版本API在新集群上直接helm install就会报unable to recognize : no matches for kind Ingress in version networking.k8s.io/v1beta1。这个问题在从老集群迁移到新集群时特别常见。排查思路是先用kubectl api-resources | grep ingress看看当前集群支持的API版本再检查Chart模板里的apiVersion字段。Helm 3自带的验证机制在一定程度上能提前发现问题但如果你用的Chart是网上找的旧Chart建议统一用helm template渲染出来检查一遍再实际部署。第二个坑钩子Hook的幂等性问题。Helm的hook机制可以在安装、升级、删除等生命周期节点执行额外任务最常见的是在安装前跑数据库迁移Job。但hook默认只会在对应事件触发时执行一次如果Job失败了重新helm upgrade时不一定会重新执行。更麻烦的是如果Job的Pod因为某种原因被删了hook对应的Job资源还存在但Pod没了Helm会认为hook已经执行过了。解决方式是在Job模板里加ttlSecondsAfterFinished或者backoffLimit同时利用helm.sh/hook-delete-policy注解来控制hook资源的清理策略apiVersion: batch/v1 kind: Job metadata: name: {{ include mychart.fullname . }}-migration annotations: helm.sh/hook: pre-upgrade,pre-install helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded helm.sh/hook-weight: -5 spec: template: spec: restartPolicy: Never containers: - name: migration image: {{ .Values.image.repository }}:{{ .Values.image.tag }} command: [/bin/sh, -c, python manage.py migrate] backoffLimit: 0hook-delete-policy里的before-hook-creation表示下次执行hook前删除上次的hook资源这是保证hook能重复执行的关键。hook-weight用来控制多个hook之间的执行顺序数值小的先执行。这个坑比较隐蔽因为不是每次都复现但一旦遇到“迁移Job明明失败了重试却跳过了”这种诡异情况大概率就是hook删除策略没配好。第三个坑大Chart发布超时。Helm默认的安装和升级超时时间是5分钟--timeout 5m0s。如果Chart包含大量资源或者遇到镜像拉取慢的情况5分钟很容易超时。超时之后Helm会报错但关键是资源不一定没有创建成功——有时候资源还在慢慢创建但客户端的等待已经超时了导致状态不确定。处理这个问题的标准做法是设置合理的超时时间并在超时后检查实际状态helm upgrade --install myapp ./mychart -n production --timeout 10m0s --atomic--atomic这个参数很有用它会在升级失败时自动回滚到上一个可用版本。不过要注意--atomic的回滚是基于Helm自己的release历史如果你是通过kubectl手动改过集群里的资源回滚可能覆盖不了这些手动修改。第四个坑资源配置和命名空间权限问题。有些Chart模板里默认了resources的规格比如requests.memory设置得很大在小集群里部署的时候Pod会一直Pending因为节点资源不够了。排查这种问题不能只盯着Helm看得用kubectl describe pod和kubectl get events看具体现象看看是镜像拉不下来、存储卷挂载失败还是资源调度不上去。多租户环境下还要注意service account的权限。Helm安装的release默认会创建service account但如果你把release装到别人管理的命名空间里可能没有权限创建service account或RoleBinding。这时可以设置serviceAccount.create: false复用已有的账号或者干脆让Helm用当前kubeconfig里的权限去部署资源。5. 从Helm 2到Helm 3Tiller移除与三段式Release设计如果你翻过老教程或者接手过老项目应该会碰到Helm 2和Helm 3的语法差异问题。Helm 2里有个叫做Tiller的服务端组件它运行在集群内部负责接收客户端的请求并执行部署。Helm 3把Tiller彻底移除了客户端直接通过kubeconfig连接Kubernetes API Server这个改动对整个安全模型产生了本质影响。Helm 2时代Tiller拥有集群内的高权限客户端只要连上Tiller就可以部署任意资源权限隔离很成问题。多团队共享集群的时候一个团队的release可能被另一个团队误操作。Helm 3采用kubeconfig的权限体系后用户能部署什么资源完全由他自己的RBAC权限决定A队没有权限就操作不了B队的命名空间安全边界清晰多了。两个版本对比下来最核心的差异可以归结为几点对比项Helm 2Helm 3架构客户端 Tiller服务端纯客户端权限模型Tiller统一服务账号使用kubeconfig对应权限release信息存储存在集群内ConfigMap和SecretSecret默认加密部分字段命名空间作用域全局/单命名空间有坑release默认绑定到指定命名空间仓库默认地址Helm 2默认stable仓库无默认仓库需手动添加API版本只支持老的K8s API支持新API需按集群适配Helm 3还有一个设计改进很值得提——release记录被保存为Secret。这意味着你可以用kubectl get secrets -n namespace -l ownerhelm看到当前命名空间下所有Helm release的记录包括每个版本的详细配置。排查问题的时候这个能力非常实用可以直接看到某次发布用了什么values。helm history命令可以看到release的完整变更历史helm rollback release revision可以回到任意历史版本。回滚操作本质上是把release记录里的values和模板信息重新渲染一遍并应用到集群。需要注意回滚不会自动处理集群里已经被手动删除的资源如果某次发布后有人手动改了Deployment里的某个字段回滚只会覆盖release关联的资源手动改的部分会被Helm认为是你想要的最终状态覆盖掉。Helm 3的发布设计被社区称为“三段式”release name、release version、release revision三层概念。release name是应用实例的名字同一个Chart可以安装出多个不同名字的实例release version是Chart自身的版本release revision是安装/升级次数。这种设计让Helm具备了完整的“应用版本管理”能力和Git管理代码的思路非常相似只是管理的是K8s资源的状态。6. 结合CI/CD的最佳实践小步发布、环境隔离与版本策略Helm在单机上手容易但在团队协作里要跑得顺还是得配套一些规范和流程。这里分享我们内部沉淀下来的一套最佳实践不一定适合所有团队但可以参考。先说说values的组织方式。我们把values文件按环境拆分成values-dev.yaml、values-staging.yaml、values-prod.yaml每个文件里只写当前环境差异化的配置公共配置放在values.yaml里。部署时用-f多次传入Helm会做合并。helm upgrade --install myapp ./mychart \ -n dev \ -f values.yaml \ -f values-dev.yaml \ --set image.tagdev-20240115-01合并顺序是后面的覆盖前面的--set优先级最高。这样既保证了默认值的完整性又能让环境配置保持精简。其次是版本管理策略。镜像tag统一用分支名-构建时间-commit短哈希的格式比如main-20240115-0930-a1b2c3d。每次提交代码触发CI构建产出新的镜像tag然后自动更新对应环境的values文件再跑helm upgrade。注意不要在CI里用latest标签作为发布版本否则很难定位线上跑的是哪次提交。有条件的团队可以上GitOps模式Git仓库作为唯一可信源通过ArgoCD或Flux监听仓库变化自动同步Helm release。这种模式最大的好处是可审计、可回滚任何变更都有记录。我们是从手动执行Helm命令逐步演进到GitOps的过程本身也是渐进式的先是CI脚本里执行Helm命令然后引入专门的发布分支最后整个流程由Git事件驱动。再来说说环境隔离。开发和测试环境共用一个集群的时候可以用命名空间做软隔离release name叫myapp-dev、myapp-staging避免冲突。生产环境建议独立集群并且用--atomic配合--timeout做安全发布。还有个实用的技巧是给生产release加上helm.sh/resource-policy: keep注解防止某些需要保留的数据卷被误删。Helm的--dry-run参数也是CI里非常好用的检查手段。在真实发布前先跑一次helm upgrade --dry-run --debug可以完整预览将要生成的K8s资源并做一定的客户端校验。我们会在发布流水线里加这一步发现问题可以直接终止流水线省得把坏资源推上去再清理。最后聊一下Chart的代码规范和测试。我们要求每个新增Chart必须通过helm lint并且把lint步骤放到MR流水线里。有条件的团队还可以写一些简单的单元测试来验证模板渲染结果社区里有helm-unittest这个插件可以做模板级测试。它支持对渲染后的YAML做断言、快照对比在复杂Chart里帮助不小。7. 一个真实的部署Demo从空集群到一套完整应用栈理论讲太多容易飘我实际操作一遍给大家看。假设我现在接到一个任务在一个全新的K8s集群里部署一套应用栈包含一个Nginx前端、一个后端API服务和一个Redis缓存要求所有组件都通过Helm管理并且支持多环境配置。先安装Helm客户端。不同系统的安装方式不一样macOS可以用HomebrewLinux可以直接下载二进制# macOS brew install helm # Linux curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 chmod 700 get_helm.sh ./get_helm.sh装完验证一下版本helm version # version.BuildInfo{Version:v3.14.0, GitCommit:..., ...}接着添加需要的仓库。这里我们用Bitnami的Redis Chart作为依赖如果公司有私有仓库也一并加进来helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update创建一个简单的业务Charthelm create myappmyapp目录生成之后我删掉不必要的模板留下Deployment、Service和Ingress。然后修改values.yaml定义出前端和后端的镜像、端口、副本数以及Redis的开关# values.yaml replicaCount: 2 image: repository: nginx tag: 1.25 pullPolicy: IfNotPresent service: type: ClusterIP port: 80 ingress: enabled: true host: demo.example.com path: / backend: enabled: true image: repository: myapp-backend tag: 1.0.0 service: port: 8080 redis: enabled: true在Chart.yaml里声明依赖dependencies: - name: redis version: 17.3.0 repository: https://charts.bitnami.com/bitnami condition: redis.enabled执行helm dependency update ./myapp这一步会把Bitnami Redis Chart下载到./myapp/charts/redis目录。然后写环境配置values-dev.yaml因为开发环境的资源有限副本数少一点、域名也不一样replicaCount: 1 ingress: host: dev.example.com backend: image: tag: dev-20240115-01现在可以做发布前的预检helm lint ./myapp helm template ./myapp -f values-dev.yaml --debug /tmp/rendered.yamlhelm template渲染出来的内容可以直接用kubectl apply --dry-runclient -f -再做一层校验。确认无误后正式部署helm upgrade --install myapp ./myapp -n dev --create-namespace -f values-dev.yaml --atomic --timeout 5m这里--create-namespace的作用是目标命名空间不存在时自动创建。部署完成后用以下命令查看状态和访问信息helm list -n dev helm status myapp -n devhelm status的输出会包含NOTES.txt里的内容通常会有访问URL、默认账号密码之类的提示信息。如果需要查看某次release的详细配置可以用helm get values myapp -n dev这套流程跑通之后后续的每一次发布其实都是一条命令的事。版本升级改tag、配置变更改values、组件调整改Chart结构所有的变更都变得可预测、可追踪、可回滚。我个人在实际操作中的体会是Helm最大的价值在于它把“部署应用”从一门手艺变成了一道流程。有了Chart每个人都能用一致的方式部署应用不用再靠某个“老师傅”手工沟通来执行有了values不同环境之间的差异被显式管理起来而不是散落在各种脚本里有了release历史和回滚能力出问题时的恢复时间从小时级降低到了分钟级。如果你正在为K8s应用的交付管理发愁Helm值得作为第一个引入的规范化工具。
返回列表