K8s應(yīng)用部署:Helm Chart倉庫搭建與版本管理實(shí)踐)
企業(yè)項(xiàng)目里的k8s用了一段時(shí)間后你會發(fā)現(xiàn)每天寫那一堆YAML簡直是噩夢。同一個(gè)應(yīng)用在開發(fā)、測試、生產(chǎn)環(huán)境不同配置變來變?nèi)ヒ粋€(gè)服務(wù)從Deployment到Service、Ingress、ConfigMap、Secret加起來十幾份清單要發(fā)一版改一遍。今天這個(gè)系列的第16篇咱們就聊Helm這套打包、分發(fā)、版本管理的機(jī)制以及怎么在企業(yè)里搭一個(gè)自己的Chart倉庫。這套東西解決了我團(tuán)隊(duì)里最頭疼的兩個(gè)問題一是“應(yīng)用部署定義”沒法版本化二是新環(huán)境上線要靠人工回憶改參數(shù)。如果你也在維護(hù)多套環(huán)境、多個(gè)業(yè)務(wù)應(yīng)用這篇文章應(yīng)該能直接省下你不少熬夜時(shí)間。1. 為什么企業(yè)k8s項(xiàng)目需要Helm1.1 手動(dòng)管理YAML的痛點(diǎn)說個(gè)我自己的經(jīng)歷。早年間做某個(gè)后臺管理系統(tǒng)的上云改造十幾個(gè)微服務(wù)每個(gè)服務(wù)一個(gè)目錄里面放著deployment.yaml、service.yaml、configmap.yaml。每次要發(fā)布一個(gè)新版本我需要在服務(wù)器上執(zhí)行一串kubectl apply -f然后自己盯著Pod狀態(tài)。這還算好的更崩潰的是換一個(gè)環(huán)境比如從測試環(huán)境切到生產(chǎn)環(huán)境我得手工把鏡像tag改掉、把副本數(shù)調(diào)高、把某些配置項(xiàng)換掉稍有不慎漏改一個(gè)字段線上就出幺蛾子。另一個(gè)痛點(diǎn)是用文件復(fù)制的方式來“備份”應(yīng)用定義根本沒法回溯。昨天改完的YAML到底和上周有什么差異靠肉眼diff整目錄文件那是反人類的。一旦出問題想回滾只能寄希望于自己還記得之前是什么樣。1.2 Helm的三層模型Chart、Release、RepositoryHelm的核心思想特別樸素把一組相關(guān)的k8s資源定義打包成一個(gè)應(yīng)用包叫Chart這個(gè)包安裝到集群里就產(chǎn)生一個(gè)Release一次發(fā)布的實(shí)例而包本身可以放到一個(gè)共享倉庫Repository里供團(tuán)隊(duì)下載。這三個(gè)概念配合起來解決了我的痛點(diǎn)Chart就是一個(gè)目錄/壓縮包里面含有Chart.yaml元數(shù)據(jù)、values.yaml默認(rèn)參數(shù)、templates/模板文件、charts/依賴的子Chart。Release一條記錄表示“這個(gè)Chart在某一次安裝/升級后生成的實(shí)際K8s資源集合”。每次升級都會生成新的版本記錄可以隨時(shí)回滾。Repository存放Chart包的HTTP服務(wù)器可以理解成YUM倉庫、npm私服的翻版供helm search、install時(shí)拉取。1.3 Helm帶來的企業(yè)級價(jià)值對團(tuán)隊(duì)協(xié)作來說Helm真正的價(jià)值不只是少寫幾個(gè)YAML而是把“應(yīng)用部署定義”當(dāng)成軟件制品來管理。部署什么版本、什么參數(shù)、什么依賴全部被Chart文件化、版本化、可審計(jì)。后續(xù)不管誰來發(fā)布只要拿著Chart包和values文件就能在另一個(gè)集群完整復(fù)現(xiàn)一套應(yīng)用。對于管理層來說“上線可回滾、變更可追溯”這種要求用Helm比用散裝YAML容易落地得多。注意這里說的版本管理主要靠Chart包的版本和Release的版本記錄而不是簡單靠Git記錄。兩者要配合起來用。2. Helm環(huán)境準(zhǔn)備與基礎(chǔ)使用2.1 安裝Helm客戶端Helm 3是當(dāng)前主流它去掉了Tiller客戶端直接和kube-apiserver交互權(quán)限取決于你的kubeconfig安全性好了很多。安裝方法很簡單也可以從官方GitHub下載二進(jìn)制然后放到/usr/local/bin下。我用的是v3.14.0wget https://get.helm.sh/helm-v3.14.0-linux-amd64.tar.gz tar -zxvf helm-v3.14.0-linux-amd64.tar.gz mv linux-amd64/helm /usr/local/bin/helm helm version裝完以后確保你的kubeconfig能連到集群。測一下kubectl cluster-info helm env建議把HELM_NAMESPACE設(shè)置成你常用的命名空間避免每次都要寫-n。不過在企業(yè)里不同業(yè)務(wù)最好還是顯式指定namespace防止串環(huán)境。2.2 常用命令速覽這些命令是我每天用到的建議先混個(gè)臉熟helm repo add 別名 地址添加Chart倉庫。helm repo update更新本地倉庫緩存。helm search repo 關(guān)鍵詞搜索倉庫里的Chart。helm install Release名 Chart -f values文件安裝一個(gè)Chart成為一個(gè)Release。helm upgrade Release名 Chart -f values文件升級Release。helm history Release名查看Release的所有版本。helm rollback Release名 版本號回滾到某個(gè)歷史版本。helm uninstall Release名卸載Release。helm template Chart本地渲染模板而不實(shí)際安裝調(diào)試用。如果用的時(shí)候記不住helm help本身就是最好的速查手冊。我強(qiáng)烈建議在實(shí)際操作前先拿一個(gè)簡單Chart跑通install、upgrade、rollback這一條鏈路后面才能在企業(yè)里玩得轉(zhuǎn)。2.3 第一個(gè)Chart的創(chuàng)建與安裝演示用helm create可以快速生成一個(gè)標(biāo)準(zhǔn)Chart結(jié)構(gòu)helm create my-demo tree my-demo生成的結(jié)構(gòu)里有幾個(gè)關(guān)鍵文件my-demo/ ├── Chart.yaml ├── charts/ ├── templates/ │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ └── serviceaccount.yaml └── values.yamlChart.yaml里最重要的是name和version這兩個(gè)字段決定了這個(gè)Chart在倉庫里的標(biāo)識。比如apiVersion: v2 name: my-demo description: A Helm chart for Kubernetes type: application version: 0.1.0 appVersion: 1.16.0version是Chart版本appVersion是應(yīng)用版本兩者不要混。我習(xí)慣把模板文件精簡一下只保留Deployment和Service再加個(gè)ConfigMap。values.yaml里設(shè)置鏡像地址、副本數(shù)、端口這些變量。修改好以后直接試安裝helm install my-demo-release ./my-demo --namespace dev如果模板有報(bào)錯(cuò)它會直接在終端提示你。安裝完成后可以看一下Release狀態(tài)helm list -n dev helm status my-demo-release -n dev實(shí)踐中最常用的調(diào)試方式是helm template它會把模板渲染成最終的YAML輸出到終端而且不會真的創(chuàng)建資源helm template my-demo ./my-demo -f values-dev.yaml這個(gè)命令在CI里特別有用先渲染出來檢查一遍再apply能避免很多低級失誤。3. Chart倉庫搭建從零構(gòu)建企業(yè)私有倉庫3.1 倉庫選型對比企業(yè)里搭建Chart倉庫主流方案大概是這三種Harbor功能最全支持鏡像倉庫和Chart倉庫帶權(quán)限控制、審計(jì)、復(fù)制適合已經(jīng)用了Harbor作為鏡像倉庫的團(tuán)隊(duì)。ChartMuseum專門的Chart倉庫服務(wù)輕量、部署簡單支持很多后端存儲本地磁盤、OSS、S3等適合獨(dú)立部署。Nginx 靜態(tài)文件最簡方案直接把Chart包放到目錄里用helm repo index生成索引文件Nginx托管配合basic auth可做簡單權(quán)限。適合國內(nèi)網(wǎng)絡(luò)環(huán)境或者內(nèi)網(wǎng)小規(guī)模團(tuán)隊(duì)不想多維護(hù)組件。我建議如果團(tuán)隊(duì)已經(jīng)有Harbor直接用它少一套系統(tǒng)。如果只是想快速搭一個(gè)讓開發(fā)們能拉取Chart用Nginx方案最快。下面把這兩種都寫一下。3.2 基于Harbor搭建Chart倉庫如果你的Harbor版本在2.x以上默認(rèn)支持OCI的Chart倉庫但傳統(tǒng)Helm 3也兼容。操作上需要先開啟Harbor的“Helm Chart”功能。我這邊步驟大概這樣首先在Harbor的web界面上創(chuàng)建一個(gè)項(xiàng)目例如叫k8s-charts。然后在這個(gè)項(xiàng)目底下把“Helm Chart”這個(gè)tab激活不同版本入口略有差異最新版在項(xiàng)目配置里打開“Helm Chart”開關(guān)。之后你就能得到一個(gè)倉庫地址形如https://harbor.example.com/chartrepo/k8s-charts。在客戶端側(cè)添加倉庫并認(rèn)證helm repo add myrepo https://harbor.example.com/chartrepo/k8s-charts \ --username admin --password xxxx helm repo update然后打包并推送Chart。Helm 3有現(xiàn)成的helm push命令但需要先安裝helm-push插件helm plugin install https://github.com/chartmuseum/helm-push cd my-demo helm package . helm push my-demo-0.1.0.tgz myrepo如果推送沒問題你可以在Harbor頁面上看到這個(gè)Chart包點(diǎn)開還能看到版本、依賴等信息。注意Harbor使用過程中如果走的是自簽名HTTPS客戶端需要在Trust store里放證書否則helm repo add會報(bào)x509證書錯(cuò)誤。3.3 基于Nginx搭建輕量級Chart倉庫如果你沒那么多組件依賴用一個(gè)Nginx來托Chart倉庫也很穩(wěn)。原理很簡單把Chart包和index.yaml放在同一個(gè)目錄Nginx對外提供該目錄的靜態(tài)文件服務(wù)即可。具體步驟在服務(wù)器上建目錄比如/opt/helm-repo/charts。把你打包好的*.tgz文件放進(jìn)去。用helm repo index生成索引文件helm repo index /opt/helm-repo/charts --url https://charts.example.com/這會在目錄下生成一個(gè)index.yaml里面記錄了所有Chart包的名稱、版本、URL和校驗(yàn)值。配置Nginxserver { listen 80; server_name charts.example.com; root /opt/helm-repo/charts; autoindex on; location / { try_files $uri $uri/ 404; } }如果要加訪問控制可以用Basic Auth生成類似Caddy或者Nginx的賬戶配置文件。在企業(yè)里建議至少套一層HTTPS和認(rèn)證否則任何人都能下載內(nèi)網(wǎng)Chart包敏感信息容易泄露。在其他機(jī)器上添加這個(gè)倉庫helm repo add corp http://charts.example.com --username dev --password xxxx helm repo update helm search repo corp這樣就很方便了。我團(tuán)隊(duì)早期就是這么干的內(nèi)網(wǎng)幾十個(gè)ChartNginx一臺小機(jī)器夠用了。3.4 將Chart上傳至倉庫與下載驗(yàn)證倉庫搭好后需要驗(yàn)證整個(gè)流轉(zhuǎn)是不是通的。流程一般是本地打包 → push/復(fù)制到倉庫 → 遠(yuǎn)程添加倉庫 → install。以Nginx方案為例更常見的做法是在CI里用腳本把Chart包上傳到目錄然后再執(zhí)行一次helm repo index更新索引。注意helm repo index不是自動(dòng)增量的它會重新掃描目錄生成新索引。如果目錄里已有舊的tgz它會一并加進(jìn)去。所以更新索引這個(gè)動(dòng)作要小心別把舊包丟了。驗(yàn)證一下下載helm repo update helm search repo myrepo/my-demo --versions helm pull myrepo/my-demo --version 0.1.0能正常pull下來說明倉庫OK。然后可以用helm install遠(yuǎn)程安裝這個(gè)Chart確保index.yaml里記錄的URL是客戶端可達(dá)的否則會報(bào)“chart not found”或“failed to download”。4. Helm企業(yè)級實(shí)戰(zhàn)部署、升級與回滾4.1 多環(huán)境Value管理企業(yè)里一個(gè)應(yīng)用至少要在dev、test、prod三個(gè)環(huán)境各跑一套。如果每個(gè)環(huán)境都復(fù)制一份values文件會有一堆重復(fù)和容易漏改的參數(shù)。我的建議是一個(gè)Chart多個(gè)values文件用層次化組合。比如目錄結(jié)構(gòu)charts/my-app/ ├── Chart.yaml ├── values.yaml # 默認(rèn)值 ├── values-dev.yaml # dev覆蓋 ├── values-test.yaml # test覆蓋 └── values-prod.yaml # prod覆蓋安裝的時(shí)候helm install my-app-dev ./charts/my-app -n dev -f values-dev.yaml helm install my-app-test ./charts/my-app -n test -f values-test.yaml helm install my-app-prod ./charts/my-app -n prod -f values-prod.yamlvalues.yaml里只放“所有環(huán)境都一樣的公共配置”比如鏡像倉庫前綴、對外端口。而每個(gè)環(huán)境只寫“跟別的環(huán)境不同”的那幾項(xiàng)比如副本數(shù)、數(shù)據(jù)庫地址、日志級別。這樣以后加一個(gè)環(huán)境只需要新增一個(gè)values文件改動(dòng)范圍很小。還有一個(gè)細(xì)節(jié)我認(rèn)為--set參數(shù)適合臨時(shí)調(diào)試不適合長期維護(hù)。因?yàn)?-set寫在命令行里不留痕下次復(fù)制命令根本不知道上次設(shè)了哪些值。所以正式環(huán)境一定要用values文件并且把values文件納入Git管理。4.2 企業(yè)級Chart設(shè)計(jì)規(guī)范在多人協(xié)作的場景里Chart的結(jié)構(gòu)和命名最好約定好否則沒過多久就會亂。我總結(jié)過幾條經(jīng)驗(yàn)Chart目錄按業(yè)務(wù)域劃分比如apps/order-center/、apps/user-center/。Chart中的模板要盡量參數(shù)化凡是會變化的字段都放進(jìn)values.yaml不要硬編碼在模板里。每個(gè)Chart都必須有README.md說明默認(rèn)參數(shù)、端口、依賴、部署步驟。版本號遵守語義化Chart version: 0.1.0appVersion: 1.2.3升級應(yīng)用時(shí)只改appVersion和version的patch位改動(dòng)模板導(dǎo)致功能變化時(shí)升級version。依賴的外部資源數(shù)據(jù)庫、消息隊(duì)列不要直接內(nèi)嵌在業(yè)務(wù)Chart里最好單獨(dú)部署基礎(chǔ)設(shè)施Chart然后在業(yè)務(wù)Chart里通過values傳入地址。這里還要聊一下_helpers.tpl。這個(gè)文件用來生成通用模板片段比如資源名稱、標(biāo)簽。我建議所有Chart都讓模板調(diào)用include來構(gòu)造名字而不是直接硬寫。這樣統(tǒng)一改名時(shí)只要?jiǎng)右粋€(gè)地方就不會出現(xiàn)Deployment名叫AService名叫B這類低級錯(cuò)誤。4.3 升級與回滾操作實(shí)戰(zhàn)升級一個(gè)Release到新版本最簡單的命令是helm upgrade my-app-prod ./charts/my-app -n prod -f values-prod.yaml升級時(shí)Helm會對比values.yaml和模板渲染結(jié)果然后對K8s資源做patch或create。你可以用--dry-run先模擬helm upgrade my-app-prod ./charts/my-app -n prod \ -f values-prod.yaml --dry-run --debug確定沒問題后去掉--dry-run執(zhí)行。假如你改了values里的鏡像tag那么Deployment期望的Pod模板變了k8s會自動(dòng)滾動(dòng)更新。但你改了ConfigMap內(nèi)容時(shí)Deployment本身沒變Pod不會自動(dòng)重啟這會讓很多人迷惑。解決辦法很經(jīng)典在模板里給Deployment加一個(gè)基于ConfigMap內(nèi)容哈希的annotation比如annotations: checksum/config: {{ include (print $.Template.BasePath /configmap.yaml) . | sha256sum }}這樣只要ConfigMap內(nèi)容變了Deployment的annotation就變k8s就會滾動(dòng)?;貪L操作也很直接helm history my-app-prod -n prod helm rollback my-app-prod 3 -n prod這里的3是REVISION序號不是Chart版本。回滾后Helm會生成一個(gè)新的REVISION第4個(gè)并盡可能恢復(fù)到第3版時(shí)的資源配置。要注意如果你在回滾后立即發(fā)現(xiàn)了問題還可以rollback回第4個(gè)版本所以別怕回滾它很安全。4.4 配合CI/CD的自動(dòng)化發(fā)布流程企業(yè)里最爽的用法是把Helm集成進(jìn)CI/CD流水線。我在某公司的流水線里大概是這樣工作的代碼提交觸發(fā)CI編譯并構(gòu)建Docker鏡像推送到鏡像倉庫。CI生成一個(gè)帶Git commit hash的鏡像tag。更新對應(yīng)環(huán)境的values文件里的image.tag提交到Git。CD階段調(diào)用helm upgrade把Chart和更新后的values一起安裝到集群。偽代碼差不多這樣# CD 階段 helm repo add corp http://charts.example.com --username $HELM_USER --password $HELM_PASS helm repo update helm upgrade --install my-app-prod corp/my-app \ --namespace prod \ -f values-prod.yaml \ --set image.tag${CI_COMMIT_SHA}這里用了--install參數(shù)意思是如果Release不存在就安裝存在就升級非常省事。如果用了GitOps那么你也可以把values文件和Chart倉庫都當(dāng)成Git里的變量通過流水線自動(dòng)執(zhí)行。但不管哪條路基本套路都一樣把鏡像版本作為主要變更輸入通過values文件或者--set傳給Helm。我們還做了發(fā)布權(quán)限管控CI使用的ServiceAccount只擁有部署相關(guān)命名空間的寫權(quán)限不能操作其他業(yè)務(wù)。這是Helm 3比Helm 2安全的地方不需要一個(gè)全局的Tiller所有權(quán)限都交給kube-apiserver來管。5. 常見問題與坑位實(shí)錄5.1 倉庫認(rèn)證與Chart拉取失敗常見報(bào)錯(cuò)是failed to fetch http://.../index.yaml : 401 Unauthorized。這種情況分兩種你repo add時(shí)用戶名密碼不對重新添加即可。認(rèn)證信息被緩存了錯(cuò)誤內(nèi)容特別是CI里環(huán)境變量沒傳對??梢杂胔elm repo remove再重新add或者檢查~/.config/helm/repositories.yaml里的配置。另一種情況是index.yaml里記錄的Chart包的URL指向了一個(gè)不存在的地址。如果你用Nginx方案最常見的是你生成index時(shí)用的--url參數(shù)寫錯(cuò)了比如寫成了http://localhost:8080別人自然拉不到。解決辦法是重跑helm repo index用正確的對外訪問地址重新生成。5.2 升級后Pod不更新的詭異現(xiàn)象除了上面提到的ConfigMap變更不觸發(fā)滾動(dòng)外還有一個(gè)容易踩的坑你在模板里用了semverCompare或者復(fù)雜條件導(dǎo)致某些字段沒有變化。用--dry-run能幫你看到最終渲染結(jié)果對比一下就能發(fā)現(xiàn)問題。我習(xí)慣用helm diff插件helm-diff來查看預(yù)期變更能在執(zhí)行升級前直觀列出哪些資源會被修改非常穩(wěn)。如果安裝的是別人寫的Chart里面模板用了random函數(shù)生成Secret會導(dǎo)致每次升級都生成一個(gè)新Secret而引用Secret的Pod卻因?yàn)橐弥挡蛔兤鋵?shí)變了而不更新。這類問題只能靠排查模板邏輯或者指定helm.sh/resource-policy: keep等策略。5.3 Helm版本與K8s版本兼容性Helm 3本身要求K8s的API版本不能太老。比如老的k8s 1.14之前用Helm 3默認(rèn)的apps/v1Deployment是沒問題的但如果你用了很老的集群一些內(nèi)置API可能不支持。我在某團(tuán)隊(duì)的舊集群上就遇到過Ingress資源版本不兼容的問題需要修改Chart模板里的apiVersion。另外helm 2到helm 3遷移是一道大坑主要是模板變量和label的變化。如果你的團(tuán)隊(duì)還在用Helm 2我還是建議盡早遷移Helm 2已經(jīng)非常老了而且安全性堪憂。5.4 Hook與資源清理的坑Helm支持pre-install、pre-upgrade、post-install等Hook用來執(zhí)行Job或者初始化動(dòng)作。如果你定義了Hook要記得Hook完成的Pod在Release卸載時(shí)會被清理。但有時(shí)候我們想保留一些數(shù)據(jù)比如數(shù)據(jù)庫初始化結(jié)果那就不該用Hook而是用一個(gè)獨(dú)立Chart來管理初始化任務(wù)否則升級的時(shí)候Hook重跑可能把數(shù)據(jù)搞亂。還有一個(gè)坑如果Hook Job失敗了Helm會默認(rèn)一直重試直到失敗次數(shù)達(dá)到backoffLimit然后才會標(biāo)記Release失敗。這個(gè)地方可能導(dǎo)致一次發(fā)布卡死很久。我建議在Hook Job模板里顯式設(shè)置restartPolicy: Never和backoffLimit: 3避免死循環(huán)。結(jié)尾Helm這東西初期學(xué)起來簡單但真正在企業(yè)里用得順是需要一些沉淀的。我在實(shí)際使用過程中最深的體會是Helm的價(jià)值不在于“少寫幾個(gè)YAML”而在于它逼著你把應(yīng)用部署這事兒做成一個(gè)標(biāo)準(zhǔn)化的交付物。把Chart倉庫建好、把values分層規(guī)范起來之后新項(xiàng)目上線的時(shí)間從原來的半天壓縮到了十幾分鐘而且再也不用擔(dān)心有人“不小心改錯(cuò)了環(huán)境配置”。如果你還沒有接觸過Helm我建議先從一個(gè)小Chart跑通全流程再逐步推廣到核心應(yīng)用。這個(gè)系列后面我還會繼續(xù)聊更細(xì)的實(shí)踐比如在Helm里管理數(shù)據(jù)庫遷移、多集群發(fā)布等咱們下次接著聊。