Helm
Kubernetes 包管理工具
Helm 可以幫助我們管理 Kubernetes 應用程序 - Helm Charts 可以定義、安裝和升級復雜的 Kubernetes 應用程序,Charts 包很容易創建、版本管理、分享和分布。Helm 對于 Kubernetes 來說就相當于 yum 對于 Centos 來說,如果沒有 yum 的話,我們在 Centos 下面要安裝一些應用程序是極度麻煩的,同樣的,對于越來越復雜的 Kubernetes 應用程序來說,如果單純依靠我們去手動維護應用程序的 YAML 資源清單文件來說,成本也是巨大的。接下來我們就來了解了 Helm 的使用方法。
安裝
首先當然需要一個可用的 Kubernetes 集群,然后在我們使用 Helm 的節點上已經配置好可以通過 kubectl 訪問集群,因為 Helm 其實就是讀取的 kubeconfig 文件來訪問集群的。
由于 Helm V2 版本必須在 Kubernetes 集群中安裝一個 Tiller 服務進行通信,這樣大大降低了其安全性和可用性,所以在 V3 版本中移除了服務端,采用了通用的 Kubernetes CRD 資源來進行管理,這樣就只需要連接上 Kubernetes 即可,而且 V3 版本已經發布了穩定版,所以我們這里來安裝最新的 v3.0.1 版本,軟件包下載地址為:https://github.com/helm/helm/releases,我們可以根據自己的節點選擇合適的包,比如我這里是Mac,就下載 MacOS amd64 的版本。
下載到本地解壓后,將 helm 二進制包文件移動到任意的 PATH 路徑下即可:
$ helm version version.BuildInfo{Version:"v3.0.1", GitCommit:"7c22ef9ce89e0ebeb7125ba2ebf7d421f3e82ffa", GitTreeState:"clean", GoVersion:"go1.13.4"}
看到上面的版本信息證明已經成功了。
一旦 Helm 客戶端準備成功后,我們就可以添加一個 chart 倉庫,當然最常用的就是官方的 Helm stable charts 倉庫,但是由于官方的 charts 倉庫地址需要ke xue shang wang,我們可以使用微軟的 charts 倉庫代替:
$ helm repo add stable http://mirror.azure.cn/kubernetes/charts/ $ helm repo list NAME URL stable http://mirror.azure.cn/kubernetes/charts/
安裝完成后可以用 search 命令來搜索可以安裝的 chart 包:
$ helm search repo stable NAME CHART VERSION APP VERSION DESCRIPTION stable/acs-engine-autoscaler 2.2.2 2.1.1 DEPRECATED Scales worker nodes within agent pools stable/aerospike 0.3.1 v4.5.0.5 A Helm chart for Aerospike in Kubernetes stable/airflow 5.2.1 1.10.4 Airflow is a platform to programmatically autho... stable/ambassador 5.1.0 0.85.0 A Helm chart for Datawire Ambassador stable/anchore-engine 1.3.7 0.5.2 Anchore container analysis and policy evaluatio... stable/apm-server 2.1.5 7.0.0 The server receives data from the Elastic APM a... ......
示例
為了安裝一個 chart 包,我們可以使用 helm install 命令,Helm 有多種方法來找到和安裝 chart 包,但是最簡單的方法當然是使用官方的 stable 這個倉庫直接安裝:
首先從倉庫中將可用的 charts 信息同步到本地,可以確保我們獲取到最新的 charts 列表:
$ helm repo update Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "stable" chart repository Update Complete. ? Happy Helming!?
比如我們現在安裝一個 mysql 應用:
$ helm install stable/mysql --generate-name NAME: mysql-1575619811 LAST DEPLOYED: Fri Dec 6 1614 2019 NAMESPACE: default STATUS: deployed REVISION: 1 NOTES: MySQL can be accessed via port 3306 on the following DNS name from within your cluster: mysql-1575619811.default.svc.cluster.local ......
我們可以看到 stable/mysql 這個 chart 已經安裝成功了,我們將安裝成功的這個應用叫做一個 release,由于我們在安裝的時候指定了--generate-name 參數,所以生成的 release 名稱是隨機生成的,名為 mysql-1575619811。我們可以用下面的命令來查看 release 安裝以后對應的 Kubernetes 資源的狀態:
$ kubectl get all -l release=mysql-1575619811 NAME READY STATUS RESTARTS AGE pod/mysql-1575619811-8479b5b796-dgggz 0/1 Pending 0 27m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/mysql-1575619811 ClusterIP 10.106.141.2283306/TCP 27m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/mysql-1575619811 0/1 1 0 27m NAME DESIRED CURRENT READY AGE replicaset.apps/mysql-1575619811-8479b5b796 1 1 0 27m
我們也可以 helm show chart 命令來了解 MySQL 這個 chart 包的一些特性:
$ helm show chart stable/mysql ......
如果想要了解更多信息,可以用 helm show all 命令:
$ helm show all stable/mysql ......
需要注意的是無論什么時候安裝 chart,都會創建一個新的 release,所以一個 chart 包是可以多次安裝到同一個集群中的,每個都可以獨立管理和升級。
同樣我們也可以用 Helm 很容易查看到已經安裝的 release:
$ helm ls NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION mysql-1575619811 default 1 2019-12-06 1614.682302 +0800 CST deployed mysql-1.5.0 5.7.27
如果需要刪除這個 release,也很簡單,只需要使用 helm uninstall 命令即可:
$ helm uninstall mysql-1575619811 release "mysql-1575619811" uninstalled $ kubectl get all -l release=mysql-1575619811 No resources found. $ helm status mysql-1575619811 Error: release: not found
uninstall 命令會從 Kubernetes 中刪除 release,也會刪除與 release 相關的所有 Kubernetes 資源以及 release 歷史記錄。也可以在刪除的時候使用--keep-history參數,則會保留 release 的歷史記錄,可以獲取該 release 的狀態就是 UNINSTALLED,而不是找不到 release了:
$ helm uninstall mysql-1575619811 --keep-history release "mysql-1575619811" uninstalled $ helm status mysql-1575619811 helm status mysql-1575619811 NAME: mysql-1575619811 LAST DEPLOYED: Fri Dec 6 16:47:14 2019 NAMESPACE: default STATUS: uninstalled ... $ helm ls -a NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION mysql-1575619811 default 1 2019-12-06 16:47:14.415214 +0800 CST uninstalled mysql-1.5.0 5.7.27
因為 Helm 會在刪除 release 后跟蹤你的 release,所以你可以審查歷史甚至取消刪除 release(使用 helm rollback 命令)。
定制
上面我們都是直接使用的 helm install 命令安裝的 chart 包,這種情況下只會使用 chart 的默認配置選項,但是更多的時候,是各種各樣的需求,索引我們希望根據自己的需求來定制 chart 包的配置參數。
我們可以使用 helm show values 命令來查看一個 chart 包的所有可配置的參數選項:
$ helm show values stable/mysql ## mysql image version ## ref: https://hub.docker.com/r/library/mysql/tags/ ## image: "mysql" imageTag: "5.7.14" busybox: image: "busybox" tag: "1.29.3" testFramework: enabled: true image: "dduportal/bats" tag: "0.4.0" ## Specify password for root user ## ## Default: random 10 character string # mysqlRootPassword: testing ## Create a database user ## # mysqlUser: ## Default: random 10 character string # mysqlPassword: ## Allow unauthenticated access, uncomment to enable ## # mysqlAllowEmptyPassword: true ## Create a database ## # mysqlDatabase: ## Specify an imagePullPolicy (Required) ## It's recommended to change this to 'Always' if the image tag is 'latest' ## ref: http://kubernetes.io/docs/user-guide/images/#updating-images ## imagePullPolicy: IfNotPresent ......
上面我們看到的所有參數都是可以用自己的數據來覆蓋的,可以在安裝的時候通過 YAML 格式的文件來傳遞這些參數:
$ cat config.yaml mysqlUser: user0 mysqlPassword: user0pwd mysqlDatabase: user0db persistence: enabled: false $ helm install -f config.yaml stable/mysql helm install -f config.yaml mysql stable/mysql NAME: mysql LAST DEPLOYED: Fri Dec 6 1756 2019 NAMESPACE: default STATUS: deployed REVISION: 1 NOTES: MySQL can be accessed via port 3306 on the following DNS name from within your cluster: mysql.default.svc.cluster.local ......
release 安裝成功后,可以查看對應的 Pod 信息:
$ kubectl get pod -l release=mysql NAME READY STATUS RESTARTS AGE mysql-ddd798f48-gnrzd 0/1 PodInitializing 0 119s $ kubectl describe pod mysql-ddd798f48-gnrzd ...... Environment: MYSQL_ROOT_PASSWORD:Optional: false MYSQL_PASSWORD: Optional: false MYSQL_USER: user0 MYSQL_DATABASE: user0db ......
可以看到環境變量 MYSQL_USER=user0,MYSQL_DATABASE=user0db 的值和我們上面配置的值是一致的。在安裝過程中,有兩種方法可以傳遞配置數據:
--values(或者 -f):指定一個 YAML 文件來覆蓋 values 值,可以指定多個值,最后邊的文件優先
--set:在命令行上指定覆蓋的配置
如果同時使用這兩個值,--set將被合并到具有更高優先級的--values,使用--set指定的值將持久化在 ConfigMap 中,對于給定的 release,可以使用helm get values
--set選項接收零個或多個 name/value 對,最簡單的用法就是--set name=value,相當于 YAML 文件中的:
name: value
多個值之間用字符串“,”隔開,用法就是--set a=b,c=d,相當于 YAML 文件中的:
a: b c: d
也支持更加復雜的表達式,例如--set outer.inner=value,對應 YAML:
outer: inner: value
對于列表數組可以用{}來包裹,比如-set name={a, b, c},對應 YAML:
name: - a - b - c
從 Helm 2.5.0 開始,就可以使用數組索引語法來訪問列表中某個項,比如--set servers[0].port=80,對應的 YAML 為:
servers: - port: 80
也可以這樣設置多個值,比如-set servers[0].port=80,servers[0].host=example,對應的 YAML 為:
servers - port: 80 host: example
有時候你可能需要在--set選項中使用特殊的字符,這個時候可以使用反斜杠來轉義字符,比如--set name=value1,value2,對應的 YAML 為:
name: "value1,value2"
類似的,你還可以轉義.,當 chart 模板中使用 toYaml 函數來解析 annotations、labels 以及 node selectors 之類的時候,這非常有用,比如--set nodeSelector."kubernetes.io/role"=master,對應的 YAML 文件:
nodeSelector: kubernetes.io/role: master
深度嵌套的數據結構可能很難使用--set來表示,所以一般推薦還是使用 YAML 文件來進行覆蓋,當然在設計 chart 模板的時候也可以結合考慮到--set這種用法,盡可能的提供更好的支持。
更多安裝方式
helm install 命令可以從多個源進行安裝:
chart 倉庫(類似于上面我們提到的)
本地 chart 壓縮包(helm install foo-0.1.1.tgz)
本地解壓縮的 chart 目錄(helm install foo path/to/foo)
在線的 URL(helm install foolhttps://example.com/charts/foo-1.2.3.tgz)
升級或回滾
當新版本的 chart 包發布的時候,或者當你要更改 release 的配置的時候,你可以使用 helm upgrade 命令來操作。升級需要一個現有的 release,并根據提供的信息對其進行升級。因為 Kubernetes charts 可能很大而且很復雜,Helm 會嘗試以最小的侵入性進行升級,它只會更新自上一版本以來發生的變化:
$ helm upgrade -f panda.yaml mysql stable/mysql helm upgrade -f panda.yaml mysql stable/mysql Release "mysql" has been upgraded. Happy Helming! NAME: mysql LAST DEPLOYED: Fri Dec 6 2111 2019 NAMESPACE: default STATUS: deployed REVISION: 2 ...
我們這里 mysql 這個 release 用相同的 chart 包進行升級,但是新增了一個配置項:
mysqlRootPassword: passw0rd
我們可以使用helm get values來查看新設置是否生效:
$ helm get values mysql USER-SUPPLIED VALUES: mysqlDatabase: user0db mysqlPassword: user0pwd mysqlRootPassword: passw0rd mysqlUser: user0 persistence: enabled: false
helm get 命令是查看集群中 release 的非常有用的命令,正如我們在上面看到的,它顯示了 panda.yaml 中的新配置值被部署到了集群中,現在如果某個版本在發布期間沒有按計劃進行,那么可以使用 helm rollback [RELEASE] [REVISION] 命令很容易回滾到之前的版本:
$ helm ls NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION mysql default 2 2019-12-06 21:06:11.36358 +0800 CST deployed mysql-1.5.0 5.7.27 $ helm history mysql REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION 1 Fri Dec 6 17:53:03 2019 superseded mysql-1.5.0 5.7.27 Install complete 2 Fri Dec 6 21:06:11 2019 deployed mysql-1.5.0 5.7.27 Upgrade complete $ helm rollback mysql 1 Rollback was a success! Happy Helming! $ kubectl get pods -l release=mysql NAME READY STATUS RESTARTS AGE mysql-ddd798f48-gnrzd 1/1 Running 0 3h25m $ helm get values mysql USER-SUPPLIED VALUES: mysqlDatabase: user0db mysqlPassword: user0pwd mysqlUser: user0 persistence: enabled: false
可以看到 values 配置已經回滾到之前的版本了。上面的命令回滾到了 release 的第一個版本,每次進行安裝、升級或回滾時,修訂號都會加 1,第一個修訂號始終為1,我們可以使用helm history [RELEASE]來查看某個版本的修訂號。
除此之外我們還可以指定一些有用的選項來定制install/upgrade/rollback的一些行為,要查看完整的參數標志,我們可以運行helm
--timeout: 等待 Kubernetes 命令完成的時間,默認是 300(5分鐘)
--wait: 等待直到所有 Pods 都處于就緒狀態、PVCs 已經綁定、Deployments 具有處于就緒狀態的最小 Pods 數量(期望值減去 maxUnavailable)以及 Service 有一個 IP 地址,然后才標記 release 為成功狀態。它將等待與 --timeout 值一樣長的時間,如果達到超時,則 release 將標記為失敗。注意:在 Deployment 將副本設置為 1 并且作為滾動更新策略的一部分,maxUnavailable 未設置為0的情況下,--wait 將返回就緒狀態,因為它已滿足就緒狀態下的最小 Pod 數量
--no-hooks: 將會跳過命令的運行 hooks
--recreate-pods: 僅適用于 upgrade 和 rollback,這個標志將導致重新創建所有的 Pods。(Helm3 中啟用了)
Charts
Helm 使用一種名為 charts 的包格式,一個 chart 是描述一組相關的 Kubernetes 資源的文件集合,單個 chart 可能用于部署簡單的應用,比如 memcached pod,或者復雜的應用,比如一個帶有 HTTP 服務、數據庫、緩存等等功能的完整 web 應用程序。
Charts 是創建在特定目錄下面的文件集合,然后可以將它們打包到一個版本化的存檔中來部署。接下來我們就來看看使用 Helm 構建 charts 的一些基本方法。
文件結構
chart 被組織為一個目錄中的文件集合,目錄名稱就是 chart 的名稱(不包含版本信息),下面是一個 WordPress 的 chart,會被存儲在 wordpress/ 目錄下面,基本結構如下所示:
wordpress/ Chart.yaml # 包含當前 chart 信息的 YAML 文件 LICENSE # 可選:包含 chart 的 license 的文本文件 README.md # 可選:一個可讀性高的 README 文件 values.yaml # 當前 chart 的默認配置 values values.schema.json # 可選: 一個作用在 values.yaml 文件上的 JSON 模式 charts/ # 包含該 chart 依賴的所有 chart 的目錄 crds/ # Custom Resource Definitions templates/ # 模板目錄,與 values 結合使用時,將渲染生成 Kubernetes 資源清單文件 templates/NOTES.txt # 可選: 包含簡短使用使用的文本文件
另外 Helm 會保留 charts/、crds/ 以及 templates/ 目錄以及上面列出的文件名的使用。
Chart.yaml 文件
對于一個 chart 包來說 Chart.yaml 文件是必須的,它包含下面的這些字段:
apiVersion: chart API 版本 (必須) name: chart 名 (必須) version: SemVer 2版本 (必須) kubeVersion: 兼容的 Kubernetes 版本 (可選) description: 一句話描述 (可選) type: chart 類型 (可選) keywords: - 當前項目關鍵字集合 (可選) home: 當前項目的 URL (可選) sources: - 當前項目源碼 URL (可選) dependencies: # chart 依賴列表 (可選) - name: chart 名稱 (nginx) version: chart 版本 ("1.2.3") repository: 倉庫地址 ("https://example.com/charts") maintainers: # (可選) - name: 維護者名字 (對每個 maintainer 是必須的) email: 維護者的 email (可選) url: 維護者 URL (可選) icon: chart 的 SVG 或者 PNG 圖標 URL (可選). appVersion: 包含的應用程序版本 (可選). 不需要 SemVer 版本 deprecated: chart 是否已被棄用 (可選, boolean)
其他字段默認會被忽略。
版本
每個 chart 都必須有一個版本號,版本必須遵循 SemVer2 標準,和 Helm Classic 不同,Kubernetes Helm 使用版本號作為 release 的標記,倉庫中的軟件包通過名稱加上版本號來標識的。
例如,將一個 nginx 的 chart 包 version 字段設置為:1.2.3,則 chart 最終名稱為:
nginx-1.2.3.tgz
還支持更復雜的 SemVer2 名稱,例如版本:1.2.3-alpha.1+ef365,但是需要注意的是系統明確禁止使用非 SemVer 的名稱。
Chart.yaml 中的 version 字段被很多 Helm 工具使用,包括 CLI 工具,生成包的時候,命令 helm package 將使用該字段作為包名稱中的標記,系統是默認 Chart 包中的版本號與 chart.yaml 中的版本號匹配的,所以如果不匹配的話就導致一系列錯誤。
apiVersion 字段
對于 Helm 3 以上的版本 apiVersion 字段應該是 v2,之前版本的 Chart 應該設置為1,并且也可以有 Helm 3 進行安裝。
appVersion 字段
要注意 appVersion 字段與 version 字段無關,這是一種指定應用程序版本的方法,比如 drupal 的 Chart 包可能有一個 appVersion: 8.2.1 的字段,表示 Chart 中包含的 drupal 版本是 8.2.1,該字段僅供參考,對 Chart 版本的計算不會產生影響。
棄用 Charts
當在 Chart 倉庫中管理 charts 的時候,有時候需要棄用一個 chart,Chart.yaml 中的可選字段 deprecated 可以用來標記一個 chart 為棄用狀態。如果將倉庫中最新版本的 chart 標記為棄用,則整個 chart 都會被當做棄用狀態了。以后可以通過發布一個未被標記為棄用狀態的新版本來重新使用該 chart。棄用 charts 的工作流程如下所示:
更新 chart 的 Chart.yaml 來標記 chart 為棄用狀態
發布該新版本到 Chart 倉庫
從源碼倉庫(比如 git)中刪除 chart
Chart 類型
type 字段定義 chart 的類型,可以定義兩種類型:應用程序(application)和庫(library)。應用程序是默認的類型,它是一個可以完整操作的標準 chart,庫或者輔助類 chart 為 chart 提供了一些實用的功能,library 不同于應用程序 chart,因為它沒有資源對象,所以無法安裝。
一個應用 chart 也可以當作庫進行使用。通過將類型設置為 library,然后該 chart 就會渲染成一個庫,可以在其中使用所有的實用性功能,chart 的所有資源對象都不會被渲染。
LICENSE, README 和 NOTES
Chart 還可以包含用于描述 chart 的安裝、配置、用法和許可證書的文件。
LICENSE 是一個純文本文件,其中包含 chart 的許可證書。chart 可以包含一個許可證書,因為它可能在模板中具有編程邏輯,所以不只是配置,如果需要,chart 還可以為應用程序提供單獨的 license(s)。
Chart 的 README 文件應該采用 Markdown(README.md)格式,并且通常應該包含如下的一些信息:
chart 提供的應用程序的描述信息
運行 chart 的任何先決條件或者要求
values.yaml 和默認值中的一些選項說明
與 chart 的安裝或配置有關的任何其他信息
chart 還可以包含簡短的純文本模板或者 NOTES.txt 文件,該文件將在安裝后以及查看 release 狀態的時候打印出來。該文件會被當成模板文件,并且可以用于顯示使用說明,后續步驟或與 release 有關的任何其他信息。例如,可以提供用于連接到數據或訪問 Web UI 的指令。由于在運行 helm install 或者 helm status 的時候該文件會打印到 STDOUT 中,所以建議該文件內容保持內容簡短然后可以指向 README 文件來獲取更多詳細信息。
依賴
在 Helm 中,一個 chart 包可能會依賴許多其他的 chart。這些依賴關系可以使用 Chart.yaml 中的依賴關系字段動態鏈接,也可以引入到 charts/ 目錄手動進行管理。
使用 dependencies 字段管理依賴
當前 chart 所需的依賴 chart 需要在 dependencies 字段中進行定義,如下所示:
dependencies: - name: apache version: 1.2.3 repository: https://example.com/charts - name: mysql version: 3.2.1 repository: https://another.example.com/charts
name 字段是所依賴的 chart 的名稱
version 字段是依賴的 chart 版本
repository 字段是 chart 倉庫的完整 URL,不過需要注意,必須使用 helm repo add 在本地添加該 repo
定義了依賴項后,可以運行 helm dependency update 來更新依賴項,它將根據你的依賴項文件把你所有指定的 chart 包下載到 charts/ 目錄中:
$ helm dependency update foochart Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "local" chart repository ...Successfully got an update from the "stable" chart repository ...Successfully got an update from the "example" chart repository ...Successfully got an update from the "another" chart repository Update Complete. Happy Helming! Saving 2 charts Downloading apache from repo https://example.com/charts Downloading mysql from repo https://another.example.com/charts
當執行 helm dependency update 命令的時候會解析 chart 的依賴項,會將他們作為 chart 包文件下載存放到 charts/ 目錄中,所以,對于上面的示例,我們可以在 charts 目錄中看到如下的文件:
charts/ apache-1.2.3.tgz mysql-3.2.1.tgz
alias 字段
除了上面的幾個字段之外,每個依賴項還可以包含一個可選的 alias 別名字段。為依賴 chart 添加別名將使用別名作為依賴的名稱。在需要訪問其他名稱的 chart 情況下,就可以使用別名,如下所示:
# parentchart/Chart.yaml dependencies: - name: subchart repository: http://localhost:10191 version: 0.1.0 alias: new-subchart-1 - name: subchart repository: http://localhost:10191 version: 0.1.0 alias: new-subchart-2 - name: subchart repository: http://localhost:10191 version: 0.1.0
在上面示例中,我們將獲得3個依賴項:
subchart new-subchart-1 new-subchart-2
當然其實我們也可以手動來實現,將同一個 chart 以不同的名稱多次復制/粘貼到 charts/ 目錄中也是可以的。
TEMPLATES 和 VALUES
Helm Chart 模板是用 Go template 語言 進行編寫的,另外還額外增加了(【Sprig】](https://github.com/Masterminds/sprig)庫中的50個左右的附加模板函數和一些其他專用函數。
所有模板文件都存儲在 chart 的 templates/ 目錄下面,當 Helm 渲染 charts 的時候,它將通過模板引擎傳遞該目錄中的每個文件。模板的 Values 可以通過兩種方式提供:
Chart 開發人員可以在 chart 內部提供一個名為 values.yaml 的文件,該文件可以包含默認的 values 值內容。
Chart 用戶可以提供包含 values 值的 YAML 文件,可以在命令行中通過 helm install 來指定該文件。
當用戶提供自定義 values 值的時候,這些值將覆蓋 chart 中 values.yaml 文件中的相應的值。
模板文件
模板文件遵循編寫 Go 模板的標準約定(可以查看 text/template 包文檔查看詳細信息),下面是一個模板文件示例:
apiVersion: v1 kind: ReplicationController metadata: name: deis-database namespace: deis labels: app.kubernetes.io/managed-by: deis spec: replicas: 1 selector: app.kubernetes.io/name: deis-database template: metadata: labels: app.kubernetes.io/name: deis-database spec: serviceAccount: deis-database containers: - name: deis-database image: {{ .Values.imageRegistry }}/postgres:{{ .Values.dockerTag }} imagePullPolicy: {{ .Values.pullPolicy }} ports: - containerPort: 5432 env: - name: DATABASE_STORAGE value: {{ default "minio" .Values.storage }}
上面這個示例是 Kubernetes replication 控制器的一個模板,它可以使用以下4個模板值(通常在 values.yaml 文件中定義的):
imageRegistry:Docker 鏡像倉庫
dockerTag:Docker 鏡像 tag
pullPolicy:鏡像拉取策略
storage:存儲后端,默認設置為 "minio"
這些所有的 values 值都是有模板作者來定義的,Helm 不會也不需要規定這些參數。可以可以查看Kubernetes Charts 項目去了解更多的 charts 項目的詳細內容。
預定義 Values
在模板中用 .Values 可以獲取到 values.yaml 文件(或者 --set 參數)提供的 values 值,此外,還可以在模板中訪問其他預定義的數據。下面是一些預定義的、可用于每個模板、并且不能被覆蓋的 values 值,與所有 values 值一樣,名稱都是區分大小寫的:
Release.Name:release 的名稱(不是 chart)
Release.Namespace:release 被安裝到的命名空間
Release.Service:渲染當前模板的服務,在 Helm 上,實際上該值始終為 Helm
Release.IsUpgrade:如果當前操作是升級或回滾,則該值為 true
Release.IsInstall:如果當前操作是安裝,則該值為 true
Chart:Chart.yaml 文件的內容,可以通過 Chart.Version 來獲得 Chart 的版本,通過 Chart.Maintainers 可以獲取維護者信息
Files: 一個包含 chart 中所有非特殊文件的 map 對象,這不會給你訪問模板的權限,但是會給你訪問存在的其他文件的權限(除非使用 .helmignore 排除它們),可以使用 {{ index .Files "file.name" }} 或者 {{ .Files.Get name }} 或者 {{ .Files.GetString name }} 函數來訪問文件,你還可以使用 {{ .Files.GetBytes }} 以 []byte 的形式獲取訪問文件的內容
Capabilities:也是一個類 map 的對象,其中包含有關 Kubernetes 版本({{ .Capabilities.KubeVersion }})和支持的 Kubernetes API 版本({{ .Capabilities.APIVersions.Has "batch/v1" }})信息。
任何未知的 Chart.yaml 字段都會被刪除,在 Chart 對象內部無法訪問他們,所以,Chart.yaml 不能用于將任意結構化的數據傳遞到模板中,但是可以使用 values 文件來傳遞。
Values 文件
為模板提供一些必須的 values 值的 values.yaml 文件如下所示:
imageRegistry: "quay.io/deis" dockerTag: "latest" pullPolicy: "Always" storage: "s3"
values 文件的格式是 YAML,一個 chart 包可能包含一個默認的 values.yaml 文件,helm install 命令允許用戶通過提供其他的 YAML 值文件來覆蓋默認的值:
$ helm install --values=myvals.yaml wordpress
用這種方式來傳遞 values 值的時候,它們將合并到默認值文件中,比如有一個 myvals.yaml 文件如下所示:
storage: "gcs"
將其與 chart 的 values.yaml 文件合并后,得到的結果為:
imageRegistry: "quay.io/deis" dockerTag: "latest" pullPolicy: "Always" storage: "gcs"
我們可以看到只有最后一個字段被覆蓋了。
chart 內包含的默認 values 文件必須命名為 values.yaml,但是在命令行上指定的文件可以任意命名。 如果在 helm install 或者 helm upgrade 的時候使用 --set 參數,則這些值將在客戶端轉換為 YAML 格式。 如果 values 文件存在任何必須的條目,則可以使用 required 函數在 chart 模板中將它們聲明為必須選項。
然后我們就可以使用.Values對象在模板中訪問任意一個 values 值,類似于下面的模板文件:
apiVersion: v1 kind: ReplicationController metadata: name: deis-database namespace: deis labels: app.kubernetes.io/managed-by: deis spec: replicas: 1 selector: app.kubernetes.io/name: deis-database template: metadata: labels: app.kubernetes.io/name: deis-database spec: serviceAccount: deis-database containers: - name: deis-database image: {{ .Values.imageRegistry }}/postgres:{{ .Values.dockerTag }} imagePullPolicy: {{ .Values.pullPolicy }} ports: - containerPort: 5432 env: - name: DATABASE_STORAGE value: {{ default "minio" .Values.storage }}
作用范圍、依賴和 Values
values 文件可以聲明頂級的 chart 以及該 chart 的 charts/ 目錄中包含的任何 chart 的值。或者,換句話說,values 文件可以為 chart 以及他的任何依賴項提供 values 值。例如,上面提到了 WordPress 這個 chart 同時依賴 mysql 和 apache 這兩個依賴項,values 文件可以為所有這些組件提供 values 值:
title: "My WordPress Site" # 傳遞到 WordPress 模板 mysql: max_connections: 100 # 傳遞到 MySQL password: "secret" apache: port: 8080 # 傳遞到 Apache
較高級別的 Charts 可以訪問下面定義的所有變量,所以,WordPress 這個 chart 可以通過 .Values.mysql.password 來訪問 MySQL 的密碼,但是較低級別的 chart 是無法訪問父 chart 中的內容的,所有 MySQL 無法獲取到 title 屬性,當然同樣也不能訪問 apache.port。
Values 是有命名空間的,但是會對其進行調整,比如對于 WordPress 這個 chart 來說,它可以通過.Values.mysql.password來進行訪問,但是對于 MySQL 這個 chart 本身來說,values 的范圍縮小了,命名空間前綴會被刪除,所以它只需要通過 .Values.password 就可以訪問到。
全局 Values
從 2.0.0-Alpha.2 版本開始,Helm 開始支持特殊的 global 全局值,比如將上面的示例修改如下:
title: "My WordPress Site" # 傳遞到 WordPress 模板 global: app: MyWordPress mysql: max_connections: 100 # 傳遞到 MySQL password: "secret" apache: port: 8080 # 傳遞到 Apache
上面我們添加了一個全局范圍的 value 值:app: MyWordPress,該值可以通過.Values.global.app提供給所有 chart 使用。
例如,mysql 模板可以以{{ .Values.global.app }}來訪問 app,apache chart 也可以,實際上,上面的 values 文件會這樣重新生成:
title: "My WordPress Site" # 傳遞到 WordPress 模板 global: app: MyWordPress mysql: global: app: MyWordPress max_connections: 100 # 傳遞到 MySQL password: "secret" apache: global: app: MyWordPress port: 8080 # 傳遞到 Apache
這種方式提供了一種與所有子 chart 共享一個頂級變量的方式,這對于設置 meta 數據這種屬性是非常有用的。如果子 chart 聲明了全局變量,則該全局變量將向下(傳遞到子 chart 的子 chart 中)傳遞,而不會向上傳遞到父 chart,子 chart 無法影響 父 chart的值。同樣,父 chart 的全局遍歷優先與子 chart 中的全局變量。
Schema 文件
有時候,chart 開發者可能希望在其 values 值上面定義一個結構,這種情況下可以通過在 values.schema.json 文件中定義一個 schema 來完成,這里的 schema 就是一個 JSON Schema 文件結構規范,如下所示:
{ "$schema": "https://json-schema.org/draft-07/schema#", "properties": { "image": { "description": "Container Image", "properties": { "repo": { "type": "string" }, "tag": { "type": "string" } }, "type": "object" }, "name": { "description": "Service name", "type": "string" }, "port": { "description": "Port", "minimum": 0, "type": "integer" }, "protocol": { "type": "string" } }, "required": [ "protocol", "port" ], "title": "Values", "type": "object" }
該 schema 會對 values 值進行校驗,調用以下任何命令時,都會進行驗證:
helm install
helm upgrade
helm lint
helm template
比如下面的示例文件就可以滿足上面的 schema 要求:
name: frontend protocol: https port: 443
需要注意的是該 schema 將應用于最終的.Values對象,而不僅僅是應用于 values.yaml 文件,所以下面的文件也是可以滿足 schema 要求的:
name: frontend protocol: https
因為在安裝的時候我們通過--set選項傳遞了必須的 port 屬性:
$ helm install --set port=443
此外,還會根據所有的子 chart schemas 來檢查最終的.Values對象,這意味著父 chart 無法規避對子 chart 的限制。同樣的,如果子 chart 要求未滿足子 chart 的 values.yaml 文件,則父 chart 必須滿足這些限制才能生效。
參考文檔
在編寫模板、values、和 schema 文件的時候,下面這些文檔可以提供一些幫助:
Go Template
額外的模板函數
YAML 文件
JSON Schema
CRDS
Kubernetes 提供了一種聲明新類型的 Kubernetes 對象的機制,使用 CustomResourceDefinitions(CRDS)可以讓 Kubernetes 開發人員聲明自定義資源類型。
在 Helm 3 中,CRD 被視為一種特殊的對象,它們在 chart 部分之前被安裝,并且會受到一些限制。CRD YAML 文件應該放置 chart 內的 crds/ 目錄下面。多個 CRDs 可以放在同一個文件中,Helm 將嘗試將 CRD 目錄中的所有文件加載到 Kubernetes 中。
需要注意的是 CRD 文件不能模板化,它們必須是純的 YAML 文件。
當 Helm 安裝一個新的 chart 的時候,它將會安裝 CRDs,然后會暫停直到 API Server 提供 CRD 為止,然后才開始啟動模板引擎,渲染其余的 chart 模板,并將其安裝到 Kubernetes 中。由于這個安裝順序,CRD 信息在 Helm 模板的 .Capabilities 對象中是可以獲取到的,并且 Helm 模板可能會創建在 CRD 中聲明的對象的新實例。
比如,如果你的呃 chart 在 crds 目錄下面有一個 CronTab 的 CRD,則可以在 templates/ 目錄下面創建 CronTab 類型的實例:
crontabs/ Chart.yaml crds/ crontab.yaml templates/ mycrontab.yaml
crontab.yaml 文件必須包含不帶模板指定的 CRD:
kind: CustomResourceDefinition metadata: name: crontabs.stable.example.com spec: group: stable.example.com versions: - name: v1 served: true storage: true scope: Namespaced names: plural: crontabs singular: crontab kind: CronTab
然后模板 mycrontab.yaml 可以創建一個新的 CronTab(和平時使用模板一樣):
apiVersion: stable.example.com kind: CronTab metadata: name: {{ .Values.name }} spec: # ...
在繼續安裝 templates/ 之前,Helm 會確保已經安裝上了 CronTab 類型,并且可以從 Kubernetes API server 上獲得該類型。
CRDs 的限制
與 Kubernetes 中的大多數對象不同,CRDs 是全局安裝的,所以 Helm 在管理 CRD 的時候比較謹慎,會有一些限制:
CRDs 不會重新安裝,如果 Helm 確定 crds/ 目錄中的 CRD 已經存在(無論版本如何),Helm 都不會重新安裝或升級。
CRDs 不會在升級或回滾的時候安裝,只會在安裝操作的時候創建 CRDs。
CRDs 不會被刪除,刪除 CRD 會自動刪除集群中所有 namespace 中的 CRDs 內容,所以 Helm 不會刪除 CRD。
Helm 希望想要升級或刪除 CRDs 的操作人員可以手動來仔細地操作。
使用 Helm 管理 Charts
helm 工具有幾個用于操作 charts 的命令,如下所示。
創建一個新的 chart 包:
$ helm create mychart Created mychart/
一旦你已經編輯了一個 chart 包,Helm 可以將其打包到一個獨立文件中:
$ helm package mychart Archived mychart-0.1.-.tgz
你還可以使用 helm 幫助你查找 chart 包的格式要求方面或其他問題:
$ helm lint mychart No issues found
Chart 倉庫
chart 倉庫實際上就是一個 HTTP 服務器,其中包含一個或多個打包的 chart 包,雖然可以使用 helm 來管理本地 chart 目錄,但是在共享 charts 的時候,最好的還是使用 chart 倉庫。
可以提供 YAML 文件和 tar文件并可以相應 GET 請求的任何 HTTP 服務器都可以作為 chart 倉庫服務器。倉庫的主要特征是存在一個名為 index.yaml 的特殊文件,該文件具有倉庫中提供的所有軟件包的列表以及允許檢索和驗證這些軟件包的元數據。
在客戶端,可以使用 helm repo 命令來管理倉庫,但是 Helm 不提供用于將 chart 上傳到遠程 chart 倉庫的工具。
模板開發
內置對象
前面我們介紹了 Helm Chart 的一些基本概念和使用,接下來我們重點介紹下 Chart 模板的編寫。模板會渲染成 Kubernetes 的資源清單文件,下面我們將來學習下模板的結構,如何使用它們,如何編寫 Go 模板以及如何調試。
對象從模板引擎傳遞到模板中,在代碼中可以傳遞對象,也有一種方法可以在模板宏創建新的對象,比如 tuple 函數。對象可以很簡單,也可以包含其他對象或函數,例如,Release 對象就包含幾個對象(比如 Release.Name),Files 對象就包含幾個函數。
前面提到過我們可以在模板中使用 {{ .Release.Name }} 獲取 release 的名稱,Release 是我們可以在模板中訪問的幾個頂級對象之一:
Release:該對象描述了 release 本身的相關信息,它內部有幾個對象:
Release.Name:release 名稱
Release.Namespace:release 安裝到的命名空間
Release.IsUpgrade:如果當前操作是升級或回滾,則該值為 true
Release.IsInstall:如果當前操作是安裝,則將其設置為 true
Release.Revision:release 的 revision 版本號,在安裝的時候,值為1,每次升級或回滾都會增加
Reelase.Service:渲染當前模板的服務,在 Helm 上,實際上該值始終為 Helm
Values:從 values.yaml 文件和用戶提供的 values 文件傳遞到模板的 Values 值,默認情況下,Values 是空的。
Chart:獲取 Chart.yaml 文件的內容,該文件中的任何數據都可以訪問,例如 {{ .Chart.Name }}-{{ .Chart.Version}} 可以渲染成 mychart-0.1.0,該對象下面可用的字段前面我們已經提到過了。
Files:可以訪問 chart 中的所有非特殊文件,雖然無法使用它來訪問模板文件,但是可以來訪問 chart 中的其他文件。
Files.Get:用于根據名稱獲取文件(比如 .Files.Get config.ini)
Files.GetBytes:用于以 bytes 數組而不是字符串的形式來獲取文件內容的函數,這對于類似于圖片之類的東西很有用
Files.Glob:用于返回名稱于給定的 shell glob 模式匹配的文件列表
Files.Lines:可以逐行讀取文件的函數,對于遍歷文件中的每行內容很有用
Files.AsSecrets:將文件內容以 Base64 編碼的字符串返回的函數
Files.AsConfig:將文件正文作為 YAML 字典返回的函數
Capabilities:提供了獲取有關 Kubernetes 集群支持功能的信息的對象
Capabilities.APIVersions:支持的版本集合
Capabilities.APIVersions.Has $version:判斷一個版本(比如 batch/v1)或資源(比如 apps/v1/Deployment)是否可用
Capabilities.Kube.Version:Kubernetes 的版本
Capabilities.Kube:是 Kubernetes 版本的縮寫
Capabilities.Kube.Major:Kubernetes 主版本
Capabilities.Kube.Minor:Kubernetes 的次版本
Template:包含當前正在執行的模板的相關信息
Name:當前模板的命名空間文件路徑(比如 mychart/templates/mytemplate.yaml)
BaePath:當前 chart 的模板目錄的命名空間路徑(比如 mychart/templates)
需要注意的是內置的對象始終是以大寫字母開頭的,這也是符合 Go 的命名約定的,創建自己的名稱的時候,可以自由使用適合你團隊的約定,一些團隊,比如 Kubernetes Charts 團隊,選擇僅使用首字母小寫,以區分本地名稱和內置名稱,這里我們也會遵循該約定。
Values
前面我們介紹了 Helm 模板提供的內置對象,其中就有一個內置對象 Values,該對象提供對傳遞到 chart 忠的 values 值的訪問,其內容主要有4個來源:
chart 文件中的 values.yaml 文件
如果這是子 chart,父 chart 的 values.yaml 文件
用 -f 參數傳遞給 helm install 或 helm upgrade 的 values 值文件(例如 helm install -f myvals.yaml ./mychart)
用--set傳遞的各個參數(例如 helm install --set foo=bar ./mychart)
values.yaml 文件是默認值,可以被父 chart 的 values.yaml 文件覆蓋,而后者又可以由用戶提供的 values 值文件覆蓋,而該文件又可以被--set參數覆蓋。
values 值文件是純 YAML 文件,我們可以來編輯 mychart/values.yaml 文件然后編輯 ConfigMap 模板。刪除 values.yaml 中的默認設置后,我們將只設置一個參數:
favoriteDrink: coffee
現在我們可以在模板中直接使用它:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favoriteDrink }}
可以看到在最后一行我們將 favoriteDrink 作為 Values 的屬性進行訪問:{{ .Values.favoriteDrink }}。我們可以來看看是如何渲染的:
$ helm install --generate-name --dry-run --debug ./mychart install.go [debug] Original chart version: "" install.go [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/course/k8strain/content/helm/manifests/mychart NAME: mychart-1575963545 LAST DEPLOYED: Tue Dec 10 1506 2019 NAMESPACE: default STATUS: pending-install REVISION: 1 TEST SUITE: None USER-SUPPLIED VALUES: {} COMPUTED VALUES: favoriteDrink: coffee HOOKS: MANIFEST: --- # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575963545-configmap data: myvalue: "Hello World" drink: coffee
由于在默認的 values.yaml 文件中將 favoriteDrink 設置為了 coffee,所以這就是模板中顯示的值,我們可以通過在調用 helm install 的過程中添加--set參數來覆蓋它:
$ helm install --generate-name --dry-run --debug --set favoriteDrink=slurm ./mychart install.go [debug] Original chart version: "" install.go [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/course/k8strain/content/helm/manifests/mychart NAME: mychart-1575963760 LAST DEPLOYED: Tue Dec 10 1543 2019 NAMESPACE: default STATUS: pending-install REVISION: 1 TEST SUITE: None USER-SUPPLIED VALUES: favoriteDrink: slurm COMPUTED VALUES: favoriteDrink: slurm HOOKS: MANIFEST: --- # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575963760-configmap data: myvalue: "Hello World" drink: slurm
因為--set的優先級高于默認的 values.yaml 文件,所以我們的模板會生成 drink: slurm。Values 值文件也可以包含更多結構化的內容,例如我們可以在 values.yaml 文件中創建一個 favorite 的部分,然后在其中添加幾個 keys:
favorite: drink: coffee food: pizza
現在我們再去修改下我們的模板:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink }} food: {{ .Values.favorite.food }}
雖然我們可以通過這種方式來構造數據,但是還是建議你將 values 樹保持更淺,這樣在使用的時候更加簡單。當我們考慮為子 chart 分配 values 值的時候,我們就可以看到如何使用樹形結構來命名 values 值了。
刪除默認 KEY
如果你需要從默認值中刪除 key,則可以將該 key 的值覆蓋為 null,在這種情況下,Helm 將從覆蓋的 values 中刪除該 key。例如,在 Drupal chart 中配置一個 liveness 探針:
livenessProbe: httpGet: path: /user/login port: http initialDelaySeconds: 120
如果你想使用--set livenessProbe.exec.command=[cat, docroot/CHANGELOG.txt]將 livenessProbe 的處理程序覆蓋為 exec 而不是 httpGet,則 Helm 會將默認鍵和覆蓋鍵合并在一起,如下所示:
livenessProbe: httpGet: path: /user/login port: http exec: command: - cat - docroot/CHANGELOG.txt initialDelaySeconds: 120
但是,這樣卻有一個問題,因為你不能聲明多個 livenessProbe 處理程序,為了解決這個問題,你可以讓 Helm 通過將 livenessProbe.httpGet 設置為 null 來刪除它:
$ helm install stable/drupal --set image=my-registry/drupal:0.1.0 --set livenessProbe.exec.command=[cat, docroot/CHANGELOG.txt] --set livenessProbe.httpGet=null
到這里我們已經了解到了幾個內置對象,并利用它們將信息注入到了模板中,現在我們來看看模板引擎的另外方面:函數和管道。
函數與管道
在我們已經了解了如何將信息加入到模板中,但是這些信息都是直接原樣的放置過去的,有時候,我們希望以一種對我們更有用的方式來轉換提供的數據。
下面讓我們從一個最佳實踐開始:將.Values對象中的字符串注入模板時,我們應該引用這些字符串,我們可以通過在 template 指令中調用 quote 函數來實現,比如:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ quote .Values.favorite.drink }} food: {{ quote .Values.favorite.food }}
模板函數遵循的語法規則是 functionName arg1 arg2...,在上面的代碼片段中,quote .Values.favorite.drink 會調用 quote 函數并傳遞一個單個參數。
Helm 有60多種可用的函數,其中一些是由 Go 模板語言本身定義的,其他大多數都是 Sprig 模板庫提供的,接下來我們會通過部分示例來逐步介紹其中的一些功能函數。
當我們談論 Helm 模板語言 的時候,就好像是特定于 Helm 一樣,但實際上它是 Go 模板語言加上一些額外的函數以及各種封裝程序的組合,以將某些對象暴露給模板。當我們需要學習模板的時候,Go 模板上有許多資源會對我們有所幫助的。
管道
模板語言有一個強大的功能就是管道(Pipeline)概念,管道利用 UNIX 的概念,將一系列模板命令鏈接在一起,一起對外提供服務,換句話說,管道是按順序完成多項工作的有效方式,我們來使用管道重寫上面的示例模板:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | quote }} food: {{ .Values.favorite.food | quote }}
在這里我們沒有調用 quote ARGUMENT 函數,而是顛倒了下順序,我們使用管道符(|)將參數發送給函數:.Values.favorite.drink | quote,使用管道,我們可以將多個功能鏈接在一起:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | quote }} food: {{ .Values.favorite.food | upper | quote }}
管道順序: 反轉順序是模板中常見的做法,我們會看到 .val | quote 比 quote .val 用法更多,雖然兩種方法都是可以的。
最后,模板渲染后,會產生如下所示的結果:
$ helm install --generate-name --dry-run --debug ./mychart install.go [debug] Original chart version: "" install.go [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/course/k8strain/content/helm/manifests/mychart NAME: mychart-1575966483 LAST DEPLOYED: Tue Dec 10 1604 2019 NAMESPACE: default STATUS: pending-install REVISION: 1 TEST SUITE: None USER-SUPPLIED VALUES: {} COMPUTED VALUES: favorite: drink: coffee food: pizza favoriteDrink: coffee HOOKS: MANIFEST: --- # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575966483-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA"
我們可以看到 values 中的 pizza 值已經被轉換成了 "PIZZA"。當這樣傳遞參數的時候,第一個求值結果(.Values.favorite.drink)會作為一個參數發送給函數,我們可以修改上面的 drink 示例,用一個帶有兩個參數的函數進行說明:repeat COUNT STRING。
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | repeat 5 | quote }} food: {{ .Values.favorite.food | upper | quote }}
repeat 函數將重復字符串給定的次數,渲染后我們可以得到如下的輸出結果:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575966939-configmap data: myvalue: "Hello World" drink: "coffeecoffeecoffeecoffeecoffee" food: "PIZZA"
default 函數
在模板中經常會使用到的一個函數是 default 函數:default DEFAULT_VALUE GIVEN_VALUE,該函數允許你在模板內部指定默認值,我們來修改上面示例中的模板:
food: {{ .Values.favorite.food | default "rice" | upper | quote }}
正常運行,我們還是可以得到 values.yaml 文件中定義的 pizza:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575966939-configmap data: myvalue: "Hello World" drink: "coffeecoffeecoffeecoffeecoffee" food: "PIZZA"
現在我們從 values.yaml 文件中移除 food 的定義:
favorite: drink: coffee # food: pizza
現在我們重新運行helm install --generate-name --dry-run --debug ./mychart將渲染成如下的 YAML 文件:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575967394-configmap data: myvalue: "Hello World" drink: "coffeecoffeecoffeecoffeecoffee" food: "RICE"
在一個真實的 chart 模板中,所有的靜態默認值都應位于 values.yaml 文件中,并且不應該重復使用 default 函數,但是,默認命令非常適合計算不能在 values.yaml 文件中聲明的 values 值,例如:
food: {{ .Values.favorite.food | default (printf "%s-rice" (include "fullname" .)) }}
不過在有些地方,if 條件語句可能比 default 函數更合適,我們會在后面了解到。
模板函數和管道是將數據轉換后然后將其插入到 YAML 文件中的一種強大方法,但是有的時候有必要添加一些模板邏輯,這些邏輯比僅僅插入字符串要復雜得多,下面我們將來了解模板語言中提供的控制流程。
運算符函數
另外需要注意的是在模板中,運算符(eq、ne、lt、gt、and、or 等等)均實現為函數,在管道中,運算符可以用括號()進行分割。
接下來我們可以去了解控制流程條件語句、循環和作用域修飾符的使用。
流程控制
控制流程為模板作者提供了控制模板生成流程的功能,Helm 的模板語言提供了以下一些流程控制:
if/else 條件語句
with 指定一個作用域范圍
range 提供類似于 for each 這樣的循環樣式
除此之外,還提供了一些聲明和使用命名模板的操作:
define 在模板內部聲明一個新的命名模板
template 導入一個命名模板
block 聲明了一種特殊的可填充模板區域。
這里我們先來了解 if、with、range 語句的使用,其他將在后面的命名模板部分介紹。
if/else
首先我們先來了解下有條件地在模板中包含一個文本區域,就是 if/else ,這個條件判斷的基本結構如下所示:
{{ if PIPELINE }} # Do something {{ else if OTHER PIPELINE }} # Do something else {{ else }} # Default case {{ end }}
可以看到我們這里判斷的是管道而不是一個 values 值,這是因為控制結構可以執行整個管道,而不僅僅是判斷值。如果值為以下的一些內容,則將管道判斷為 false:
布爾 false
數字零
一個空字符串
nil(empty 或者 null)
一個空集合(map、slice、tuple、dict、array)
在其他條件下,條件都為真。
現在我們在上面的示例模板 ConfigMap 中添加一個簡單的條件,如果 drink 設置為 coffee,我們就添加另外一個設置:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | default "tea" | quote }} food: {{ .Values.favorite.food | upper | quote }} {{ if eq .Values.favorite.drink "coffee" }}mug: true{{ end }}
我們把 values.yaml 文件內容設置成下面的樣子:
favorite: # drink: coffee food: pizza
由于我們注釋掉了 drink: coffee,所以渲染后輸出不會包含 mug: true 的標志,但是如果我們把注釋取消掉,則應該輸出如下所示的內容:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575970308-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" mug: true
這是因為上面模板中我們添加了 if eq .Values.favorite.drink "coffee" 這樣的條件判斷,相當于是判斷.Values.favorite.drink值是否等于 "coffee",如果相等則渲染 mug: true。
空格控制
還有一個非常重要的功能點就是關于空格的控制,因為空格對于 YAML 文件非常重要的,不是說任意縮進就可以,依然還是以前面的例子為例,我們來格式化下模板格式以更易于閱讀:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | default "tea" | quote }} food: {{ .Values.favorite.food | upper | quote }} {{ if eq .Values.favorite.drink "coffee" }} mug: true {{ end }}
現在我們的模板看上去更易于閱讀了,但是我們通過模板引擎來渲染下,卻會得到如下的錯誤信息:
$ helm install --generate-name --dry-run --debug ./mychart install.go:148: [debug] Original chart version: "" install.go:165: [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/course/k8strain/content/helm/manifests/mychart Error: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 9: did not find expected key
這是因為我們在模板中添加了空格,生成了不正確的 YAML 文件:
apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575970308-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" mug: true
我們可以看到 mug: true 的縮進是有問題的,不符合 YAML 文件格式,現在我們講縮進去掉試看看:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | default "tea" | quote }} food: {{ .Values.favorite.food | upper | quote }} {{ if eq .Values.favorite.drink "coffee" }} mug: true {{ end }}
重新渲染模板,然后可以發現已經可以正常通過了,但是渲染出來的 YAML 文件格式看上去還是有點奇怪:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575971172-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" mug: true
我們可以看到得到的 YAML 文件中多了一些空行,這是因為模板引擎渲染的時候它會刪除 {{ 和 }} 之間的內容,但是會完全保留其余的空格。我們知道在 YAML 文件中空格是有意義的,所以管理空格就變得非常重要了,不過 Helm 模板也提供了一些工具來幫助我們管理空格。
首先可以使用特殊字符修改模板聲明的花括號語法,以告訴模板引擎去掉空格。{{-添加了破折號和空格表示應將左邊的空格移除,-}}表示將右邊的空格移除,另外也需要注意的是,換行符也是空格。
需要注意的時候要確保-和指令的其余部分之間要有空格,{{- 3 }}表示刪除左邊的空格并打印3,但是{{-3 }}表示打印-3。
使用這個語法,我們可以修改上面的模板來移除多余的空行:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | default "tea" | quote }} food: {{ .Values.favorite.food | upper | quote }} {{- if eq .Values.favorite.drink "coffee" }} mug: true {{- end }}
渲染后可以看到空行被移除掉了:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575972373-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" mug: true
為了更加清楚地說明這個問題,我們用*來代替將要刪除的每個空格,行尾的*表示被刪除的換行符:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | default "tea" | quote }} food: {{ .Values.favorite.food | upper | quote }}* **{{- if eq .Values.favorite.drink "coffee" }} mug: true* **{{- end }}
所以我們這里用{{-表示的就是刪除本行開頭的兩個空格以及上一行的換行符,這樣是不是就將空行都刪除了啊。
在使用移除空格的時候還需要小心,比如下面的操作:
food: {{ .Values.favorite.food | upper | quote }} {{- if eq .Values.favorite.drink "coffee" -}} mug: true {{- end -}}
我們依然還是可以用*來代替空格進行分析,如下所示:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | default "tea" | quote }} food: {{ .Values.favorite.food | upper | quote }}* **{{- if eq .Values.favorite.drink "coffee" -}}* mug: true* **{{- end -}}
第一個{{-會刪除前面的空格和前面的換行符,然后后面的-}}會刪除當前行的換行符,這樣就會把 mug: true 移動到 food: "PIZZA" 后面去了,最終渲染過后就會變成:food: "PIZZA"mug: true,因為在兩側都去掉換行符。
有關模板中空格控制的詳細信息,可以查看 Go 模板官方文檔介紹。
不過有時候告訴模板系統如何縮進比起去控制模板指令的間距更加容易,所以,有時候你會發現縮進函數({{ indent 2 "mug: true" }})更有用。
使用 with 修改作用域
接下來需要了解的是 with 操作,它可以控制變量的作用域,然后重新用 . 調用就表示對當前作用域的引用,所以,.Values是告訴模板引擎在當前作用域下內查找 Values 對象。
with 語句的語法和 if 語句比較類似:
{{ with PIPELINE }} # 限制范圍 {{ end }}
范圍可以更改,可以讓你將當前范圍 . 設置為特定的對象,例如,我們一直在使用Values.favorites,讓我們重寫下模板文件 ConfigMap 來更改.的范圍指向Values.favorites:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" {{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} food: {{ .food | upper | quote }} {{- end }}
我們這里將前面練習的 if 條件語句刪除了,在模板中我們添加了一個{{- with .Values.favorite }}的語句,意思就是說在 with 語句的作用范圍內可以用.表示.Values.favorite了,所以我們可以引用.drink和.food了,但是在{{ end }}之后就會重置為之前的作用域了。
不過需要注意得是,在受限的作用域范圍內,你無法從父級范圍訪問到其他對象,比如,下面得模板會失敗:
{{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} food: {{ .food | upper | quote }} release: {{ .Release.Name }} {{- end }}
因為 Release.Name 并不在 . 的限制范圍內,所以會產生錯誤,但是,如果我們交換最后兩行,則就可以正常工作了,因為{{ end }}之后會重置作用域。
{{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} food: {{ .food | upper | quote }} {{- end }} release: {{ .Release.Name }}
下面我先來了解下 range,然后我們再去了解下模板變量,它可以為上面得這個范圍問題提供一種解決方案。
range 循環操作
我們知道許多編程語言都支持使用 for 循環、foreach 循環或者類似功能機制進行循環迭代,在 Helm 得模板語言中,迭代集合得方法是使用 range 運算符。
比如首先我們在 values.yaml 文件中添加一份 pizza 餡料列表:
favorite: drink: coffee food: pizza pizzaToppings: - mushrooms - cheese - peppers - onions
現在我們有了 pizzaToppings 列表(在模板中稱為切片),我們可以來修改下模板將列表打印到 ConfigMap 中:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" {{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} food: {{ .food | upper | quote }} {{- end }} toppings: |- {{- range .Values.pizzaToppings }} - {{ . | title | quote }} {{- end }}
我們仔細觀察下模板中的 toppings: 列表,range 函數將遍歷 Values.pizzaToppings 列表,我們看到里面使用了一個 .,類似于上面我們用 with 設置范圍一樣,運算符也是這樣的,每次循環,. 都會被設置為當前的 pizzaTopping,也就是說第一次設置為mushrooms,第二次迭代設置為cheese,依次類推。
我們可以直接傳遞 . 這個值到管道上,所以我們這里 {{ . | title | quote }} 就相當于發送 . 給 title(標題大小寫函數)函數,然后發送給 quote 函數,我們渲染這個模板,會輸出如下的內容:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575975849-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" toppings: |- - "Mushrooms" - "Cheese" - "Peppers" - "Onions"
在上面模板中,我們做了一些小小的特殊處理,toppings: |- 行表示聲明一個多行字符串,所以其實我們的 toppings 列表不是一個 YAML 列表,而是一個比較大的字符串,這是因為 ConfigMap 中的數據由 key/value 對組成,所有 key 和 value 都是簡單的字符串,要了解為什么是這樣的,可以查看 Kubernetes ConfigMap 文檔,不過這個細節對我們這里不重要。
多行字符串可以使用 | 保留換行符,也可以使用 > 折疊換行,如: this: | Foo Bar that: > Foo Bar 對應的意思就是:{this: 'Foo Bar ', that: 'Foo Bar '} + 表示保留文字塊末尾的換行,- 表示刪除字符串末尾的換行,如: s1: | Foo s2: |+ Foo s3: |- Foo 對應的意思就是:{s1: 'Foo ', s2: 'Foo ', s3: 'Foo'}
有時候,在模板中快速創建一個列表,然后遍歷該列表很有用,Helm 模板具有簡化該功能的函數:tuple。元組是固定大小的列表集合,但是具有任意數據類型,下面是元組的大概使用方法:
sizes: |- {{- range tuple "small" "medium" "large" }} - {{ . }} {{- end }}
上面的模板最終會被渲染成如下的 YAML:
sizes: |- - small - medium - large
除了列表和元組之外,range 還可以用于遍歷字典,我們在下一節介紹模板變量的時候再來了解這個用法吧。
變量
有了函數、管道、對象以及控制結構,我們可以想象下大多數編程語言中更基本的思想之一:變量。在模板中,變量的使用頻率較低,但是,我們還是可以使用他們來簡化代碼,以及更好地使用 with 和 range。
在前面的示例中,我們知道下面的模板渲染會出錯:
{{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} food: {{ .food | upper | quote }} release: {{ .Release.Name }} {{- end }}
因為 Release.Name 不在 with 語句塊限制的范圍之內,解決作用域問題的一種方法是將對象分配給在不考慮當前作用域情況下訪問的變量。
在 Helm 模板中,變量是對另外一個對象的命名引用。它遵循 $name 格式,變量使用特殊的賦值運算符進行賦值:=,我們可以修改上面的模板,為 Release.Name 聲明一個變量:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" {{- $relname := .Release.Name -}} {{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} food: {{ .food | upper | quote }} release: {{ $relname }} {{- end }}
注意在 with 語句之前,我們先分配了$relname := .Release.Name,然后在 with 語句塊中,$relname變量仍然表示 release 的名稱,我們渲染該模板,可以得到如下的正確結果:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575982655-configmap data: myvalue: "Hello World" drink: "coffee" food: "PIZZA" release: mychart-1575982655
變量在 range 循環里面非常有用,它們可以用于類似于列表的對象來捕獲索引和 value 值:
toppings: |- {{- range $index, $topping := .Values.pizzaToppings }} {{ $index }}: {{ $topping }} {{- end }}
注意 range 在前面,然后是變量,然后是賦值運算符,然后才是列表,這會將整數索引(從0開始)分配給$index,并將 value 值分配給 $topping,上面的內容會被渲染成如下內容:
toppings: |- 0: mushrooms 1: cheese 2: peppers 3: onions
對于同時具有 key 和 value 的數據結構,我們也可以使用 range 來獲得 key、value 的值,比如,我們可以像這樣循環遍歷.Values.favorite:
apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" {{- range $key, $val := .Values.favorite }} {{ $key }}: {{ $val | quote }} {{- end }}
在第一次迭代中,$key是 drink,$val是 coffee,在第二次迭代中,$key是 food,$val是 pizza。運行上面的命令將生成下面的內容:
# Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1575983119-configmap data: myvalue: "Hello World" drink: "coffee" food: "pizza"
一般來說變量不是全局的,它們的作用域是聲明它們的塊區域,之前,我們在模板的頂層分配了$relname,該變量將在整個模板的范圍內,但是在我們上面的示例中,$key和$val作用域只在{{ range... }}{{ end }}區域內。
但是,有一個始終是全局變量的$始終指向頂層根上下文,當我們在 range 循環內需要知道 chart 包的 release 名稱的時候,該功能就非常有用了,比如下面的模板文件:
{{- range .Values.tlsSecrets }} apiVersion: v1 kind: Secret metadata: name: {{ .name }} labels: # helm 模板經常使用 `.`,但是這里是無效的,用 `$` 是可以生效的。 app.kubernetes.io/name: {{ template "fullname" $ }} # 這里不能引用 `.Chart.Name`,但是可用使用 `$.Chart.Name` helm.sh/chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" app.kubernetes.io/instance: "{{ $.Release.Name }}" # 值來自于 Chart.yaml 文件中的 appVersion app.kubernetes.io/version: "{{ $.Chart.AppVersion }}" app.kubernetes.io/managed-by: "{{ $.Release.Service }}" type: kubernetes.io/tls data: tls.crt: {{ .certificate }} tls.key: {{ .key }} --- {{- end }}
到現在為止,我們只研究了在一個文件中聲明的一個模板,但是,Helm 模板語言的強大功能之一是它能夠聲明多個模板并將其一起使用。我們將在下面的章節中來討論這一點。
命令模板
前面我們都是只操作的一個模板,現在我們來嘗試使用多個模板文件。在本節中,我們可以了解到如何在一個文件中定義命名模板,然后在其他地方使用它們。命名模板(有時也叫子模板)只是在文件內部定義的有名稱的模板。主要有兩種創建方式以及幾種不同的使用方式。 當使用命名模板的時候有幾個重要細節:模板名稱是全局的,如果聲明兩個具有相同名稱的模板,則會使用最后被加載的模板。由于子 chart 中的模板是與頂級模板一起編譯的,所以需要謹慎命名。 一種流行的命名約定是在每個定義的模板前添加 chart 名稱:{{ define "mychart.labels" }},通過使用特定的 chart 名作為前綴,我們可以避免由于兩個不同的 chart 實現了相同名稱的模板而引起的沖突。 partials 和 _ 文件? 到目前為止,我們只使用了一個模板文件,但是 Helm 的模板語言允許我們創建命名的嵌入式模板,可以在其他位置進行訪問。在編寫這些模板之前,有一些值得一提的命名約定: templates/ 中的大多數文件都被視為 Kubernetes 資源清單文件(NOTES.txt 除外) 以 _ 開頭命名的文件也不會被當做 Kubernetes 資源清單文件 下劃線開頭的文件不會被當做資源清單之外,還可以被其他 chart 模板調用 _ 開頭的這些文件其實就是 Helm 中的 partials 文件,所以其實我們完全可以將命名模板定義在這些 partials 文件中,默認就是 _helpers.tpl 文件,其實在前面我們創建的 mychart 包中也可以找到這個文件。 define 和 template? define 關鍵字可以讓我們在模板文件中創建命名模板,它的語法如下所示: {{ define "MY.NAME" }} # 模板內容區域 {{ end }} 比如我們可以定義一個模板來封裝下 Kubernetes 的 labels 標簽: {{- define "mychart.labels" }} labels: generator: helm date: {{ now | htmlDate }} {{- end }} 現在我們可以將該模板嵌入到前面的 ConfigMap 模板中,然后將其包含在模板中: {{- define "mychart.labels" }} labels: generator: helm date: {{ now | htmlDate }} {{- end }} apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap {{- template "mychart.labels" }} data: myvalue: "Hello World" {{- range $key, $val := .Values.favorite }} {{ $key }}: {{ $val | quote }} {{- end }} 當模板引擎讀取這個文件的時候,它會存儲 mychart.labels 的引用,直到該模板被調用,然后會內聯渲染該模板。我們渲染這個模板可以都到如下所示的結果(記得先刪掉默認生成的 _helpers.tpl 文件): # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1576034036-configmap labels: generator: helm date: 2019-12-11 data: myvalue: "Hello World" drink: "coffee" food: "pizza" 一般來說,Helm 中約定將這些模板統一放到一個 partials 文件中,通常就是 _helpers.tpl 文件中,我們將上面的命名模板移動到該文件(templates/_helpers.tpl)中去: {{/* 生成基本的 Label 標簽 */}} {{- define "mychart.labels" }} labels: generator: helm date: {{ now | htmlDate }} {{- end }} 一般來說,我們也會用一個簡單的塊({{/*...*/}})來注釋這個命名模板的作用。 現在雖然我們把命名模板放到了 _helpers.tpl 文件中,但是我們在 configmap.yaml 模板中還是可以訪問,因為命名模板是全局的: apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap {{- template "mychart.labels" }} data: myvalue: "Hello World" {{- range $key, $val := .Values.favorite }} {{ $key }}: {{ $val | quote }} {{- end }} 因為上面我們提到過命名模板是全局的,我們可以再渲染下上面的模板可以得到正確的結果。 設置模板范圍? 上面我們定義的模板中,還沒有使用到任何對象,只使用了函數,現在我們來修改下定義的命名模板,包含 chart 的名稱和版本: {{/* 生成基本的 Label 標簽 */}} {{- define "mychart.labels" }} labels: generator: helm date: {{ now | htmlDate }} chart: {{ .Chart.Name }} version: {{ .Chart.Version }} {{- end }} 現在我們來渲染下模板,會出現下面的錯誤: $ helm install --generate-name --dry-run --debug ./my chart install.go [debug] Original chart version: "" install.go [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/cour se/k8strain/content/helm/manifests/mychart Error: unable to build kubernetes objects from release manifest: error validati ng "": error validating data: [unknown object type "nil" in ConfigMap.metadata. labels.chart, unknown object type "nil" in ConfigMap.metadata.labels.version] helm.go [debug] error validating "": error validating data: [unknown object type "nil" in ConfigMap.metadata.labels.chart, unknown object type "nil" in Co nfigMap.metadata.labels.version] ...... 我們可以看到提示 labels.chart 為 nil,這是因為我們使用的 .Chart.Name 不在定義的這個模板的作用域范圍內,當渲染命名模板(使用 define 定義)的時候,它將接收模板調用傳遞的作用域。在我們這個示例中,我們是這樣引用這個模板的: {{- template "mychart.labels" }} 沒有傳入任何作用域,所以在模板內我們無法訪問 . 中的任何內容,當然要解決很簡單,我們只需要把作用域范圍傳遞給模板即可: apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap {{- template "mychart.labels" . }} ...... 我們這里在使用 template 調用模板的時候傳遞了 .,我們可以很容易傳遞 .Values 或者 .Values.favorite 或者我們想要的任何范圍,但是這里我們想要的是頂級作用域,所以我們傳遞的是 .。 現在我們再來重新渲染我們的模板,可以得到如下所示的結果: # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1576035668-configmap labels: generator: helm date: 2019-12-11 chart: mychart version: 0.1.0 data: myvalue: "Hello World" drink: "coffee" food: "pizza" 現在 {{ .Chart.Name }} 解析為了 mychart,而 {{ .Chart.Version }} 解析為了 0.1.0。 include 函數? 假設我們定義了一個如下所示的簡單模板: {{- define "mychart.app" -}} app_name: {{ .Chart.Name }} app_version: "{{ .Chart.Version }}" {{- end -}} 現在我們想把上面的內容插入到模板的 labels 部分,在 data 部分也想要這個內容: apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap labels: {{ template "mychart.app" . }} data: myvalue: "Hello World" {{- range $key, $val := .Values.favorite }} {{ $key }}: {{ $val | quote }} {{- end }} {{ template "mychart.app" . }} 但是我們直接渲染上面的模板還是會有錯誤: $ helm install --generate-name --dry-run --debug ./my chart install.go [debug] Original chart version: "" install.go [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/cour se/k8strain/content/helm/manifests/mychart Error: unable to build kubernetes objects from release manifest: error validati ng "": error validating data: [ValidationError(ConfigMap): unknown field "app_n ame" in io.k8s.api.core.v1.ConfigMap, ValidationError(ConfigMap): unknown field "app_version" in io.k8s.api.core.v1.ConfigMap] helm.go [debug] error validating "": error validating data: [ValidationErro r(ConfigMap): unknown field "app_name" in io.k8s.api.core.v1.ConfigMap, Validat ionError(ConfigMap): unknown field "app_version" in io.k8s.api.core.v1.ConfigMap] ...... 因為 template 只是一個動作,而不是一個函數,所以無法將模板調用的輸出傳遞給其他函數,只是內聯插入,相當于渲染的結果是這樣的: apiVersion: v1 kind: ConfigMap metadata: name: measly-whippet-configmap labels: app_name: mychart app_version: "0.1.0+1478129847" data: myvalue: "Hello World" drink: "coffee" food: "pizza" app_name: mychart app_version: "0.1.0+1478129847" 很明顯上面的 YAML 文件是不符合 ConfigMap 資源對象的格式要求的,所以報錯了。為解決這個問題,Helm 提供了代替 template 的函數 include,可以將模板的內容導入到當前的管道中,這樣就可以在管道中傳遞給其他函數進行處理了,如下所示: apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap labels: {{ include "mychart.app" . | indent 4 }} data: myvalue: "Hello World" {{- range $key, $val := .Values.favorite }} {{ $key }}: {{ $val | quote }} {{- end }} {{ include "mychart.app" . | indent 2 }} 現在我們重新渲染就可以得到正確的結果了,這是因為我們用 include 函數得到模板內容后通過管道傳給了后面的 indent 函數來保證了縮進: Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1576036671-configmap labels: app_name: mychart app_version: "0.1.0" data: myvalue: "Hello World" drink: "coffee" food: "pizza" app_name: mychart app_version: "0.1.0" 建議 在 Helm 模板中最好使用 include 而不是 template,這樣可以更好地處理 YAML 文檔的輸出格式。 有時候如果我們只想導入內容而不是模板,這個時候我們可以通過下面描述的 .Files 對象來訪問文件實現。
訪問文件
在上一節中我們介紹了幾種創建和訪問命名模板的方法,這使得從另一個模板中導入一個模板變得很容易,但是有時候需要導入一個不是模板的文件并注入其內容,而不通過模板渲染器獲得內容。 Helm 提供了一個 .Files 對象對文件的訪問,但是在模板中使用這個對象之前,還有幾個需要注意的事項值得一提: 可以在 Helm chart 中添加額外的文件,這些文件也會被打包,不過需要注意,由于 Kubernetes 對象的存儲限制,Charts 必須小于 1M 由于一些安全原因,通過 .Files 對象無法訪問某些文件 無法訪問 templates/ 下面的文件 無法訪問使用 .helmignore 排除的文件 Chart 不會保留 UNIX 模式的信息,所以,當使用 .Files 對象時,文件級別的權限不會對文件的可用性產生影響。 基本示例? 現在我們來編寫一個模板,將3個文件讀入到 ConfigMap 模板中,首先我們在 chart 中添加3個文件,將3個文件都直接放置在 mychart/ 目錄中。 config1.toml: message = Hello from config 1 config2.toml: message = This is config 2 config3.toml: message = Goodbye from config 3 3個文件都是簡單的 TOML 文件,我們知道這些文件的名稱,所以我們可以使用 range 函數來遍歷它們,并將其內容注入到 ConfigMap 中去。 apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: {{- $files := .Files }} {{- range tuple "config1.toml" "config2.toml" "config3.toml" }} {{ . }}: |- {{ $files.Get . }} {{- end }} 這里我們聲明了一個 $files 的變量來保存 .Files 對象的引用,還使用了 tuple 函數來循環文件列表,然后我們打印每個文件夾 {{ . }}: |-,后面使用 {{ $files.Get . }} 獲取文件內容。 現在我們渲染這個模板會產生包含3個文件內容的單個 ConfigMap: # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1576046462-configmap data: config1.toml: |- message = Hello from config 1 config2.toml: |- message = This is config 2 config3.toml: |- message = Goodbye from config 3 另外在處理文件的時候,對文件路徑本身執行一些標準操作可能非常有用,為了解決這個問題,Helm 從 Go 的路徑包中導入了許多功能供你使用,它們都可以使用與 Go 包中相同的相同名稱來訪問,但是首字母需要小寫,比如 Base 需要變成 base,導入的函數有:- Base - Dir - Ext - IsAbs - Clean。 Glob 模式? 隨著 chart 的增長,你可能需要更多地組織文件,因此 Helm 提供了 Files.Glob 的方法來幫助我們獲取具有 glob 模式的文件。 .Glob 返回 Files 類型,所以你可以在返回的對象上調用任何 Files 方法。比如,我們的文件目錄結構如下所示: foo/: foo.txt foo.yaml bar/: bar.go bar.conf baz.yaml 我們可以用 Glob 進行多種選擇: {{ range $path := .Files.Glob "**.yaml" }} {{ $path }}: | {{ .Files.Get $path }} {{ end }} 或者 {{ range $path, $bytes := .Files.Glob "foo/*" }} {{ $path }}: '{{ b64enc $bytes }}' {{ end }} ConfigMap 和 Secrets? 想要將文件內容同時放入 ConfigMap 和 Secrets 中,以便在運行時安裝到 Pod 中,這種需求很常見,為了解決這個問題,Helm 在 Files 類型上添加了一個實用的方法。 根據上面的目錄結構,我們可以按照如下的方式進行處理: apiVersion: v1 kind: ConfigMap metadata: name: conf data: {{ (.Files.Glob "foo/*").AsConfig | indent 2 }} --- apiVersion: v1 kind: Secret metadata: name: very-secret type: Opaque data: {{ (.Files.Glob "bar/*").AsSecrets | indent 2 }} 編碼? 我們也可以導入一個文件并用 base64 編碼進行編碼: apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-secret type: Opaque data: token: |- {{ .Files.Get "config1.toml" | b64enc }} 上面將采用我們上面的 config1.toml 文件并對其內容進行 base64 編碼,渲染會得到如下所示的結果: # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: Secret metadata: name: mychart-1576048287-secret type: Opaque data: token: |- bWVzc2FnZSA9IEhlbGxvIGZyb20gY29uZmlnIDEK Lines? 有時,需要訪問模板中文件的每一行內容,Helm 也提供了方法的 Lines 方法,我們可以使用 range 函數遍歷沒行內容: data: some-file.txt: {{ range .Files.Lines "foo/bar.txt" }} {{ . }}{{ end }} 在 Helm 安裝的時候無法將文件傳遞到 chart 外部,所以,如果你要求用戶提供數據的話,則必須使用 helm install -f 或者 helm install --set 來獲取。
NOTES.txt
在本節中我們將來了解為 chart 用戶提供說明的一個 NOTES.txt 文件,在 chart 安裝或者升級結束時,Helm 可以為用戶打印出一些有用的信息,使用模板也可以自定義這些信息。 要將安裝說明添加到 chart 中,只需要創建一個 templates/NOTES.txt 文件,該文件純文本的,但是可以像模板一樣進行處理,并具有所有常規模板的功能和可用對象。 現在讓我們來創建一個簡單的 NOTES.txt 文件: Thank you for installing {{ .Chart.Name }}. Your release is named {{ .Release.Name }}. To learn more about the release, try: $ helm status {{ .Release.Name }} $ helm get {{ .Release.Name }} 現在我們運行 helm install ./mychart,我們就可以在底部看到這樣的消息: RESOURCES: ==> v1/Secret NAME TYPE DATA AGE rude-cardinal-secret Opaque 1 0s ==> v1/ConfigMap NAME DATA AGE rude-cardinal-configmap 3 0s NOTES: Thank you for installing mychart. Your release is named rude-cardinal. To learn more about the release, try: $ helm status rude-cardinal $ helm get rude-cardinal 用這種方式可以向用戶提供一個有關如何使用其新安裝的 chart 的詳細信息,強烈建議創建 NOTES.txt 文件,雖然這不是必須的。
子Chart與全局值
到現在為止,我們從單一模板,到多個模板文件,但是都僅僅是處理的一個 chart 包,但是 charts 可能具有一些依賴項,我們稱為 subcharts(子 chart),接下來我們將創建一個子 chart。 同樣在深入了解之前,我們需要了解下子 chart 相關的一些信息。 子 chart 是獨立的,這意味著子 chart 不能顯示依賴其父 chart 所以子 chart 無法訪問其父級的值 父 chart 可以覆蓋子 chart 的值 Helm 中有可以被所有 charts 訪問的全局值的概念 創建子chart? 同樣還是在之前操作的 mychart/ 這個 chart 包中,我們來嘗試添加一些新的子 chart: $ cd mychart/charts $ helm create mysubchart Creating mysubchart $ rm -rf mysubchart/templates/*.* 和前面一樣,我們刪除了所有的基本模板,這樣我們可以從頭開始。 添加 values 和 模板? 接下來我們為 mysubchart 這個子 chart 創建一個簡單的模板和 values 值文件,mychart/charts/mysubchart 中已經有一個 values.yaml 文件了,在文件中添加下面的 values: dessert: cake 下面我們再創建一個新的 ConfigMap 模板 mychart/charts/mysubchart/templates/configmap.yaml: apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-cfgmap2 data: dessert: {{ .Values.dessert }} 因為每個子 chart 都是獨立的 chart,所以我們可以單獨測試 mysubchart: helm install --generate-name --dry-run --debug mychart/charts/mysubchart install.go [debug] Original chart version: "" install.go [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/course/k8strain/content/helm/manifests/mychart/charts/mysubchart NAME: mysubchart-1576050755 LAST DEPLOYED: Wed Dec 11 1536 2019 NAMESPACE: default STATUS: pending-install REVISION: 1 TEST SUITE: None USER-SUPPLIED VALUES: {} COMPUTED VALUES: dessert: cake HOOKS: MANIFEST: --- # Source: mysubchart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mysubchart-1576050755-cfgmap2 data: dessert: cake 從父 chart 覆蓋 values? 我們原來的 chart - mychart 現在是 mysubchart 的父級 chart 了。由于 mychart 是父級,所以我們可以在 mychart 中指定配置,并將該配置發送到 mysubchart 中去,比如,我們可以這樣修改 mychart/values.yaml: favorite: drink: coffee food: pizza pizzaToppings: - mushrooms - cheese - peppers - onions mysubchart: dessert: ice cream 最后兩行,mysubchart 部分中的所有指令都回被發送到 mysubchart 子 chart 中,所以,如果我們現在渲染模板,我們可以看到 mysubchart 的 ConfigMap 會被渲染成如下的內容: # Source: mychart/charts/mysubchart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1576051914-cfgmap2 data: dessert: ice cream 我們可以看到頂層的 values 值覆蓋了子 chart 中的值。這里有一個細節需要注意,我們沒有將 mychart/charts/mysubchart/templates/configmap.yaml 模板更改為指向 .Values.mysubchart.dessert,因為從該模板的絕度來看,該值仍然位于 .Values.dessert,當模板引擎傳遞 values 值的時候,它會設置這個作用域,所以,對于 mysubchart 模板,.Values 中僅僅提供用于該子 chart 的值。 但是有時候如果我們確實希望某些值可以用于所有模板,這個時候就可以使用全局 chart values 值來完成了。 全局值? 全局值是可以從任何 chart 或子 chart 中都可以訪問的值,全局值需要顯示的聲明,不能將現有的非全局對象當作全局對象使用。 Values 數據類型具有一個名為 Values.global 的保留部分,可以在其中設置全局值,我們在 mychart/values.yaml 文件中添加一個全局值: favorite: drink: coffee food: pizza pizzaToppings: - mushrooms - cheese - peppers - onions mysubchart: dessert: ice cream global: salad: caesar 由于全局值的原因,在 mychart/templates/configmap.yaml 和 mysubchart/templates/configmap.yaml 下面都應該可以以 {{ .Values.global.salad }} 的形式來訪問這個值。 mychart/templates/configmap.yaml: apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: salad: {{ .Values.global.salad }} mysubchart/templates/configmap.yaml: apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-cfgmap2 data: dessert: {{ .Values.dessert }} salad: {{ .Values.global.salad }} 然后我們渲染這個模板,可以得到如下所示的內容: --- # Source: mychart/charts/mysubchart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1576053485-cfgmap2 data: dessert: ice cream salad: caesar --- # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mychart-1576053485-configmap data: salad: caesar 全局值對于傳遞這樣的數據比較有用。 共享模板? 父級 chart 和子 chart 可以共享模板,任何 chart 中已定義的塊都可以用于其他 chart。比如,我們可以定義一個簡單的模板,如下所示: {{- define "labels" }}from: mychart{{ end }} 前面我們提到過可以使用在模板中使用 include 和 template,但是使用 include 的一個優點是可以動態引入模板的內容: {{ include $mytemplate }}
模板調試
調試模板可能比較麻煩,因為渲染的模板會發送到 Kubernetes API server,而 API server 可能會因為格式以外的一些原因而拒絕 YAML 文件。 下面這些命令可以幫助你調試一些問題: helm lint 是驗證 chart 是否遵循最佳實踐的首選工具 helm install --dry-run --debug 或者 helm template --debug:前面我們已經使用了這個技巧,這個是讓服務器渲染模板,然后返回生成的資源清單文件的好方法,而且不會真正的去安裝這些資源 helm get manifest:這是查看服務器上安裝了哪些模板的好方法 當你的 YAML 文件無法解析的時候,但你想要查看生成的內容的時候,檢索 YAML 的一種簡單方法是注釋掉模板中的問題部分,然后重新運行 helm install --dry-run --debug: apiVersion: v2 # some: problem section # {{ .Values.foo | quote }} 上面的內容將呈現并返回完整的注釋: apiVersion: v2 # some: problem section # "bar" 這提供了一種查看生成的內容的快速方法。
Chart Hooks
Helm 也提供了一種 Hook 機制,可以允許 chart 開發人員在 release 生命周期的某些時間點進行干預。比如,可以使用 hook 來進行下面的操作: 在加載任何 charts 之前,在安裝的時候加載 ConfigMap 或者 Secret 在安裝新的 chart 之前,執行一個 Job 來備份數據庫,然后在升級后執行第二個 Job 還原數據 在刪除 release 之前運行一個 JOb,以在刪除 release 之前適當地取消相關服務 Hooks 的工作方式類似于普通的模板,但是他們具有特殊的注解,這些注解使 Helm 可以用不同的方式來使用他們。 Hooks? 在 Helm 中定義了如下一些可供我們使用的 Hooks: 預安裝pre-install:在模板渲染后,kubernetes 創建任何資源之前執行 安裝后post-install:在所有 kubernetes 資源安裝到集群后執行 預刪除pre-delete:在從 kubernetes 刪除任何資源之前執行刪除請求 刪除后post-delete:刪除所有 release 的資源后執行 升級前pre-upgrade:在模板渲染后,但在任何資源升級之前執行 升級后post-upgrade:在所有資源升級后執行 預回滾pre-rollback:在模板渲染后,在任何資源回滾之前執行 回滾后post-rollback:在修改所有資源后執行回滾請求 測試test:在調用 Helm test 子命令的時候執行(可以查看測試文檔) 生命周期? Hooks 允許開發人員在 release 的生命周期中的一些關鍵節點執行一些鉤子函數,我們正常安裝一個 chart 包的時候的生命周期如下所示: 用戶運行 helm install foo Helm 庫文件調用安裝 API 經過一些驗證,Helm 庫渲染 foo 模板 Helm 庫將產生的資源加載到 kubernetes 中去 Helm 庫將 release 對象和其他數據返回給客戶端 Helm 客戶端退出 如果開發人員在 install 的生命周期中定義了兩個 hook:pre-install和post-install,那么我們安裝一個 chart 包的生命周期就會多一些步驟了: 用戶運行helm install foo Helm 庫文件調用安裝 API 在 crds/ 目錄下面的 CRDs 被安裝 經過一些驗證,Helm 庫渲染 foo 模板 Helm 庫將 hook 資源加載到 kubernetes 中,準備執行pre-install hooks Helm 庫會根據權重對 hooks 進行排序(默認分配權重0,權重相同的 hook 按升序排序) Helm 庫然后加載最低權重的 hook Helm 庫會等待,直到 hook 準備就緒 Helm 庫將產生的資源加載到 kubernetes 中,注意如果添加了 --wait 參數,Helm 庫會等待所有資源都準備好,在這之前不會運行 post-install hook Helm 庫執行 post-install hook(加載 hook 資源) Helm 庫等待,直到 hook 準備就緒 Helm 庫將 release 對象和其他數據返回給客戶端 Helm 客戶端退出 等待 hook 準備就緒,這是一個阻塞的操作,如果 hook 中聲明的是一個 Job 資源,Helm 將等待 Job 成功完成,如果失敗,則發布失敗,在這個期間,Helm 客戶端是處于暫停狀態的。 對于所有其他類型,只要 kubernetes 將資源標記為加載(添加或更新),資源就被視為就緒狀態,當一個 hook 聲明了很多資源是,這些資源是被串行執行的。 另外需要注意的是 hook 創建的資源不會作為 release 的一部分進行跟蹤和管理,一旦 Helm 驗證了 hook 已經達到了就緒狀態,它就不會去管它了。 所以,如果我們在 hook 中創建了資源,那么不能依賴 helm uninstall 去刪除資源,因為 hook 創建的資源已經不受控制了,要銷毀這些資源,你需要將 helm.sh/hook-delete-policy 這個 annotation 添加到 hook 模板文件中,或者設置 Job 資源的生存(TTL)字段。 編寫 Hook? Hooks 就是 Kubernetes 資源清單文件,在元數據部分帶有一些特殊的注解,因為他們是模板文件,所以你可以使用普通模板所有的功能,包括讀取 .Values、.Release 和 .Template。 例如,在 templates/post-install-job.yaml 文件中聲明一個 post-install 的 hook: apiVersion: batch/v1 kind: Job metadata: name: "{{ .Release.Name }}" labels: app.kubernetes.io/managed-by: {{ .Release.Service | quote }} app.kubernetes.io/instance: {{ .Release.Name | quote }} app.kubernetes.io/version: {{ .Chart.AppVersion }} helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" annotations: # 因為添加了這個 hook,所以我們這個資源被定義為了 hook # 如果沒有這行,則當前這個 Job 會被當成 release 的一部分內容。 "helm.sh/hook": post-install "helm.sh/hook-weight": "-5" "helm.sh/hook-delete-policy": hook-succeeded spec: template: metadata: name: "{{ .Release.Name }}" labels: app.kubernetes.io/managed-by: {{ .Release.Service | quote }} app.kubernetes.io/instance: {{ .Release.Name | quote }} helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" spec: restartPolicy: Never containers: - name: post-install-job image: "alpine:3.3" command: ["/bin/sleep","{{ default "10" .Values.sleepyTime }}"] 當前這個模板成為 hook 的原因就是添加這個注解: annotations: "helm.sh/hook": post-install 一種資源也可以實現多個 hooks: annotations: "helm.sh/hook": post-install,post-upgrade 類似的,實現給定 hook 的資源數量也沒有限制,比如可以將 secret 和一個 configmap 都聲明為 pre-install hook。 當子 chart 聲明 hooks 的時候,也會對其進行調用,頂層的 chart 無法禁用子 chart 所聲明的 hooks。可以為 hooks 定義權重,這將有助于確定 hooks 的執行順序: annotations: "helm.sh/hook-weight": "5" hook 權重可以是正數也可以是負數,但是必須用字符串表示,當 Helm 開始執行特定種類的 hooks 的時候,它將以升序的方式對這些 hooks 進行排序。 Hook 刪除策略? 我們還可以定義確定何時刪除相應 hook 資源的策略,hook 刪除策略可以使用下面的注解進行定義: annotations: "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 我們也可以選擇一個或多個已定義的注解: before-hook-creation:運行一個新的 hook 之前刪除前面的資源(默認) hook-succeeded:hook 成功執行后刪除資源 hook-failed:hook 如果執行失敗則刪除資源 如果未指定任何 hook 刪除策略注解,則默認情況下會使用 before-hook-creation 策略。
示例
前面介紹了 Helm 的基本使用,以及 Helm Chart 包開發相關的一些知識點,下面我們用一個實例來演示下如何開發一個真正的 Helm Chart 包。 應用? 我們這里為前面的完整示例 Wordpress 開發一個 Chart 包,在開發 Chart 包之前很明顯我們最需要的就是要知道我們自己的應用應該如何使用,如何部署,不然是不可能編寫出對應的 Chart 包的。 我們可以用 helm create 命令來創建一個 Chart 包,這里我們就完全手動來創建,首先創建一個名為 wordpress 的文件夾: $ mkdir wordpress && cd wordpress 然后在目錄下面創建如下所示的 Chart.yaml 文件: apiVersion: v2 name: wordpress description: A Helm chart for Kubernetes home: https://wordpress.org/ type: application # chart 版本號 version: 0.1.0 # wordpress 的應用版本 appVersion: 5.3.2 由于 Wordpress 應用本身是一來 MySQL 數據庫的,所以同樣需要添加一個依賴說明 requirements.yaml: dependencies: - name: mysql version: 1.6.2 repository: http://mirror.azure.cn/kubernetes/charts/ condition: mysql.enabled tags: - wordpress-database 這里依賴的應用我們可以通過 helm search 命令來獲取,當然也可以隨意指定一個 Chart 版本: $ helm search repo stable/mysql NAME CHART VERSION APP VERSION DESCRIPTION stable/mysql 1.6.2 5.7.28 Fast, reliable, scalable, and easy to use open-... 需要注意的是在依賴的文件中我們添加了一個 condition: mysql.enabled 條件,這意味著當 Values 值 mysql.enabled 為 true 的時候才會真正去依賴這個子 Chart,因為我們完全可以直接使用一個外部的數據庫。所以首先我們要先添加一個 mysql.enabled 的 Values 值,添加如下所示的 values.yaml 文件: ## ## MySQL Chart 配置,參考如下 values.yaml ## https://github.com/helm/charts/blob/master/stable/mysql/values.yaml ## mysql: ## 是否部署mysql服務來滿足wordpress的需求。如果要使用外部數據庫,將 enabled 設置為 false 并配置 externalDatabase 參數。 enabled: true ## todo,其他 mysql 配置 ## 當 mysql.enabled=false 的時候使用外部數據庫 externalDatabase: ## 數據庫地址 host: localhost:3306 ## Wordpress 數據庫的非root用戶 user: wordpress ## 數據庫密碼 password: wordpress ## 數據庫名稱 database: wordpress 上面的 Values 配置很好理解,就是如果 mysql.enabled=true 則我們就用 mysql 這個子 Chart 去安裝一個 MySQL 數據庫,如果為 false 則使用下面 externalDatabase 的外部數據庫信息來作為 wordpress 的數據庫配置,雖然現在這些配置還完全沒有任何作用,但是這些卻是一開始就很容易考慮到的事情。 接下來創建一個 templates 和 charts 目錄,并在 charts 目錄下面獲取 mysql 這個子 chart: $ mkdir templates && mkdir charts $ cd charts && helm fetch stable/mysql && cd .. 現在的整個 Chart 包目錄結構如下所示: $ tree . . ├── Chart.yaml ├── charts │ └── mysql-1.6.2.tgz ├── requirements.yaml ├── templates └── values.yaml 2 directories, 4 files 接下來就是我們的重頭戲模板的開發了。我們可以將前面課程示例中的 Wordpress 資源清單直接拷貝到 templates 目錄下面,將 Deployment 和 Service 分別放在不同的 YAML 文件中,同樣還有數據持久化的資源清單,但是這里我們就需要先將 MySQL 的部分移除掉,因為我們會通過外部數據庫或者子 Chart 來渲染,不需要在這里顯示的聲明了,另外還需要將所有資源對象的命名空間去掉,因為我們可以通過 helm install 命令直接指定即可,現在我們的模板目錄結構就如下所示了: $ tree templates templates ├── deployment.yaml ├── pvc.yaml └── service.yaml 0 directories, 4 files 名稱? 這個時候我們可以嘗試去安裝調試下這個 Chart 模板: $ helm install --generate-name --dry-run --debug . install.go [debug] Original chart version: "" install.go [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/course/k8strain/content/helm/manifests/wordpress load.go Warning: Dependencies are handled in Chart.yaml since apiVersion "v2". We recommend migrating dependencies to Chart.yaml. Error: rendered manifests contain a resource that already exists. Unable to continue with install: existing resource conflict: kind: Middleware, namespace: default, name: redirect-https helm.go [debug] existing resource conflict: kind: Middleware, namespace: default, name: redirect-https rendered manifests contain a resource that already exists. Unable to continue with install helm.sh/helm/v3/pkg/action.(*Install).Run /home/circleci/helm.sh/helm/pkg/action/install.go:242 main.runInstall /home/circleci/helm.sh/helm/cmd/helm/install.go:209 main.newInstallCmd.func1 /home/circleci/helm.sh/helm/cmd/helm/install.go:115 github.com/spf13/cobra.(*Command).execute /go/pkg/mod/github.com/spf13/[email protected]/command.go:826 github.com/spf13/cobra.(*Command).ExecuteC /go/pkg/mod/github.com/spf13/[email protected]/command.go:914 github.com/spf13/cobra.(*Command).Execute /go/pkg/mod/github.com/spf13/[email protected]/command.go:864 main.main /home/circleci/helm.sh/helm/cmd/helm/helm.go:75 runtime.main /usr/local/go/src/runtime/proc.go:203 runtime.goexit /usr/local/go/src/runtime/asm_amd64.s:1357 我們看到出現了錯誤,其中還有這樣的一條警告信息 load.go Warning: Dependencies are handled in Chart.yaml since apiVersion "v2". We recommend migrating dependencies to Chart.yaml.,大概意思就是在 Helm3 中依賴項的配置被移動到了 Chart.yaml 文件中,也就是在 Helm2 版本的時候依賴才會在 requirements.yaml 中聲明,這里我們可以將 requirements.yaml 文件中的內容全部拷貝到 Chart.yaml 文件中去,然后刪除 requirements.yaml 文件,這個時候重新 DEBUG 下就沒有這個提示信息了。 下面的錯誤提示基本上都是類似于 rendered manifests contain a resource that already exists. 這樣的信息,意思就是這些資源對象在現有集群中已經包含了,這很正常,因為我們的資源對象名稱都是寫死的,是非常有可能和現有的集群沖突的,所以接下來我們要給這些資源清單改名,如何才能避免沖突又具有很好的擴展性呢?這里我們創建一個如下所示的命名模板: {{/* 創建一個默認的應用名稱,截取63個字符是因為 Kubernetes 的 name 屬性的限制(DNS 命名規范)。 */}} {{- define "wordpress.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 -}} 根據規范命名模板的名稱最好以 Chart 名作為前綴,這里我們命名成 wordpress.fullname,首先就是判斷是否定義了 fullnameOverride 這個 Values 值,如果定義了則截取 63 個字符并移除 - 這樣的前后綴作為名稱,如果沒有定義呢?就要檢查 Chart 名是否包含 Release 名稱,如果包含則截取 Release 名的 63 個字符并移除 - 這樣的前后綴作為名稱,如果不包含,則將 Release 名稱和 Chart 名稱用 - 連接起來作為名稱,這個命名的方式也是符合大部分 Chart 模板中的命名,以后的模板開發中都可以直接使用。在 templates 目錄下面新建一個 _helpers.tpl 這樣的 partials 文件,然后將上面定義的命名模板放置到里面去。后面我們所有的命名模板都將在該文件中完成。 接下來將 templates 目錄下面的所有資源對象 name 屬性全都換成上面我們定義的命名模板,由于這里并沒有什么空格控制之類的,我們直接使用 template 函數即可,同樣作為慣例,也習慣將 Release 和 Chart 名稱之類作為 Label 標簽,所以最終,這些資源清單的 Meta 信息如下所示,如果有其他額外的信息當然可以隨意添加: metadata: name: {{ template "wordpress.fullname" . }} labels: app: "{{ template "wordpress.fullname" . }}" chart: "{{ template "wordpress.chart" . }}" release: {{ .Release.Name | quote }} heritage: {{ .Release.Service | quote }} 然后當然也需要將 Deployment 的 matchLabels 和 template 下面的 Label 標簽進行修改,以及 Service 的 selector 匹配的標簽。 同樣還有 PVC 對象,以前我們直接使用的一個確定名稱的 PVC 對象: volumes: - name: wordpress-data persistentVolumeClaim: claimName: wordpress-pvc 但是現在我們這里作為模板就要考慮到各種情況了,很有可能使用我們模板的用戶根本就不需要持久化,也有可能直接傳遞一個存在的 PVC 對象進來使用,所以這里做了如下改變: volumes: - name: wordpress-data {{- if .Values.persistence.enabled }} persistentVolumeClaim: claimName: {{ .Values.persistence.existingClaim | default (include "wordpress.fullname" .) }} {{- else }} emptyDir: {} {{ end }} 當我們定義了 Values 值 persistence.enabled=true 為 true 時候就表示要使用持久化了,所以要指定下面的 claimName 屬性,但是還需要判斷 persistence.existingClaim 這個 Values 值是否存在,如果存在則表示直接使用,如果不存在則使用我們模板里面渲染的 PVC 對象,如果不需要持久化,則直接使用 emptyDir:{} 即可。最后我們的 PVC 對象模板變成了如下所示: {{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} kind: PersistentVolumeClaim apiVersion: v1 metadata: name: {{ template "wordpress.fullname" . }} labels: app: "{{ template "wordpress.fullname" . }}" chart: "{{ template "wordpress.chart" . }}" release: {{ .Release.Name | quote }} heritage: {{ .Release.Service | quote }} spec: {{- if .Values.persistence.storageClass }} storageClassName: {{ .Values.persistence.storageClass | quote }} {{- end }} accessModes: - {{ .Values.persistence.accessMode | quote }} resources: requests: storage: {{ .Values.persistence.size | quote }} {{- end -}} 其中訪問模式、存儲容量、StorageClass、存在的 PVC 都通過 Values 來指定,增加了靈活性。對應的 values.yaml 配置部分我們可以給一個默認的配置: ## 是否使用 PVC 開啟數據持久化 persistence: enabled: true ## 是否使用 storageClass,如果不適用則補配置 # storageClass: "xxx" ## ## 如果想使用一個存在的 PVC 對象,則直接傳遞給下面的 existingClaim 變量 # existingClaim: your-claim accessMode: ReadWriteMany # 訪問模式 size: 2Gi # 存儲容量 現在我們去重新做一次 DEBUG,可以看到正常了,但是還遠遠不夠,接下來我們就來定制其他部分。 定制? 比如副本數我們可以通過 Values 來指定,變成 replicas: {{ .Values.replicaCount }}。 更新策略也可以,因為更新策略并不是一層不變的,這里和之前不太一樣,我們需要用到一個新的函數 toYaml: {{- if .Values.updateStrategy }} strategy: {{ toYaml .Values.updateStrategy | nindent 4 }} {{- end }} 意思就是我們將 updateStrategy 這個 Values 值轉換成 YAML 格式,并保留4個空格。然后添加其他的配置,比如是否需要添加 nodeSelector、容忍、親和性這些,這里我們都是使用 toYaml 函數來控制空格,如下所示: {{- if .Values.nodeSelector }} nodeSelector: {{ toYaml .Values.nodeSelector | indent 8 }} {{- end -}} {{- with .Values.affinity }} affinity: {{ toYaml . | indent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{ toYaml . | indent 8 }} {{- end }} 接下來當然就是鏡像的配置了,如果是私有倉庫還需要指定 imagePullSecrets: {{- if .Values.image.pullSecrets }} imagePullSecrets: {{- range .Values.image.pullSecrets }} - name: {{ . }} {{- end }} {{- end }} containers: - name: wordpress image: {{ printf "%s:%s" .Values.image.name .Values.image.tag }} imagePullPolicy: {{ .Values.image.pullPolicy | quote }} ports: - containerPort: 80 name: web 對應的 Values 值如下所示: image: name: wordpress tag: 5.3.2-apache ## 指定 imagePullPolicy 默認為 Always pullPolicy: IfNotPresent ## 如果是私有倉庫,需要指定 imagePullSecrets # pullSecrets: # - myRegistryKeySecretName 然后就是最重要的環境變量配置部分了,因為涉及到數據庫的配置,同樣最核心的三個環境變量配置 WORDPRESS_DB_HOST、WORDPRESS_DB_USER、WORDPRESS_DB_PASSWORD,由于可能使用我們模板的用戶可能使用外部數據庫,也有可能使用我們依賴的 mysql 這個子 Chart,所以我們需要分別判斷來進行渲染: env: - name: WORDPRESS_DB_HOST {{- if .Values.mysql.enabled }} value: {{ printf "%s:%d" (include "mysql.fullname" .) (int64 .Values.mysql.service.port) }} {{- else }} value: {{ .Values.externalDatabase.host | quote }} {{- end }} - name: WORDPRESS_DATABASE_NAME {{- if .Values.mysql.enabled }} value: {{ .Values.mysql.mysqlDatabase | quote }} {{- else }} value: {{ .Values.externalDatabase.database | quote }} {{- end }} - name: WORDPRESS_DB_USER {{- if .Values.mysql.enabled }} value: {{ .Values.mysql.mysqlUser | quote }} {{- else }} value: {{ .Values.externalDatabase.user | quote }} {{- end }} - name: WORDPRESS_DB_PASSWORD valueFrom: secretKeyRef: {{- if .Values.mysql.enabled }} name: {{ template "mysql.fullname" . }} key: mysql-password {{- else }} name: {{ printf "%s-%s" .Release.Name "externaldb" }} key: db-password {{- end }} 每一個環境變量首先都是判斷 mysql.enabled 是否為 true,才表示使用子 Chart 來渲染,而對應的值都是子 Chart 中渲染過后的值,所以也需要我們去了解子 Chart 的渲染行為,如果使用外部的數據庫就要簡單很多,因為只需要讀取 Values 值即可。另外一個值得注意的是如果是配置的密碼,我們還需要去創建一個 Secret 資源對象來引用。所以我們在 templates 目錄下面創建了一個 externaldb-secrets.yaml 的資源文件,里面配置的密碼通過 b64enc 函數轉換為 Base64 編碼格式。 {{- if not .Values.mysql.enabled }} apiVersion: v1 kind: Secret metadata: name: {{ printf "%s-%s" .Release.Name "externaldb" }} labels: app: {{ printf "%s-%s" .Release.Name "externaldb" }} chart: "{{ template "wordpress.chart" . }}" release: {{ .Release.Name | quote }} heritage: {{ .Release.Service | quote }} type: Opaque data: db-password: {{ .Values.externalDatabase.password | b64enc | quote }} {{- end }} 然后就是 resource 資源聲明,這里我們定義一個默認的 resources 值,同樣用 toYaml 函數來控制空格: resources: {{ toYaml .Values.resources | indent 10 }} 最后是健康檢查部分,雖然我們之前沒有做 livenessProbe,但是我們開發 Chart 模板的時候就要盡可能考慮周全一點,這里我們加上存活性和可讀性兩個探針,并且根據 livenessProbe.enabled 和 readinessProbe.enabled 兩個 Values 值來判斷是否需要添加探針,探針對應的參數也都通過 Values 值來配置: {{- if .Values.livenessProbe.enabled }} livenessProbe: initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} periodSeconds: {{ .Values.livenessProbe.periodSeconds }} timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} successThreshold: {{ .Values.livenessProbe.successThreshold }} failureThreshold: {{ .Values.livenessProbe.failureThreshold }} httpGet: path: /wp-login.php port: 80 {{- end }} {{- if .Values.readinessProbe.enabled }} readinessProbe: initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} periodSeconds: {{ .Values.readinessProbe.periodSeconds }} timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} successThreshold: {{ .Values.readinessProbe.successThreshold }} failureThreshold: {{ .Values.readinessProbe.failureThreshold }} httpGet: path: /wp-login.php port: 80 {{- end }} 這樣我們的 Deployment 的模板就基本上完成了,我們可以通過 DEBUG 模式來模擬渲染: $ helm install --generate-name --dry-run --debug . 可以根據結果來判斷是否符合我們的需求。 最后我們還需要來對 Service 對象做模板化,因為前面我們是默認的 NodePort 類型,我們需要通過 Values 來定制: apiVersion: v1 kind: Service metadata: name: {{ template "wordpress.fullname" . }} labels: app: "{{ template "wordpress.fullname" . }}" chart: "{{ template "wordpress.chart" . }}" release: {{ .Release.Name | quote }} heritage: {{ .Release.Service | quote }} spec: selector: app: "{{ template "wordpress.fullname" . }}" type: {{ .Values.service.type }} {{- if (or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort")) }} externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy | quote }} {{- end }} ports: - name: web port: {{ .Values.service.port }} targetPort: web {{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort)))}} nodePort: {{ .Values.service.nodePort }} {{- end }} 這樣我們就可以通過配置 Values 值來配置 Service 對象了。最后就是 Ingress/IngressRoute 對象了,大家可以自己嘗試講這部分補齊。 最后在 templates 目錄下面加上 NOTES.txt 文件來說明如何使用我們的 Chart 包就可以了: Get the WordPress Manifests Objects: $ kubectl get all -l app={{ .Release.Name }} 最后我們來真正的使用我們的 Chart 包安裝一次來測試下: $ helm install mychart . --set service.type=NodePort NAME: mychart LAST DEPLOYED: Sun Mar 8 1704 2020 NAMESPACE: default STATUS: deployed REVISION: 1 NOTES: Get the WordPress Manifests Objects: $ kubectl get all -l app=mychart $ helm ls NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION mychart default 1 2020-03-08 1704.826185 +0800 CST deployed wordpress-0.1.0 5.3.2 安裝完成后可以查看我們的資源對象: $ kubectl get all -l app=mychart-wordpress NAME READY STATUS RESTARTS AGE pod/mychart-wordpress-5f65786d89-2m45s 1/1 Running 0 70s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/mychart-wordpress NodePort 10.99.239.2980:30427/TCP 70s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/mychart-wordpress 1/1 1 1 70s NAME DESIRED CURRENT READY AGE replicaset.apps/mychart-wordpress-5f65786d89 1 1 1 70s 這個時候我們通過上面的 NodePort 就可以去訪問到我們的應用了,當然還有很多配置我們都是可以直接通過 Values 去進行定制的。
鏈接:https://www.cnblogs.com/hahaha111122222/p/16277623.html
-
集群
+關注
關注
0文章
101瀏覽量
17367 -
管理工具
+關注
關注
0文章
25瀏覽量
7791 -
kubernetes
+關注
關注
0文章
239瀏覽量
8969
原文標題:示例
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄






使用Helm 在容器服務k8s集群一鍵部署wordpress
Python之包管理工具快速入門

評論