Helm是一款很是流行的k8s包管理工具。之前就一直想用它,但看到它產生的文件比k8s要複雜許多,就一直猶豫,不知道它的好處能不能抵消掉它的複雜度。但若是不用,而是用Kubectl來進行調式真的很麻煩。正好最近Helm3正式版出來了,比原來的Helm2簡單了很多,就決定仍是試用一下。結果證實確實很複雜,它的好處和壞處大體至關。有了它確實能大大簡化對k8s的調式,但也須要花費比較多的時間來學習,並且產生的配置文件要複雜許多。可是事實是如今沒有什麼很方便的幫助調式k8s的工具,在沒有更好的方案以前,我仍是建議用它,只是前期須要花些功夫學習和掌握它。前端
Helm3和Helm2的語法差不太多,只是使用起來更方便,不用安裝Tiller。一個比較明顯的變化是再也不須要「requirements.yaml」, 依賴關係是直接在「chart.yaml」中定義。有關Helm3和Helm2的區別,詳情請參見CHANGES SINCE HELM 2。node
網上有很多講述Helm的文章,但大部分都是主要講解安裝和舉一個簡單的例子。但Helm使用起來仍是比較複雜的,必定要有一個複雜的例子才能把它的功能講清楚,裏面有很多設計方面的問題須要思考。我剛開始接觸的時候就以爲頭緒繁多,不知從哪下手。本文就經過一個相對複雜的例子來說解用Helm3來設計配置文件的思路,使上手更容易。mysql
這裏不講Helm3的安裝,它比較很容易。也不講解Helm的基本語法,你能夠本身去看其餘文檔。即便你不懂Helm,應該也能猜出七八成,剩下的就要讀文檔了(Charts)。Helm的語法仍是比較複雜的,要想搞懂可能要花一兩天時間。nginx
本文假設你對helm有一個大概的瞭解,想要構建一個複雜的微服務,但有不知如何下手;或者你想了解一下構建Helm的最佳實踐,那就請你繼續讀下去。git
chart裏一個很重要的概念就是模板(template),它就是Go語言模板,它是裏面加入了編程邏輯的k8s文件。這些模板文件在使用時都要先進行模板解析,把其中的程序邏輯轉化成對應的編碼,最終生成k8s配置文件。github
以上就是Helm自動生成的chart目錄結構,在Helm裏每一個項目叫一個chart,它由下面幾個組成部分:sql
Helm有四個基本元素,值,常量,變量和共享常量(這個後面會講)數據庫
Helm在k8s的基礎之上增長了模板功能,使k8s的配置文件更加靈活。裏面的主要概念就是模板(Template),也就是在k8s的配置文件裏增長了常量和變量以及編程邏輯。若是你不用這些新增功能,那麼就是普通的YAML文件(k8s配置文件),裏面用到的基本元素就是值。編程
節點定位(Node Anchor):json
若是你想複用重複的值,能把它定義成常量嗎?YAML有一個功能叫節點定位(Node Anchor),相似於定義一個常量,而後引用。但它有一些限制,定義的必須是一個節點,所以不如真正的常量靈活。
例如以下文件中,用「&」定義了一個常量「&k8sdemoDatabaseService」,而後用「*k8sdemoDatabaseService」引用它。
global: k8sdemoDatabaseService: &k8sdemoDatabaseService k8sdemo-database-service mysqlHost: *k8sdemoDatabaseService
這時,「k8sdemoDatabaseService:」是YAML文件節點的鍵名,「&k8sdemoDatabaseService」是節點定位的名字,至關於常量名,「k8sdemo-database-service」是YAML節點的鍵值。在上述代碼中,k8sdemoDatabaseService和mysqlHost的值都是「k8sdemo-database-service」。
有關節點定位(Node Anchor)的詳細內容,請參見 YAML。
常量:
因爲節點定位的侷限性,Helm引入了真正的常量,也就是在"values.yaml"裏定義的內容,它能夠定義是任何東西,不僅限於節點。
在"values.yaml"裏定義常量:
replicaCount: 1
在部署模板裏引用:
replicas: {{ .Values.replicaCount }}
那麼何時用常量,何時用值(Literal)呢?若是一個值在模板中出現屢次,就要定義常量,避免重複。例如「accessModes」,既要在存儲卷裏出現,又要在存儲卷申請裏出現。另外若是值有可能變化(不管是隨部署環境變化,仍是隨時間變化),那麼就定義成常量,這樣在修改時就只用改"values.yaml",而沒必要修改模板文件。例如「replicas」的值(也就是集羣的個數)是可能變化的,就要定義成常量。在模板裏能夠引用常量的,但在"values.yaml"裏不行,由於它只是普通YAML文件,沒有模板解析功能,所以不支持常量,這裏就只能用節點定位(來代替常量)。
有關Helm常量的詳細內容,請參見 Use placeholders in yaml和Use YAML with variables。
節點定位的功能是有限的,例如你想利用已有的節點定位,對它進行轉換,定義一個新的節點定位,這在"values.yaml"裏就不行了。
例如你已有節點定位「name」,你想在這個基礎上定義一個新的節點定位「serviceName」,這個"values.yaml"就不支持了,你必需要用模板。
以下所示,這在"values.yaml"裏是不支持的。
name: &name k8sdemo-backend serviceName:*name-service
這就引出了變量的概念,但它只能在模板裏才行。 換句話說,模板既支持常量,也支持變量。但若是把變量的定義邏輯放在Helm每一個模板裏,就顯得很亂。所以通常的作法是把這些邏輯放在一個單獨的模板文件裏,這個就是前面講到的"_helpers.tpl"文件。當你須要對常量進行轉換,生成新的常量,你就在定義變量,這部分代碼就放在"_helpers.tpl"裏。
下面就是"_helpers.tpl"中定義"k8sdemo.name"的代碼。
{{- define "k8sdemo.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}}
在以上這些元素中,常量(也就是在"values.yaml"中定義的)是最靈活的,能用它時儘可能用它。並且由於它是定義在普通YAML文件中("values.yaml"),應用程序能夠直接訪問它,這樣能夠實現應用程序和k8s之間的數據共享。但若是你須要對常量進行編程轉換,那就沒辦法了,只能定義變量,把它放在"_helpers.tpl"中。
在k8s中ConfigMap和Secret是用來存儲共享配置參數和保密參數的,但在Helm中,因爲有了上面講的Helm基本元素,它們徹底能夠代替ConfigMap的功能,所以ConfigMap就不須要了,但Secret仍是須要的,由於要存儲加密信息。下面會講解。
有關ConfigMap的設計侷限性,請參見把應用程序遷移到k8s須要修改什麼?
如今咱們就用一個具體的例子來展現Helm的chart設計。這個例子是一個微服務應用程序,它共有三層: 前端,後端和數據庫,只有這樣才能讓Helm的一些設計問題付出水面,若是隻有一層的話,就太簡單了,沒有參考價值。
在k8s中,每一層就是一個單獨的服務,它裏面有各類配置文件。Helm的優點是把這些不一樣的服務組成一個Chart來共同管理和調式,方便了許多。
上面就是最終的chart目錄結構圖。「chart」是總目錄,裏面有三個子目錄「k8sdemo」,「k8sdemo-backend」,「k8sdemo-database」, 每個對應一個服務,每一個服務都是一個獨立的chart,能單獨調式部署,chart之間也能夠有依賴關係。其中「k8sdemo」是父chart,同時也是前端服務,它的「charts」目錄裏有它依賴的另外兩個服務。「k8sdemo-backend」是後端服務,「k8sdemo-database」是數據庫服務。
處理Chart的依賴關係有兩種方式:
這裏採用的是依賴導入式方式,主要緣由是我原來認爲嵌入式須要一塊兒調試,複雜度過高,若是你以爲這不是問題,這也是個不錯的辦法。用依賴導入式方式,能夠單獨調試各個chart,簡單了不少。後來發現其實採用嵌入式也能夠單獨調試子chart,只是父chart不能單獨調試而已。
當你採用依賴導入式方式時,調試順序關係不大,由於各個chart是各自獨立的,能夠單獨調試。舉個例子,雖然「k8sdemo-backend」須要「k8sdemo-database」才能正常運行,但當沒有數據庫服務時,你的程序也能夠運行,只不過輸出的是錯誤信息,但這並不影響你調試chart。
我先調試「k8sdemo」,它雖然依賴另外兩個chart,但沒有它們也能單獨工做。而後再調試「k8sdemo-backend」和「k8sdemo-database」,最後再把它們導入到「k8sdemo」中去再進行聯調。
它的調試是最容易的,因爲它裏面沒有真正的前端代碼,只要把Nginx調試成功了就能夠了。只要在生成的文件基礎上作些修改就好了。
鍵入以下命令建立chart,其中「k8sdemo」是chart的名字,這個名字很重要,服務的名字和label都是由它產生的。
helm create k8sdemo
這以後,系統會自動建立前面講到的chart目錄結構。讓後就是對已經生成的文件進行修改。
修改"values.yaml":
如下是"values.yaml"主要修改的地方
image: repository: nginx:1.17.6 pullPolicy: Never imagePullSecrets: [] nameOverride: "k8sdemo" fullnameOverride: "k8sdemo" service: type: NodePort port: 80 nodePort: 31080
另外,因爲"ingress.yaml"和"serviceaccount.yaml"暫時沒用,就把它們都設成了「false」
ingress: enabled: false serviceAccount: # Specifies whether a service account should be created create: false
修改"service.yaml":
apiVersion: v1 kind: Service metadata: name: {{ include "k8sdemo.fullname" . }} labels: {{- include "k8sdemo.labels" . | nindent 4 }} spec: type: {{.Values.service.type}} ports: - port: {{.Values.service.port}} nodePort: {{.Values.service.nodePort}} targetPort: http protocol: TCP name: http selector: {{- include "k8sdemo.selectorLabels" . | nindent 4 }}
修改"deployment.yaml":
。。。 containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: {{ .Values.image.repository }} imagePullPolicy: {{ .Values.image.pullPolicy }} 。。。
以上都是簡單的修改,不涉及到設計問題。因爲篇幅的關係,這裏沒有列出所有源碼,若是有興趣請在本文末尾找到源碼地址。
在進行下面的調試以前,先要講一個重要概念。 前面介紹Helm的基本元素時講的都是在一個chart裏共享值,若是要在不一樣chart之間共享值(例如k8s服務名,數據庫用戶名和端口),那麼這些還不夠,你須要共享常量. 一般狀況下子chart和父chart之間的常量是不能共享的,若是須要共享,須要有一種特殊的方法來定義常量,這就是共享常量。它必須是定義在父chart中。
例如,你在「k8sdemo」的「values.yaml」加入下面代碼,注意節點的名字必須是子chart名(例如「k8sdemo-backend」)
k8sdemo-backend: replicaCount: 2 k8sdemo-database: replicaCount: 2
在「k8sdemo」的模板裏就能夠經過「{{ .Values.k8sdemo-backend.replicaCount }}」 來訪問。當Helm發現節點名是子chart名時,它會自動拷貝這個常量到子chart的「values.yaml」中,所以,在「k8sdemo-backend」中,你也能夠經過「{{ .Values.replicaCount }}」 來訪問這個常量。注意這裏並無包含子chart名(「k8sdemo-backend」),而是隻有常量名,由於子chart名只是一個標識,而不是常量名的一部分。
共享常量只能把常量共享給一個字chart,若是你須要多個子chart之間共享,就須要建立全局常量,它用「global」來標識,下面是示例。
在「k8sdemo-backend」的"values.yaml"中定義:
global: k8sdemoDatabaseService: &k8sdemoDatabaseService k8sdemo-database-service mysqlUserName: dbuser mysqlUserPassword: dbuser mysqlPort: 3306 mysqlHost: *k8sdemoDatabaseService mysqlDatabase: service_config
在「k8sdemo-backend」的「deployment.yaml」中引用。
env: - name: MYSQL_USER_NAME value: {{ .Values.global.mysqlUserName }} - name: MYSQL_USER_PASSWORD value: {{ .Values.global.mysqlUserPassword }} - name: MYSQL_HOST value: {{ .Values.global.mysqlHost }} - name: MYSQL_PORT value: "{{ .Values.global.mysqlPort }}" - name: MYSQL_DATABASE value: {{ .Values.global.mysqlDatabase }}
在「k8sdemo-database」的"values.yaml"中定義:
global: k8sdemoDatabaseService: k8sdemo-database-service mysqlUserName: dbuser mysqlUserPassword: dbuser mysqlRootPassword: root mysqlDatabase: service_config
在「k8sdemo-database」的「deployment.yaml」中引用。
env: - name: MYSQL_ROOT_PASSWORD value: {{ .Values.global.mysqlRootPassword }} - name: MYSQL_USER_NAME value: {{ .Values.global.mysqlUserName }} - name: MYSQL_USER_PASSWORD value: {{ .Values.global.mysqlUserPassword }} - name: MYSQL_DATABASE value: {{ .Values.global.mysqlDatabase }}
當把「k8sdemo-backend」和「k8sdemo-database」導入"k8sdemo"後進行聯調時, 就要把上面提到的全局常量寫入"k8sdemo"的"values.yaml"文件中,這樣就能讓各個子chart共享這些常量。以下所示:
global: k8sdemoBackendService: k8sdemo-backend-service k8sdemoDatabaseService: &k8sdemoDatabaseService k8sdemo-database-service mysqlUserName: dbuser mysqlUserPassword: dbuser mysqlRootPassword: root mysqlPort: 3306 mysqlHost: *k8sdemoDatabaseService mysqlDatabase: service_config
若是父chart和子chart有重複的全局常量,這時父chart("k8sdemo")的全局常量值就會覆蓋子chart的全局常量。
它的使用原則就是若是隻是子chart獨有的常量就在子chart的"values.yaml"中定義,若是是共享的常量就在父chart中定義。但若是採用的是依賴導入方式,因爲子chart也要單獨調試,這時你在子chart裏也要定義這些全局常量。這樣在進行chart總調試時,就會使用父chart的中的值。
詳情請參見 Subcharts and Global Values。
「k8sdemo-backend」的chart須要取(與「k8ssdemo」)不一樣的名字,
建立:
helm create k8sdemo-backend
上面就是「k8sdemo-backend」的目錄圖。因爲它須要建持久卷,所以這裏增長了兩個文件「persistentvolume.yaml」和「persistentvolumeclaim.yaml」 ( 不是自動生成的)。
值得一提的是k8s對象的命名。通常狀況下,若是不須要對其進行引用,用chart的全名就好了。例如部署的名稱,以下所示。
name: {{ include "k8sdemo.fullname" . }}
若是是服務名(Service Name),它須要在應用程序和k8s之間共享,也須要在父chart和子chart之間共享,這時最好單獨定義一個全局共享常量。
在「values.yaml」中定義:
global: k8sdemoBackendService: k8sdemo-backend-service
在「service.yaml」中引用:
name: {{.Values.global.k8sdemoBackendService}}
它的調試方式與「k8sdemo-backend」大同小異,就不詳細講解了。
上面各個chart都單獨調試成功以後,就要把它們合在一塊兒進行聯合調試。
在「k8sdemo」(父chart)中加入依賴關係(Chart.yaml)。
dependencies: - name: k8sdemo-backend repository: file://../k8sdemo-backend version: 0.1.0 - name: k8sdemo-database repository: file://../k8sdemo-database version: 0.1.0
這裏爲了簡單起見,沒有用到chart庫(Chart Repository),使用了本地目錄。這裏的「file://」是針對chart的根的相對路徑,「file://..」就是「k8sdemo」的上級目錄。
詳情請參見How to refer to a helm chart in the same repository。
修改全局常量("values.yaml"):
global: k8sdemoBackendService: k8sdemo-backend-service k8sdemoDatabaseService: &k8sdemoDatabaseService k8sdemo-database-service mysqlUserName: dbuser mysqlUserPassword: dbuser mysqlRootPassword: root mysqlPort: 3306 mysqlHost: *k8sdemoDatabaseService mysqlDatabase: service_config
只有須要在chart之間共享的常量才須要在父chart裏的"values.yaml"定義,其他的在各自子chart裏的"values.yaml"定義就能夠了。
鍵入以下命令「helm dependency update k8sdemo」,更新依賴關係
~ # vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/chart$ helm dependency update k8sdemo Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "stable" chart repository Update Complete. ⎈Happy Helming!⎈ Saving 2 charts Deleting outdated charts
完成以後,生成的圖以下所示,這時在「charts」目錄下就導入了新的依賴關係「k8sdemo-backend」和「k8sdemo-database」的chart。
有一點須要注意的是,單獨調試和聯合調試時,生成的k8s配置文件大部分都是同樣的,但有一個地方不一樣
下面是聯合調試時「k8sdemo-database」的部署文件,最後一行「app.kubernetes.io/instance: 」的值是「k8sdemo」。
# Source: k8sdemo/charts/k8sdemo-database/templates/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: k8sdemo-database labels: helm.sh/chart: k8sdemo-database-0.1.0 app.kubernetes.io/name: k8sdemo-database app.kubernetes.io/instance: k8sdemo 。。。
下面是單獨調試時「k8sdemo-database」的部署文件,最後一行「app.kubernetes.io/instance: 」的值是「」k8sdemo-database」。
# Source: k8sdemo/charts/k8sdemo-database/templates/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: k8sdemo-database labels: helm.sh/chart: k8sdemo-database-0.1.0 app.kubernetes.io/name: k8sdemo-database app.kubernetes.io/instance: k8sdemo-database 。。。
由於「instance」的名字是「{{ .Release.Name }}」,而單獨調試和聯合調試時給的「release」名字不一樣。而其餘的值都是由配置文件決定的,所以不會有意外。
安裝k8sdemo:
vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/chart$ helm upgrade k8sdemo ./k8sdemo Release "k8sdemo" has been upgraded. Happy Helming! NAME: k8sdemo LAST DEPLOYED: Fri Nov 29 01:28:55 2019 NAMESPACE: default STATUS: deployed REVISION: 2 NOTES: 1. Get the application URL by running these commands: export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services k8sdemo) export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT
獲取Pod名稱:
vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/chart$ kubectl get pod NAME READY STATUS RESTARTS AGE k8sdemo-74cb7b997c-pgcj4 1/1 Running 0 33s k8sdemo-backend-5cd9d79856-dqlmz 1/1 Running 0 33s k8sdemo-database-85855485c6-jtksb 1/1 Running 0 33s k8sdemo-jenkins-deployment-675dd574cb-r57sb 1/1 Running 3 23d
運行程序進行測設:
vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/chart$ kubectl exec -ti k8sdemo-backend-5cd9d79856-dqlmz -- /bin/sh ~ # ./main.exe time="2019-11-27T07:03:03Z" level=debug msg="connect to database " time="2019-11-27T07:03:03Z" level=debug msg="dataSourceName:dbuser:dbuser@tcp(k8sdemo-database-service:3306)/service_config?charset=utf8" time="2019-11-27T07:03:03Z" level=debug msg="FindAll()" time="2019-11-27T07:03:03Z" level=debug msg="created=2019-10-21" time="2019-11-27T07:03:03Z" level=debug msg="find user:{1 Tony IT 2019-10-21}" time="2019-11-27T07:03:03Z" level=debug msg="find user list:[{1 Tony IT 2019-10-21}]" time="2019-11-27T07:03:03Z" level=debug msg="user lst:[{1 Tony IT 2019-10-21}]" ~ #
因爲篇幅有限,本文不可能把全部的問題都講清楚,還有兩個比較重要的問題,這裏簡單的提一下。
1.Secret:
本文用的都是明碼,若是須要加密的話有兩種方式,一種是 helm-secrets,另外一種是Vault,請閱讀相關文檔。
2.爲不一樣環境設置不一樣的常量:
本文只建立了針對一種環境的文件 ,若是你須要針對不一樣環境(例如DEV,QA,PROD)配置不一樣的參數的話,你能夠在「k8sdemo」的chart裏給不一樣的環境建立不一樣的"values.yaml",例如「values-dev.yaml」給DEV環境。但在子chart裏,就不能這樣作,由於系統要求"values.yaml"。這時,你能夠在父chart的「values-dev.yaml」裏爲不一樣的子chart建立常量,這樣這些常量就能覆蓋子chart裏定義的常量。
在「values-dev.yaml」加入下面代碼。
k8sdemo-backend: replicaCount: 2 k8sdemo-database: replicaCount: 2
鍵入以下命令試運行:
vagrant@ubuntu-xenial:~$ cd /home/vagrant/jfeng45/k8sdemo/script/kubernetes/chart vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/chart$ helm install --dry-run --values ./k8sdemo/values-dev.yaml --debug k8sdemo ./k8sdemo
查看結果,子chart中的相應參數已被覆蓋。
詳情請參閱How to set environment related values.yaml in Helm subcharts?
在調試過程當中仍是遇到了很多問題,但大多數都是與語法有關的問題,由於Helm和k8s都用的是YAML文件,而它對文件格式有着嚴格的要求,若是不知足要求就會報錯。幸虧它報錯時包含了錯誤代碼行號,這樣查找起來比較容易。
它的症狀是在用「helm install --dry-run --debug」調試時沒有問題,但正式運行時出了問題,用下面命令檢查,Pod的狀態是「CrashLoopBackOff」。
vagrant@ubuntu-xenial:~$ kubectl get pod NAME READY STATUS RESTARTS AGE k8sdemo-74cb7b997c-gn5v2 1/1 Running 1 47h k8sdemo-backend-6cdbb96964-tb2xd 0/1 CrashLoopBackOff 129 9h k8sdemo-database-deployment-578fc88c88-mm6x8 1/1 Running 12 37d k8sdemo-jenkins-deployment-675dd574cb-r57sb 1/1 Running 3 19d
這個問題我之前調試k8s時也碰到過,主要是與Docker鏡像有關,但此次明明鏡像是 好的。試了不少組合,最後終於發現是自動生成的代碼出了問題。
在「deployment.yaml」裏有下面代碼,這是Helm自動生成用來測試部署的。
livenessProbe: httpGet: path: / port: http readinessProbe: httpGet: path: / port: http
把它去掉以後就沒有問題了。並且它只在特定的chart(「k8sedemo-backend」)裏會出錯,在「k8sdemo」裏就沒有問題。我如今也不是特別清楚問題在哪,只是把它暫時刪除掉了。
它的症狀是宿主機的持久卷未能綁定到持久卷申請,致使持久卷申請又另外建立了一個持久卷。你用「kubectl get pv」就能看到新建立的持久卷,但實際上它是沒必要要的,只要把持久卷申請綁定到已有的PV上就好了。這個錯誤並非每次都發生,而是隨機的。大部分時間綁定正確,少數時候綁定錯誤。我開始想是否是由於執行k8s文件的順序問題,但k8s文件是按照文件類別(kind)來執行的,按理來講順序應該是正確的。再有一個可能就是時間延遲,由於建立持久卷鬚要時間,而若是持久卷申請沒有檢測到這個持久卷,那麼它就會另外建立一個。若是真是這樣的話,就要在建立時設定一個延遲。但它暫時來說對我影響不大,所以就偷了一下懶,之後有時間再來調試。
完整源碼的github連接:
k8sdemo
不堆砌術語,不羅列架構,不迷信權威,不盲從流行,堅持獨立思考