Helm Charts 開發(fā)完整示例
Helm 的使用是比較簡(jiǎn)單的,但是要讓我們自己開發(fā)一個(gè) Chart 包還是有不小難度的,主要還是 go template 的語(yǔ)法規(guī)則不夠人性化,這里我們用一個(gè)完整的實(shí)例來(lái)演示下如何開發(fā)一個(gè) Helm Chart 包。
應(yīng)用
我們這里以 Ghost 博客應(yīng)用為例來(lái)演示如何開發(fā)一個(gè)完整的 Helm Chart 包,Ghost 是基于 Node.js 的開源博客平臺(tái)。在開發(fā) Helm Chart 包之前我們最需要做的的就是要知道我們自己的應(yīng)用應(yīng)該如何使用、如何部署,不然是不可能編寫出對(duì)應(yīng)的 Chart 包的。
啟動(dòng) Ghost 最簡(jiǎn)單的方式是直接使用鏡像啟動(dòng):
??docker?run?-d?--name?my-ghost?-p?2368:2368?ghost
然后我們就可以通過(guò) http://localhost:2368 訪問(wèn) Ghost 博客了。如果我們想要在 Kubernetes 集群中部署兩個(gè)副本的 Ghost,可以直接應(yīng)用下面的資源清單文件即可:
#?ghost/deployment.yaml
apiVersion:?apps/v1
kind:?Deployment
metadata:
??name:?ghost
spec:
??selector:
????matchLabels:
??????app:?ghost-app
??replicas:?2
??template:
????metadata:
??????labels:
????????app:?ghost-app
????spec:
??????containers:
????????-?name:?ghost-app
??????????image:?ghost
??????????ports:
????????????-?containerPort:?2368
---
#?ghost/service.yaml
apiVersion:?v1
kind:?Service
metadata:
??name:?ghost
spec:
??type:?NodePort
??selector:
????app:?ghost-app
??ports:
????-?protocol:?TCP
??????port:?80
??????targetPort:?2368
直接通過(guò) kubectl 應(yīng)用上面的資源對(duì)象即可:
??kubectl?apply?-f?ghost/deployment.yaml?ghost/service.yaml
deployment.apps/ghost?created
service/ghost?created
??kubectl?get?pod?-l?app=ghost-app
NAME????????????????????READY???STATUS????RESTARTS???AGE
ghost-dfd958cc9-4s9b9???1/1?????Running???0??????????2m54s
ghost-dfd958cc9-84kmv???1/1?????Running???0??????????2m54s
??kubectl?get?svc?ghost
NAME????TYPE???????CLUSTER-IP??????EXTERNAL-IP???PORT(S)????????AGE
ghost???NodePort???10.97.227.160???????????80:31950/TCP???3m33s
這樣我們就可以通過(guò) http:// 訪問(wèn)到 Ghost 了:

看上去要部署 Ghost 是非常簡(jiǎn)單的,但是如果我們需要針對(duì)不同的環(huán)境進(jìn)行不同的設(shè)置呢?比如我們想將它部署到不同環(huán)境(staging、prod)中去,是不是我們需要一遍又一遍地復(fù)制我們的 Kubernetes 資源清單文件,這還只是一個(gè)場(chǎng)景,還有很多場(chǎng)景可能需要我們?nèi)ゲ渴饝?yīng)用,這種方式維護(hù)起來(lái)是非常困難的,這個(gè)時(shí)候就可以理由 Helm 來(lái)解放我們了。
基礎(chǔ)模板
現(xiàn)在我們開始創(chuàng)建一個(gè)新的 Helm Chart 包。直接使用 helm create 命令即可:
??helm?create?my-ghost
Creating?my-ghost
??tree?my-ghost
my-ghost
├──?Chart.yaml
├──?charts
├──?templates
│???├──?NOTES.txt
│???├──?_helpers.tpl
│???├──?deployment.yaml
│???├──?hpa.yaml
│???├──?ingress.yaml
│???├──?service.yaml
│???├──?serviceaccount.yaml
│???└──?tests
│???????└──?test-connection.yaml
└──?values.yaml
3?directories,?10?files
該命令會(huì)創(chuàng)建一個(gè)默認(rèn) Helm Chart 包的腳手架,可以刪掉下面的這些使用不到的文件:
templates/tests/test-connection.yaml
templates/serviceaccount.yaml
templates/ingress.yaml
templates/hpa.yaml
templates/NOTES.txt
然后修改 templates/deployment.yaml 模板文件:
#?templates/deployment.yaml
apiVersion:?apps/v1
kind:?Deployment
metadata:
??name:?ghost
spec:
??selector:
????matchLabels:
??????app:?ghost-app
??replicas:?{{?.Values.replicaCount?}}
??template:
????metadata:
??????labels:
????????app:?ghost-app
????spec:
??????containers:
????????-?name:?ghost-app
??????????image:?{{?.Values.image?}}
??????????ports:
????????????-?containerPort:?2368
??????????env:
????????????-?name:?NODE_ENV
??????????????value:?{{?.Values.node_env?|?default?"production"?}}
????????????{{-?if?.Values.url?}}
????????????-?name:?url
??????????????value:?http://{{?.Values.url?}}
????????????{{-?end?}}
這和我們前面的資源清單文件非常類似,只是將 replicas 的值使用 {{ .Values.replicaCount }} 模板來(lái)進(jìn)行替換了,表示會(huì)用 replicaCount 這個(gè) Values 值進(jìn)行渲染,然后還可以通過(guò)設(shè)置環(huán)境變量來(lái)配置 Ghost,同樣修改 templates/service.yaml 模板文件的內(nèi)容:
#?templates/service.yaml
apiVersion:?v1
kind:?Service
metadata:
??name:?ghost
spec:
??selector:
????app:?ghost-app
??type:?{{?.Values.service.type?}}
??ports:
????-?protocol:?TCP
??????targetPort:?2368
??????port:?{{?.Values.service.port?}}
??????{{-?if?(and?(or?(eq?.Values.service.type?"NodePort")?(eq?.Values.service.type?"LoadBalancer"))?(not?(empty?.Values.service.nodePort)))?}}
??????nodePort:?{{?.Values.service.nodePort?}}
??????{{-?else?if?eq?.Values.service.type?"ClusterIP"?}}
??????nodePort:?null
??????{{-?end?}}
同樣為了能夠兼容多個(gè)場(chǎng)景,這里我們?cè)试S用戶來(lái)定制 Service 的 type,如果是 NodePort 類型則還可以配置 nodePort 的值,不過(guò)需要注意這里的判斷,因?yàn)橛锌赡芗词古渲脼?NodePort 類型,用戶也可能不會(huì)主動(dòng)提供 nodePort,所以這里我們?cè)谀0逯凶隽艘粋€(gè)條件判斷:
{{-?if?(and?(or?(eq?.Values.service.type?"NodePort")?(eq?.Values.service.type?"LoadBalancer"))?(not?(empty?.Values.service.nodePort)))?}}
需要 service.type 為 NodePort 或者 LoadBalancer 并且 service.nodePort 不為空的情況下才會(huì)渲染 nodePort。
然后最重要的就是要在 values.yaml 文件中提供默認(rèn)的 Values 值,如下所示是我們提供的默認(rèn)的 Values 值:
#?values.yaml
replicaCount:?1
image:?ghost
node_env:?production
url:?ghost.k8s.local
service:
??type:?NodePort
??port:?80
然后我們可以使用 helm template 命令來(lái)渲染我們的模板輸出結(jié)果:
??helm?template?--debug?my-ghost
install.go:178:?[debug]?Original?chart?version:?""
install.go:195:?[debug]?CHART?PATH:?/Users/ych/devs/workspace/yidianzhishi/course/k8strain3/content/helm/manifests/my-ghost
---
#?Source:?my-ghost/templates/service.yaml
apiVersion:?v1
kind:?Service
metadata:
??name:?ghost
spec:
??selector:
????app:?ghost-app
??type:?NodePort
??ports:
????-?protocol:?TCP
??????targetPort:?2368
??????port:?80
---
#?Source:?my-ghost/templates/deployment.yaml
apiVersion:?apps/v1
kind:?Deployment
metadata:
??name:?ghost
spec:
??selector:
????matchLabels:
??????app:?ghost-app
??replicas:?1
??template:
????metadata:
??????labels:
????????app:?ghost-app
????spec:
??????containers:
????????-?name:?ghost-app
??????????image:?ghost
??????????ports:
????????????-?containerPort:?2368
??????????env:
????????????-?name:?NODE_ENV
??????????????value:?production
????????????-?name:?url
??????????????value:?http://ghost.k8s.local
上面的渲染結(jié)果和我們上面的資源清單文件基本上一致了,只是我們現(xiàn)在的靈活性更大了,比如可以控制環(huán)境變量、服務(wù)的暴露方式等等。
命名模板
雖然現(xiàn)在我們可以使用 Helm Charts 模板來(lái)渲染安裝 Ghost 了,但是上面我們的模板還有很多改進(jìn)的地方,比如資源對(duì)象的名稱我們是固定的,這樣我們就沒辦法在同一個(gè)命名空間下面安裝多個(gè)應(yīng)用了,所以一般我們也會(huì)根據(jù) Chart 名稱或者 Release 名稱來(lái)替換資源對(duì)象的名稱。
前面默認(rèn)創(chuàng)建的模板中包含一個(gè) _helpers.tpl 的文件,該文件中包含一些和名稱、標(biāo)簽相關(guān)的命名模板,我們可以直接使用即可,下面是默認(rèn)生成的已有的命名模板:
{{/*
Expand?the?name?of?the?chart.
*/}}
{{-?define?"my-ghost.name"?-}}
{{-?default?.Chart.Name?.Values.nameOverride?|?trunc?63?|?trimSuffix?"-"?}}
{{-?end?}}
{{/*
Create?a?default?fully?qualified?app?name.
We?truncate?at?63?chars?because?some?Kubernetes?name?fields?are?limited?to?this?(by?the?DNS?naming?spec).
If?release?name?contains?chart?name?it?will?be?used?as?a?full?name.
*/}}
{{-?define?"my-ghost.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?}}
{{/*
Create?chart?name?and?version?as?used?by?the?chart?label.
*/}}
{{-?define?"my-ghost.chart"?-}}
{{-?printf?"%s-%s"?.Chart.Name?.Chart.Version?|?replace?"+"?"_"?|?trunc?63?|?trimSuffix?"-"?}}
{{-?end?}}
{{/*
Common?labels
*/}}
{{-?define?"my-ghost.labels"?-}}
helm.sh/chart:?{{?include?"my-ghost.chart"?.?}}
{{?include?"my-ghost.selectorLabels"?.?}}
{{-?if?.Chart.AppVersion?}}
app.kubernetes.io/version:?{{?.Chart.AppVersion?|?quote?}}
{{-?end?}}
app.kubernetes.io/managed-by:?{{?.Release.Service?}}
{{-?end?}}
{{/*
Selector?labels
*/}}
{{-?define?"my-ghost.selectorLabels"?-}}
app.kubernetes.io/name:?{{?include?"my-ghost.name"?.?}}
app.kubernetes.io/instance:?{{?.Release.Name?}}
{{-?end?}}
然后我們可以將 Deployment 的名稱和標(biāo)簽替換掉:
#?templates/deployment.yaml
apiVersion:?apps/v1
kind:?Deployment
metadata:
??name:?{{?template?"my-ghost.fullname"?.?}}
??labels:
{{?include?"my-ghost.labels"?.?|?indent?4?}}
spec:
??selector:
????matchLabels:
{{?include?"my-ghost.selectorLabels"?.?|?indent?6?}}
??replicas:?{{?.Values.replicaCount?}}
??template:
????metadata:
??????labels:
{{?include?"my-ghost.selectorLabels"?.?|?indent?8?}}
????spec:
????#?other?spec...
為 Deployment 增加 label 標(biāo)簽,同樣 labelSelector 中也使用 my-ghost.selectorLabels 這個(gè)命名模板進(jìn)行替換,同樣對(duì) Service 也做相應(yīng)的改造:
apiVersion:?v1
kind:?Service
metadata:
??name:?{{?template?"my-ghost.fullname"?.?}}
??labels:
{{?include?"my-ghost.labels"?.?|?indent?4?}}
spec:
??selector:
{{?include?"my-ghost.selectorLabels"?.?|?indent?4?}}
??type:?{{?.Values.service.type?}}
??#?other?spec...
現(xiàn)在我們可以再使用 helm template 渲染驗(yàn)證結(jié)果是否正確:
??helm?template?--debug?my-ghost
install.go:178:?[debug]?Original?chart?version:?""
install.go:195:?[debug]?CHART?PATH:?/Users/ych/devs/workspace/yidianzhishi/course/k8strain3/content/helm/manifests/my-ghost
---
#?Source:?my-ghost/templates/service.yaml
apiVersion:?v1
kind:?Service
metadata:
??name:?release-name-my-ghost
??labels:
????helm.sh/chart:?my-ghost-0.1.0
????app.kubernetes.io/name:?my-ghost
????app.kubernetes.io/instance:?release-name
????app.kubernetes.io/version:?"1.16.0"
????app.kubernetes.io/managed-by:?Helm
spec:
??selector:
????app.kubernetes.io/name:?my-ghost
????app.kubernetes.io/instance:?release-name
??type:?NodePort
??ports:
????-?protocol:?TCP
??????targetPort:?2368
??????port:?80
---
#?Source:?my-ghost/templates/deployment.yaml
apiVersion:?apps/v1
kind:?Deployment
metadata:
??name:?release-name-my-ghost
??labels:
????helm.sh/chart:?my-ghost-0.1.0
????app.kubernetes.io/name:?my-ghost
????app.kubernetes.io/instance:?release-name
????app.kubernetes.io/version:?"1.16.0"
????app.kubernetes.io/managed-by:?Helm
spec:
??selector:
????matchLabels:
??????app.kubernetes.io/name:?my-ghost
??????app.kubernetes.io/instance:?release-name
??replicas:?1
??template:
????metadata:
??????labels:
????????app.kubernetes.io/name:?my-ghost
????????app.kubernetes.io/instance:?release-name
????spec:
??????containers:
????????-?name:?ghost-app
??????????image:?ghost
??????????ports:
????????????-?containerPort:?2368
??????????env:
????????????-?name:?NODE_ENV
??????????????value:?production
????????????-?name:?url
??????????????value:?http://ghost.k8s.local
版本兼容
由于 Kubernetes 的版本迭代非???,所以我們?cè)陂_發(fā) Chart 包的時(shí)候有必要考慮到對(duì)不同版本的 Kubernetes 進(jìn)行兼容,最明顯的就是 Ingress 的資源版本。Kubernetes 在 1.19 版本為 Ingress 資源引入了一個(gè)新的 API:networking.k8s.io/v1,這與之前的 networking.k8s.io/v1beta1 beta 版本使用方式基本一致,但是和前面的 extensions/v1beta1 這個(gè)版本在使用上有很大的不同,資源對(duì)象的屬性上有一定的區(qū)別,所以要兼容不同的版本,我們就需要對(duì)模板中的 Ingress 對(duì)象做兼容處理。
新版本的資源對(duì)象格式如下所示:
apiVersion:?networking.k8s.io/v1
kind:?Ingress
metadata:
??name:?minimal-ingress
??annotations:
????nginx.ingress.kubernetes.io/rewrite-target:?/
spec:
??ingressClassName:?nginx
??rules:
??-?http:
??????paths:
??????-?path:?/testpath
????????pathType:?Prefix
????????backend:
??????????service:
????????????name:?test
????????????port:
??????????????number:?80
而舊版本的資源對(duì)象格式如下:
apiVersion:?extensions/v1beta1
kind:?Ingress
metadata:
??name:?minimal-ingress
??annotations:
????kubernetes.io/ingress.class:?nginx
????nginx.ingress.kubernetes.io/rewrite-target:?/
spec:
??rules:
??-?http:
??????paths:
??????-?path:?/testpath
????????backend:
??????????serviceName:?test
??????????servicePort:?80
現(xiàn)在我們?cè)贋?Ghost 添加一個(gè) Ingress 的模板,新建 templates/ingress.yaml 模板文件,先添加一個(gè) v1 版本的 Ingress 模板:
apiVersion:?networking.k8s.io/v1
kind:?Ingress
metadata:
??name:?ghost
spec:
??ingressClassName:?nginx
??rules:
??-?host:?ghost.k8s.local
????http:
??????paths:
??????-?path:?/
????????pathType:?Prefix
????????backend:
??????????service:
????????????name:?ghost
????????????port:
??????????????number:?80
然后同樣將名稱和服務(wù)名稱這些使用模板參數(shù)進(jìn)行替換:
apiVersion:?networking.k8s.io/v1
kind:?Ingress
metadata:
??name:?{{?template?"my-ghost.fullname"?.?}}
??labels:
{{?include?"my-ghost.labels"?.?|?indent?4?}}
spec:
??ingressClassName:?nginx
??rules:
??-?host:?{{?.Values.url?}}
????http:
??????paths:
??????-?path:?/
????????pathType:?Prefix
????????backend:
??????????service:
????????????name:?{{?template?"my-ghost.fullname"?.?}}
????????????port:
??????????????number:?{{?.Values.service.port?}}
然后接下來(lái)我們來(lái)兼容下其他的版本格式,這里需要用到 Capabilities 對(duì)象,在 Chart 包的 _helpers.tpl 文件中添加幾個(gè)用于判斷集群版本或 API 的命名模板:
{{/*?Allow?KubeVersion?to?be?overridden.?*/}}
{{-?define?"my-ghost.kubeVersion"?-}}
??{{-?default?.Capabilities.KubeVersion.Version?.Values.kubeVersionOverride?-}}
{{-?end?-
{{/*?Get?Ingress?API?Version?*/}}
{{-?define?"my-ghost.ingress.apiVersion"?-}}
??{{-?if?and?(.Capabilities.APIVersions.Has?"networking.k8s.io/v1")?(semverCompare?">=?1.19-0"?(include?"my-ghost.kubeVersion"?.))?-}}
??????{{-?print?"networking.k8s.io/v1"?-}}
??{{-?else?if?.Capabilities.APIVersions.Has?"networking.k8s.io/v1beta1"?-}}
????{{-?print?"networking.k8s.io/v1beta1"?-}}
??{{-?else?-}}
????{{-?print?"extensions/v1beta1"?-}}
??{{-?end?-}}
{{-?end?-}}
{{/*?Check?Ingress?stability?*/}}
{{-?define?"my-ghost.ingress.isStable"?-}}
??{{-?eq?(include?"my-ghost.ingress.apiVersion"?.)?"networking.k8s.io/v1"?-}}
{{-?end?-}}
{{/*?Check?Ingress?supports?pathType?*/}}
{{/*?pathType?was?added?to?networking.k8s.io/v1beta1?in?Kubernetes?1.18?*/}}
{{-?define?"my-ghost.ingress.supportsPathType"?-}}
??{{-?or?(eq?(include?"my-ghost.ingress.isStable"?.)?"true")?(and?(eq?(include?"my-ghost.ingress.apiVersion"?.)?"networking.k8s.io/v1beta1")?(semverCompare?">=?1.18-0"?(include?"my-ghost.kubeVersion"?.)))?-}}
{{-?end?-}}
上面我們通過(guò) .Capabilities.APIVersions.Has 來(lái)判斷我們應(yīng)該使用的 APIVersion,如果版本為 networking.k8s.io/v1,則定義為 isStable,此外還根據(jù)版本來(lái)判斷是否需要支持 pathType 屬性,然后在 Ingress 對(duì)象模板中就可以使用上面定義的命名模板來(lái)決定應(yīng)該使用哪些屬性,如下所示:
{{-?if?.Values.ingress.enabled?}}
{{-?$apiIsStable?:=?eq?(include?"my-ghost.ingress.isStable"?.)?"true"?-}}
{{-?$ingressSupportsPathType?:=?eq?(include?"my-ghost.ingress.supportsPathType"?.)?"true"?-}}
apiVersion:?{{?include?"my-ghost.ingress.apiVersion"?.?}}
kind:?Ingress
metadata:
??name:?{{?template?"my-ghost.fullname"?.?}}
??annotations:
????nginx.ingress.kubernetes.io/ssl-redirect:?"false"
????{{-?if?and?.Values.ingress.ingressClass?(not?$apiIsStable)?}}
????kubernetes.io/ingress.class:?{{?.Values.ingress.ingressClass?}}
????{{-?end?}}
??labels:
????{{-?include?"my-ghost.labels"?.?|?nindent?4?}}
spec:
??{{-?if?and?.Values.ingress.ingressClass?$apiIsStable?}}
??ingressClassName:?{{?.Values.ingress.ingressClass?}}
??{{-?end?}}
??rules:
??{{-?if?not?(empty?.Values.url)?}}
??-?host:?{{?.Values.url?}}
????http:
??{{-?else?}}
??-?http:
??{{-?end?}}
??????paths:
??????-?path:?/
????????{{-?if?$ingressSupportsPathType?}}
????????pathType:?Prefix
????????{{-?end?}}
????????backend:
??????????{{-?if?$apiIsStable?}}
??????????service:
????????????name:?{{?template?"my-ghost.fullname"?.?}}
????????????port:
??????????????number:?{{?.Values.service.port?}}
??????????{{-?else?}}
??????????serviceName:?{{?template?"my-ghost.fullname"?.?}}
??????????servicePort:?{{?.Values.service.port?}}
??????????{{-?end?}}
{{-?end?}}
由于有的場(chǎng)景下面并不需要使用 Ingress 來(lái)暴露服務(wù),所以首先我們通過(guò)一個(gè) ingress.enabled 屬性來(lái)控制是否需要渲染,然后定義了一個(gè) $apiIsStable 變量,來(lái)表示當(dāng)前集群是否是穩(wěn)定版本的 API,然后需要根據(jù)該變量去渲染不同的屬性,比如對(duì)于 ingressClass,如果是穩(wěn)定版本的 API 則是通過(guò) spec.ingressClassName 來(lái)指定,否則是通過(guò) kubernetes.io/ingress.class 這個(gè) annotations 來(lái)指定。然后這里我們?cè)?values.yaml 文件中添加如下所示默認(rèn)的 Ingress 的配置數(shù)據(jù):
ingress:
??enabled:?true
??ingressClass:?nginx
現(xiàn)在我們?cè)俅武秩?Helm Chart 模板來(lái)驗(yàn)證資源清單數(shù)據(jù):
??helm?template?--debug?my-ghost
install.go:178:?[debug]?Original?chart?version:?""
install.go:195:?[debug]?CHART?PATH:?/Users/ych/devs/workspace/yidianzhishi/course/k8strain3/content/helm/manifests/my-ghost
---
#?Source:?my-ghost/templates/service.yaml
apiVersion:?v1
kind:?Service
metadata:
??name:?release-name-my-ghost
??labels:
????helm.sh/chart:?my-ghost-0.1.0
????app.kubernetes.io/name:?my-ghost
????app.kubernetes.io/instance:?release-name
????app.kubernetes.io/version:?"1.16.0"
????app.kubernetes.io/managed-by:?Helm
spec:
??selector:
????app.kubernetes.io/name:?my-ghost
????app.kubernetes.io/instance:?release-name
??type:?NodePort
??ports:
????-?protocol:?TCP
??????targetPort:?2368
??????port:?80
---
#?Source:?my-ghost/templates/deployment.yaml
apiVersion:?apps/v1
kind:?Deployment
metadata:
??name:?release-name-my-ghost
??labels:
????helm.sh/chart:?my-ghost-0.1.0
????app.kubernetes.io/name:?my-ghost
????app.kubernetes.io/instance:?release-name
????app.kubernetes.io/version:?"1.16.0"
????app.kubernetes.io/managed-by:?Helm
spec:
??selector:
????matchLabels:
??????app.kubernetes.io/name:?my-ghost
??????app.kubernetes.io/instance:?release-name
??replicas:?1
??template:
????metadata:
??????labels:
????????app.kubernetes.io/name:?my-ghost
????????app.kubernetes.io/instance:?release-name
????spec:
??????containers:
????????-?name:?ghost-app
??????????image:?ghost
??????????ports:
????????????-?containerPort:?2368
??????????env:
????????????-?name:?NODE_ENV
??????????????value:?production
????????????-?name:?url
??????????????value:?http://ghost.k8s.local
---
#?Source:?my-ghost/templates/ingress.yaml
apiVersion:?networking.k8s.io/v1
kind:?Ingress
metadata:
??name:?release-name-my-ghost
??annotations:
????nginx.ingress.kubernetes.io/ssl-redirect:?"false"
??labels:
????helm.sh/chart:?my-ghost-0.1.0
????app.kubernetes.io/name:?my-ghost
????app.kubernetes.io/instance:?release-name
????app.kubernetes.io/version:?"1.16.0"
????app.kubernetes.io/managed-by:?Helm
spec:
??ingressClassName:?nginx
??rules:
??-?host:?ghost.k8s.local
????http:
??????paths:
??????-?path:?/
????????pathType:?Prefix
????????backend:
??????????service:
????????????name:?release-name-my-ghost
????????????port:
??????????????number:?80
從上面的資源清單可以看出是符合我們的預(yù)期要求的,我們可以來(lái)安裝測(cè)試下結(jié)果:
??helm?upgrade?--install?my-ghost?./my-ghost?-n?default
Release?"my-ghost"?does?not?exist.?Installing?it?now.
NAME:?my-ghost
LAST?DEPLOYED:?Thu?Mar?17?13:11:15?2022
NAMESPACE:?default
STATUS:?deployed
REVISION:?1
TEST?SUITE:?None
??helm?ls?-n?default
NAME????????????NAMESPACE???????REVISION????????UPDATED?????????????????????????????????STATUS??????????CHART????????????????APP?VERSION
my-ghost????????default?????????1???????????????2022-03-17?13:11:15.79828?+0800?CST?????deployed????????my-ghost-0.1.0???????1.16.0
??kubectl?get?pods?-n?default
NAME????????????????????????READY???STATUS??????RESTARTS???AGE
my-ghost-7cf7fb6484-75hkk???1/1?????Running?????0??????????3m9s
??kubectl?get?svc?-n?default
NAME?????????TYPE????????CLUSTER-IP???????EXTERNAL-IP???PORT(S)????????AGE
kubernetes???ClusterIP???10.96.0.1????????????????443/TCP????????142d
my-ghost?????NodePort????10.108.125.187???????????80:32433/TCP???3m20s
??kubectl?get?ingress?-n?default
NAME???????CLASS???HOSTS?????????????ADDRESS?????????PORTS???AGE
my-ghost???nginx???ghost.k8s.local???192.168.31.31???80??????3m32s
正常就可以部署成功 Ghost 了,并且可以通過(guò)域名 http://ghost.k8s.local 進(jìn)行訪問(wèn)了:

持久化
上面我們使用的 Ghost 鏡像默認(rèn)使用 SQLite 數(shù)據(jù)庫(kù),所以非常有必要將數(shù)據(jù)進(jìn)行持久化,當(dāng)然我們要將這個(gè)開關(guān)給到用戶去選擇,修改 templates/deployment.yaml 模板文件,增加 volumes 相關(guān)配置:
#?other?spec...
spec:
??volumes:
????-?name:?ghost-data
????{{-?if?.Values.persistence.enabled?}}
??????persistentVolumeClaim:
????????claimName:?{{?.Values.persistence.existingClaim?|?default?(include?"my-ghost.fullname"?.)?}}
????{{-?else?}}
??????emptyDir:?{}
????{{?end?}}
??containers:
????-?name:?ghost-app
??????image:?{{?.Values.image?}}
??????volumeMounts:
????????-?name:?ghost-data
??????????mountPath:?/var/lib/ghost/content
??????#?other?spec...
這里我們通過(guò) persistence.enabled 來(lái)判斷是否需要開啟持久化數(shù)據(jù),如果開啟則需要看用戶是否直接提供了一個(gè)存在的 PVC 對(duì)象,如果沒有提供,則我們需要自己創(chuàng)建一個(gè)合適的 PVC 對(duì)象,如果不需要持久化,則直接使用 emptyDir:{} 即可,添加 templates/pvc.yaml 模板,內(nèi)容如下所示:
{{-?if?and?.Values.persistence.enabled?(not?.Values.persistence.existingClaim)?}}
kind:?PersistentVolumeClaim
apiVersion:?v1
metadata:
??name:?{{?template?"my-ghost.fullname"?.?}}
??labels:
????{{-?include?"my-ghost.labels"?.?|?nindent?4?}}
spec:
??{{-?if?.Values.persistence.storageClass?}}
??storageClassName:?{{?.Values.persistence.storageClass?|?quote?}}
??{{-?end?}}
??accessModes:
??-?{{?.Values.persistence.accessMode?|?quote?}}
??resources:
????requests:
??????storage:?{{?.Values.persistence.size?|?quote?}}
{{-?end?-}}
其中訪問(wèn)模式、存儲(chǔ)容量、StorageClass、存在的 PVC 都通過(guò) Values 來(lái)指定,增加了靈活性。對(duì)應(yīng)的 values.yaml 配置部分我們可以給一個(gè)默認(rèn)的配置:
##?是否使用?PVC?開啟數(shù)據(jù)持久化
persistence:
??enabled:?true
??##?是否使用?storageClass,如果不適用則補(bǔ)配置
??#?storageClass:?"xxx"
??##
??##?如果想使用一個(gè)存在的?PVC?對(duì)象,則直接傳遞給下面的?existingClaim?變量
??#?existingClaim:?your-claim
??accessMode:?ReadWriteOnce??#?訪問(wèn)模式
??size:?1Gi??#?存儲(chǔ)容量
定制
除了上面的這些主要的需求之外,還有一些額外的定制需求,比如用戶想要配置更新策略,因?yàn)楦虏呗圆⒉皇且粚硬蛔兊?,這里和之前不太一樣,我們需要用到一個(gè)新的函數(shù) toYaml:
{{-?if?.Values.updateStrategy?}}
strategy:?{{?toYaml?.Values.updateStrategy?|?nindent?4?}}
{{-?end?}}
意思就是我們將 updateStrategy 這個(gè) Values 值轉(zhuǎn)換成 YAML 格式,并保留4個(gè)空格。然后添加其他的配置,比如是否需要添加 nodeSelector、容忍、親和性這些,這里我們都是使用 toYaml 函數(shù)來(lái)控制空格,如下所示:
{{-?if?.Values.nodeSelector?}}
nodeSelector:?{{-?toYaml?.Values.nodeSelector?|?nindent?8?}}
{{-?end?-}}
{{-?with?.Values.affinity?}}
affinity:?{{-?toYaml?.?|?nindent?8?}}
{{-?end?}}
{{-?with?.Values.tolerations?}}
tolerations:?{{-?toYaml?.?|?nindent?8?}}
{{-?end?}}
接下來(lái)當(dāng)然就是鏡像的配置了,如果是私有倉(cāng)庫(kù)還需要指定 imagePullSecrets:
{{-?if?.Values.image.pullSecrets?}}
imagePullSecrets:
{{-?range?.Values.image.pullSecrets?}}
-?name:?{{?.?}}
{{-?end?}}
{{-?end?}}
containers:
-?name:?ghost
??image:?{{?printf?"%s:%s"?.Values.image.name?.Values.image.tag?}}
??imagePullPolicy:?{{?.Values.image.pullPolicy?|?quote?}}
??ports:
??-?containerPort:?2368
對(duì)應(yīng)的 Values 值如下所示:
image:
??name:?ghost
??tag:?latest
??pullPolicy:?IfNotPresent
??##?如果是私有倉(cāng)庫(kù),需要指定?imagePullSecrets
??#?pullSecrets:
??#???-?myRegistryKeySecretName
然后就是 resource 資源聲明,這里我們定義一個(gè)默認(rèn)的 resources 值,同樣用 toYaml 函數(shù)來(lái)控制空格:
resources:
{{?toYaml?.Values.resources?|?indent?10?}}
最后是健康檢查部分,雖然我們之前沒有做 livenessProbe,但是我們開發(fā) Chart 模板的時(shí)候就要盡可能考慮周全一點(diǎn),這里我們加上存活性和可讀性、啟動(dòng)三個(gè)探針,并且根據(jù) livenessProbe.enabled 、readinessProbe.enabled 以及 startupProbe.enabled 三個(gè) Values 值來(lái)判斷是否需要添加探針,探針對(duì)應(yīng)的參數(shù)也都通過(guò) Values 值來(lái)配置:
{{-?if?.Values.startupProbe.enabled?}}
startupProbe:
??httpGet:
????path:?/
????port:?2368
??initialDelaySeconds:?{{?.Values.startupProbe.initialDelaySeconds?}}
??periodSeconds:?{{?.Values.startupProbe.periodSeconds?}}
??timeoutSeconds:?{{?.Values.startupProbe.timeoutSeconds?}}
??failureThreshold:?{{?.Values.startupProbe.failureThreshold?}}
??successThreshold:?{{?.Values.startupProbe.successThreshold?}}
{{-?end?}}
{{-?if?.Values.livenessProbe.enabled?}}
livenessProbe:
??httpGet:
????path:?/
????port:?2368
??initialDelaySeconds:?{{?.Values.livenessProbe.initialDelaySeconds?}}
??periodSeconds:?{{?.Values.livenessProbe.periodSeconds?}}
??timeoutSeconds:?{{?.Values.livenessProbe.timeoutSeconds?}}
??failureThreshold:?{{?.Values.livenessProbe.failureThreshold?}}
??successThreshold:?{{?.Values.livenessProbe.successThreshold?}}
{{-?end?}}
{{-?if?.Values.readinessProbe.enabled?}}
readinessProbe:
??httpGet:
????path:?/
????port:?2368
??initialDelaySeconds:?{{?.Values.readinessProbe.initialDelaySeconds?}}
??periodSeconds:?{{?.Values.readinessProbe.periodSeconds?}}
??timeoutSeconds:?{{?.Values.readinessProbe.timeoutSeconds?}}
??failureThreshold:?{{?.Values.readinessProbe.failureThreshold?}}
??successThreshold:?{{?.Values.readinessProbe.successThreshold?}}
{{-?end?}}
默認(rèn)的 values.yaml 文件如下所示:
replicaCount:?1
image:
??name:?ghost
??tag:?latest
??pullPolicy:?IfNotPresent
node_env:?production
url:?ghost.k8s.local
service:
??type:?ClusterIP
??port:?80
ingress:
??enabled:?true
??ingressClass:?nginx
##?是否使用?PVC?開啟數(shù)據(jù)持久化
persistence:
??enabled:?true
??##?是否使用?storageClass,如果不適用則補(bǔ)配置
??#?storageClass:?"xxx"
??##
??##?如果想使用一個(gè)存在的?PVC?對(duì)象,則直接傳遞給下面的?existingClaim?變量
??#?existingClaim:?your-claim
??accessMode:?ReadWriteOnce??#?訪問(wèn)模式
??size:?1Gi??#?存儲(chǔ)容量
nodeSelector:?{}
affinity:?{}
tolerations:?{}
resources:?{}
startupProbe:
??enabled:?false
livenessProbe:
??enabled:?false
readinessProbe:
??enabled:?false
現(xiàn)在我們?cè)偃ジ?Release:
??helm?upgrade?--install?my-ghost?./my-ghost?-n?default
Release?"my-ghost"?has?been?upgraded.?Happy?Helming!
NAME:?my-ghost
LAST?DEPLOYED:?Thu?Mar?17?16:03:02?2022
NAMESPACE:?default
STATUS:?deployed
REVISION:?2
TEST?SUITE:?None
??helm?ls?-n?default
NAME????????????NAMESPACE???????REVISION????????UPDATED?????????????????????????????????STATUS??????????CHART????????????APP?VERSION
my-ghost????????default?????????2???????????????2022-03-17?16:05:07.123349?+0800?CST????deployed????????my-ghost-0.1.0???1.16.0
??kubectl?get?pods?-n?default
NAME????????????????????????READY???STATUS??????RESTARTS???AGE
my-ghost-6dbc455fc7-cmm4p???1/1?????Running?????0??????????2m42s
??kubectl?get?pvc?-n?default
NAME???????STATUS???VOLUME?????????????????????????????????????CAPACITY???ACCESS?MODES???STORAGECLASS???AGE
my-ghost???Bound????pvc-2f0b7d5a-04d4-4331-848b-af21edce673e???1Gi????????RWO????????????nfs-client?????4m59s
??kubectl?get?ingress?-n?default
NAME???????CLASS???HOSTS?????????????ADDRESS?????????PORTS???AGE
my-ghost???nginx???ghost.k8s.local???192.168.31.31???80??????3h24m
到這里我們就基本完成了這個(gè)簡(jiǎn)單的 Helm Charts 包的開發(fā),當(dāng)然以后可能還會(huì)有新的需求,我們需要不斷去迭代優(yōu)化。
共享 Charts
Helm Charts 包開發(fā)完成了,如果別人想要使用我們的包,則需要我們共享出去,我們可以通過(guò) Chart 倉(cāng)庫(kù)來(lái)進(jìn)行共享,Helm Charts 可以在遠(yuǎn)程存儲(chǔ)庫(kù)或本地環(huán)境/存儲(chǔ)庫(kù)中使用,遠(yuǎn)程存儲(chǔ)庫(kù)可以是公共的,如 Bitnami Charts 也可以是托管存儲(chǔ)庫(kù),如 Google Cloud Storage 或 GitHub。為了演示方便,這里我們使用 GitHub 來(lái)托管我們的 Charts 包。
我們可以使用 GitHub Pages 來(lái)創(chuàng)建 Charts 倉(cāng)庫(kù),GitHub 允許我們以兩種不同的方式提供靜態(tài)網(wǎng)頁(yè):
通過(guò)配置項(xiàng)目提供其 docs/目錄的內(nèi)容通過(guò)配置項(xiàng)目來(lái)服務(wù)特定的分支
這里我們將采用第二種方法,首先在 GitHub 上創(chuàng)建一個(gè)代碼倉(cāng)庫(kù):https://github.com/cnych/helm101,將上面我們創(chuàng)建的 my-ghost 包提交到倉(cāng)庫(kù) charts 目錄下,然后打包 chart 包:
??helm?package?charts/my-ghost
Successfully?packaged?chart?and?saved?it?to:?/Users/ych/devs/workspace/yidianzhishi/course/k8strain3/content/helm/manifests/helm101/my-ghost-0.1.0.tgz
我們可以將打包的壓縮包放到另外的目錄 repo/stable 中去,現(xiàn)在倉(cāng)庫(kù)的結(jié)構(gòu)如下所示:
??tree?.
.
├──?LICENSE
├──?README.md
├──?charts
│???└──?my-ghost
│???????├──?Chart.lock
│???????├──?Chart.yaml
│???????├──?charts
│???????├──?templates
│???????│???├──?_helpers.tpl
│???????│???├──?deployment.yaml
│???????│???├──?ingress.yaml
│???????│???├──?pvc.yaml
│???????│???├──?service.yaml
│???????│???└──?tests
│???????└──?values.yaml
└──?repo
????└──?stable
????????└──?my-ghost-0.1.0.tgz
執(zhí)行如下所示命令生成 index 索引文件:
??helm?repo?index?repo/stable?--url?https://raw.githubusercontent.com/cnych/helm101/main/repo/stable
上述命令會(huì)在 repo/stable 目錄下面生成一個(gè)如下所示的 index.yaml 文件:
apiVersion:?v1
entries:
??my-ghost:
??-?apiVersion:?v2
????appVersion:?1.16.0
????created:?"2022-03-17T17:40:21.093654+08:00"
????description:?A?Helm?chart?for?Kubernetes
????digest:?f6d6308d6a6cd6357ab2b952650250c2df7b2727ce84c19150531fd72732626b
????name:?my-ghost
????type:?application
????urls:
????-?https://raw.githubusercontent.com/cnych/helm101/main/repo/stable/my-ghost-0.1.0.tgz
????version:?0.1.0
generated:?"2022-03-17T17:40:21.090371+08:00"
該 index.yaml 文件是我們通過(guò)倉(cāng)庫(kù)獲取 Chart 包的關(guān)鍵。然后將代碼推送到 GitHub:
??git?status
On?branch?main
Your?branch?is?up?to?date?with?'origin/main'.
Untracked?files:
??(use?"git?add?..."?to?include?in?what?will?be?committed)
????????charts/
????????repo/
nothing?added?to?commit?but?untracked?files?present?(use?"git?add"?to?track)
??git?commit?-m?"add?charts?and?index.yaml"
[main?aae1059]?add?charts?and?index.yaml
?11?files?changed,?431?insertions(+)
?create?mode?100644?charts/my-ghost/.helmignore
?create?mode?100644?charts/my-ghost/Chart.lock
?create?mode?100644?charts/my-ghost/Chart.yaml
?create?mode?100644?charts/my-ghost/templates/_helpers.tpl
?create?mode?100644?charts/my-ghost/templates/deployment.yaml
?create?mode?100644?charts/my-ghost/templates/ingress.yaml
?create?mode?100644?charts/my-ghost/templates/pvc.yaml
?create?mode?100644?charts/my-ghost/templates/service.yaml
?create?mode?100644?charts/my-ghost/values.yaml
?create?mode?100644?repo/stable/index.yaml
?create?mode?100644?repo/stable/my-ghost-0.1.0.tgz
??git?push?origin?main
Enumerating?objects:?18,?done.
Counting?objects:?100%?(18/18),?done.
Writing?objects:?100%?(18/18),?8.71?KiB?|?2.18?MiB/s,?done.
Total?18?(delta?0),?reused?0?(delta?0)
To?github.com:cnych/helm101.git
???9c389a6..aae1059??main?->?main
接下來(lái)為該倉(cāng)庫(kù)設(shè)置 GitHub Pages,首先在本地新建一個(gè) gh-pages 分支:
??git?checkout?-b?gh-pages
只將 repo/stable/index.yaml 文件保留到根目錄下面,其他文件忽略,然后推送到遠(yuǎn)程倉(cāng)庫(kù):
??git?push?origin?gh-pages
Enumerating?objects:?2,?done.
Counting?objects:?100%?(2/2),?done.
Writing?objects:?100%?(2/2),?301?bytes?|?301.00?KiB/s,?done.
Total?2?(delta?0),?reused?0?(delta?0)
remote:
remote:?Create?a?pull?request?for?'gh-pages'?on?GitHub?by?visiting:
remote:??????https://github.com/cnych/helm101/pull/new/gh-pages
remote:
To?github.com:cnych/helm101.git
?*?[new?branch]??????gh-pages?->?gh-pages
在 GitHub Pages 頁(yè)面選擇使用 gh-pages 分支即可:
現(xiàn)在我們就可以通過(guò) https://cnych.github.io/helm101/ 來(lái)獲取我們的 Chart 包了。
使用如下所示命令添加 repo 倉(cāng)庫(kù):
??helm?repo?add?helm101?https://cnych.github.io/helm101/
"helm101"?has?been?added?to?your?repositories
我們也可以使用 helm search 來(lái)搜索倉(cāng)庫(kù)中的 Chart 包,正常就包含上面我們的 my-ghost 了:
??helm?search?repo?helm101
NAME????????????????????CHART?VERSION???APP?VERSION?????DESCRIPTION
helm101/my-ghost????????0.1.0???????????1.16.0??????????A?Helm?chart?for?Kubernetes
接下來(lái)就可以正常使用 chart 包進(jìn)行操作了,比如進(jìn)行安裝:
??helm?install?my-ghost?helm101/
