天天看點

如何開發一個完整的Helm charts應用執行個體

文章目錄

  • ​​1. 簡介​​
  • ​​2. 條件​​
  • ​​3. 應用​​
  • ​​4. 基礎模闆​​
  • ​​5. 命名模闆​​
  • ​​6. 版本相容​​
  • ​​7. 持久化​​
  • ​​8. 定制​​
  • ​​9. 共享 Charts​​

1. 簡介

Helm 圖表是​​在 Kubernetes 中建構高效叢集的最佳實踐之一​​。它是一種使用 Kubernetes 資源集合的打包形式。Helm 圖表使用這些資源來定義應用程式。

Helm 圖表使用模闆方法來部署應用程式。模闆為項目提供結構,适用于任何類型的應用程式。

如何開發一個完整的Helm charts應用執行個體

2. 條件

  • 安裝并配置了 Minikube 叢集(請按照我們的指南​​如何在 Ubuntu 上安裝 Minikube​​​和​​如何在 CentOS 上安裝 Minikube​​
  • 你需要懂得​​Helm 安裝​​和配置;
  • 你需要掌握基本​​helm文法編寫技巧​​

3. 應用

以 Ghost 部落格應用為例來示範如何開發一個完整的 Helm Chart 包,Ghost 是基于 Node.js 的開源部落格平台。在開發 Helm Chart 包之前最需要做的的就是要知道應用應該如何使用、如何部署,不然是不可能編寫出對應的 Chart 包的。

啟動 Ghost 最簡單的方式是直接使用鏡像啟動:

docker run -d --name my-ghost -p 2368:2368 ghost      
如何開發一個完整的Helm charts應用執行個體

後我們就可以通過 ​​

​http://localhost:2368​

​ 通路 Ghost 部落格了。

docker rm -f my-ghost      

如果我們想要在 Kubernetes 叢集中部署兩個副本的 Ghost,可以直接應用下面的資源清單檔案即可:

# 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      

直接通過 kubectl 應用上面的資源對象即可:

$ kubectl apply -f  ghost/
service/ghost created
deployment.apps/ghost created

$ kubectl get pod -l app=ghost-app
NAME                    READY   STATUS    RESTARTS   AGE
ghost-ddb558557-7szrc   1/1     Running   0          2m13s
ghost-ddb558557-brn9p   1/1     Running   0          2m13s


$ kubectl get svc ghost
NAME    TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
ghost   NodePort   10.97.232.158           80:30152/TCP   2m44s      

通過 ​

​http://<nodeip>:31950​

​​ 通路到 ​

​Ghost​

​​ :

如何開發一個完整的Helm charts應用執行個體

清理 deployment

$ delete -f ghost/
deployment.apps "ghost" deleted
service "ghost" deleted      

看上去要部署 Ghost 是非常簡單的,但是如果我們需要針對不同的環境進行不同的設定呢?比如我們想将它部署到不同環境(staging、prod)中去,是不是我們需要一遍又一遍地複制我們的 Kubernetes 資源清單檔案,這還隻是一個場景,還有很多場景可能需要我們去部署應用,這種方式維護起來是非常困難的,這個時候就可以理由 Helm 來解放我們了。

  • ​​官方安裝 helm​​
  • ​​helm v3.8.0 指令入門指南​​

4. 基礎模闆

現在我們開始建立一個新的 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      

該指令會建立一個預設 ​

​Helm Chart​

​​ 包的腳手架,​​helm charts 細節說明請參閱該片文章​​,可以删掉下面的這些使用不到的檔案:

rm -f my-ghost/templates/tests/test-connection.yaml
rm -f my-ghost/templates/serviceaccount.yaml
rm -f my-ghost/templates/ingress.yaml
rm -f my-ghost/templates/hpa.yaml
rm -f my-ghost/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 }}​

​​ 模闆來進行替換了,表示會用 ​

​replicaCount​

​​ 這個 ​

​Values​

​​ 值進行渲染,然後還可以通過設定環境變量來配置 Ghost,同樣修改 ​

​templates/service.yaml​

​ 模闆檔案的内容:

# 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 }}      

同樣為了能夠相容多個場景,這裡我們允許使用者來定制 ​

​Service​

​​ 的 ​

​type​

​​,如果是 ​

​NodePort​

​​ 類型則還可以配置 ​

​nodePort​

​​ 的值,不過需要注意這裡的判斷,因為有可能即使配置為 ​

​NodePort​

​​ 類型,使用者也可能不會主動提供 ​

​nodePort​

​,是以這裡我們在模闆中做了一個條件判斷:

{{- if (and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) (not (empty .Values.service.nodePort))) }}      

需要 ​

​service.type​

​​ 為 ​

​NodePort​

​​ 或者 ​

​LoadBalancer​

​​ 并且 ​

​service.nodePort​

​​ 不為空的情況下才會渲染 ​

​nodePort​

​。

然後最重要的就是要在 ​

​values.yaml​

​​ 檔案中提供預設的 ​

​Values​

​​ 值,如下所示是我們提供的預設的 ​

​Values​

​ 值:

# values.yaml
replicaCount: 1
image: ghost
node_env: production
url: ghost.k8s.local

service:
  type: NodePort
  port: 80      

然後我們可以使用 ​

​helm template​

​ 指令來渲染我們的模闆輸出結果:

$  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      

上面的渲染結果和我們上面的資源清單檔案基本上一緻了,隻是我們現在的靈活性更大了,比如可以控制環境變量、服務的暴露方式等等。

5. 命名模闆

雖然現在我們可以使用 ​

​Helm Charts​

​​ 模闆來渲染安裝 ​

​Ghost​

​​ 了,但是上面我們的模闆還有很多改進的地方,比如資源對象的名稱我們是固定的,這樣我們就沒辦法在同一個命名空間下面安裝多個應用了,是以一般我們也會根據 ​

​Chart​

​​ 名稱或者 ​

​Release​

​ 名稱來替換資源對象的名稱。

前面預設建立的模闆中包含一個 ​

​_helpers.tpl​

​​ 的檔案,該檔案中包含一些和名稱、标簽相關的​​命名模闆​​,我們可以直接使用即可,下面是預設生成的已有的命名模闆:

{{/*
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​

​ 的名稱和标簽替換掉:

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:
      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 }}      

為 Deployment 增加 label 标簽,同樣 ​

​labelSelector​

​​ 中也使用 ​

​my-ghost.selectorLabels​

​ 這個命名模闆進行替換,同樣對 Service 也做相應的改造:

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 }}
  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 }}      

現在我們可以再使用 helm template 渲染驗證結果是否正确:

$ 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      

6. 版本相容

于 Kubernetes 的版本疊代非常快,是以我們在開發 Chart 包的時候有必要考慮到對不同版本的 Kubernetes 進行相容,最明顯的就是 Ingress 的資源版本。​

​Kubernetes​

​​ 在 ​

​1.19​

​​ 版本為 Ingress 資源引入了一個新的 API:​

​networking.k8s.io/v1​

​​,這與之前的 ​

​networking.k8s.io/v1beta1 beta​

​​ 版本使用方式基本一緻,但是和前面的 ​

​extensions/v1beta1​

​ 這個版本在使用上有很大的不同,資源對象的屬性上有一定的差別,是以要相容不同的版本,我們就需要對模闆中的 Ingress 對象做相容處理。

建立ingress對象,確定你已經安裝了​​ingress controller元件​​

新版本的資源對象格式如下所示:

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      

而舊版本的資源對象格式如下:

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      

現在我們再為 Ghost 添加一個 ​

​Ingress​

​​ 的模闆,建立 ​

​templates/ingress.yaml​

​​ 模闆檔案,先添加一個 ​

​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      

然後同樣将名稱和服務名稱這些使用模闆參數進行替換:

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 }}      

然後接下來我們來相容下其他的版本格式,這裡需要用到 ​

​Capabilities​

​​ 對象,在 ​

​Chart​

​​ 包的 ​

​_helpers.tpl​

​ 檔案中添加幾個用于判斷叢集版本或 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 -}}      

上面我們通過 ​

​.Capabilities.APIVersions.Has​

​​ 來判斷我們應該使用的 ​

​APIVersion​

​​,如果版本為 ​

​networking.k8s.io/v1​

​​,則定義為 ​

​isStable​

​​,此外還根據版本來判斷是否需要支援 ​

​pathType​

​​ 屬性,然後在 ​

​Ingress​

​ 對象模闆中就可以使用上面定義的命名模闆來決定應該使用哪些屬性,如下所示:

{{- 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 }}      

由于有的場景下面并不需要使用 ​

​Ingress​

​​ 來暴露服務,是以首先我們通過一個 ​

​ingress.enabled​

​​ 屬性來控制是否需要渲染,然後定義了一個 ​

​$apiIsStable​

​​ 變量,來表示目前叢集是否是穩定版本的 API,然後需要根據該變量去渲染不同的屬性,比如對于 ​

​ingressClass​

​​,如果是穩定版本的 API 則是通過 ​

​spec.ingressClassName​

​​ 來指定,否則是通過 ​

​kubernetes.io/ingress.class​

​​ 這個 ​

​annotations​

​​ 來指定。然後這裡我們在 ​

​values.yaml​

​​ 檔案中添加如下所示預設的 ​

​Ingress​

​ 的配置資料:

ingress:
  enabled: true
  ingressClass: nginx      

現在我們再次渲染 ​

​Helm Chart​

​ 模闆來驗證資源清單資料:

$ 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      

從上面的資源清單可以看出是符合我們的預期要求的,在我們安裝測試前,一定要确認我們的kubernetes叢集是否​​安裝ingress controller​​​, 你也可以​​minikube安裝nginx ingress controller​​ ,我們可以來安裝測試下結果:

$ helm upgrade --install my-ghost ./my-ghost -n default
Release "my-ghost" does not exist. Installing it now.
NAME: my-ghost
LAST DEPLOYED: Mon Mar 28 20:03:51 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-28 20:03:51.782130568 +0800 CST  deployed  my-ghost-0.1.0  1.16.0     


$ kubectl get pods -n default
NAME                        READY   STATUS    RESTARTS   AGE
my-ghost-6f698dc49d-ccphv   1/1     Running   0          29s


$ kubectl get svc -n default
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1              443/TCP        163m
my-ghost     NodePort    10.102.53.53           80:32204/TCP   39s


$ kubectl get ingress -n default
NAME       CLASS   HOSTS             ADDRESS   PORTS   AGE
my-ghost   nginx   ghost.k8s.local             80      49s

$ k get ingress -oyaml
apiVersion: v1
items:
- apiVersion: networking.k8s.io/v1
  kind: Ingress
  metadata:
    annotations:
      meta.helm.sh/release-name: my-ghost
      meta.helm.sh/release-namespace: default
      nginx.ingress.kubernetes.io/ssl-redirect: "false"
    creationTimestamp: "2022-03-28T15:50:43Z"
    generation: 1
    labels:
      app.kubernetes.io/instance: my-ghost
      app.kubernetes.io/managed-by: Helm
      app.kubernetes.io/name: my-ghost
      app.kubernetes.io/version: 1.16.0
      helm.sh/chart: my-ghost-0.1.0
    name: my-ghost
    namespace: default
    resourceVersion: "17132"
    uid: d94aa7b8-6631-4e6f-aa2e-4317615cef76
  spec:
    ingressClassName: nginx
    rules:
    - host: ghost.k8s.local
      http:
        paths:
        - backend:
            service:
              name: my-ghost
              port:
                number: 80
          path: /
          pathType: Prefix
  status:
    loadBalancer:
      ingress:
      - ip: 192.168.211.51
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""      

正常就可以部署成功 Ghost 了,并且可以通過域名 ​

​http://ghost.k8s.local​

​ 進行通路了:

當然虛拟機部署要配置域名解析:

echo '192.168.211.51  ghost.k8s.local' >> /etc/hosts      
如何開發一個完整的Helm charts應用執行個體

7. 持久化

上面我們使用的 Ghost 鏡像預設使用 SQLite 資料庫,是以非常有必要将資料進行持久化,當然我們要将這個開關給到使用者去選擇,修改 ​

​templates/deployment.yaml​

​ 模闆檔案,增加 volumes 相關配置:

# 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...      

這裡我們通過 ​

​persistence.enabled​

​​ 來判斷是否需要開啟持久化資料,如果開啟則需要看使用者是否直接提供了一個存在的 PVC 對象,如果沒有提供,則我們需要自己建立一個合适的 PVC 對象,如果不需要持久化,則直接使用 ​

​emptyDir:{}​

​​ 即可,添加 ​

​templates/pvc.yaml​

​ 模闆,内容如下所示:

{{- 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 -}}      

其中通路模式、存儲容量、​

​StorageClass​

​​、存在的 PVC 都通過 Values 來指定,增加了靈活性。對應的 ​

​values.yaml​

​ 配置部分我們可以給一個預設的配置:

## 是否使用 PVC 開啟資料持久化
persistence:
  enabled: true
  ## 是否使用 storageClass,如果不适用則補配置
  # storageClass: "xxx"
  ##
  ## 如果想使用一個存在的 PVC 對象,則直接傳遞給下面的 existingClaim 變量
  # existingClaim: your-claim
  accessMode: ReadWriteOnce  # 通路模式
  size: 1Gi  # 存儲容量      

8. 定制

除了上面的這些主要的需求之外,還有一些額外的定制需求,比如使用者想要配置更新政策,因為更新政策并不是一層不變的,這裡和之前不太一樣,我們需要用到一個新的函數 ​

​toYaml​

​:

{{- if .Values.updateStrategy }}
strategy: {{ toYaml .Values.updateStrategy | nindent 4 }}
{{- end }}      

意思就是我們将 ​

​updateStrategy​

​​ 這個 Values 值轉換成 YAML 格式,并保留4個空格。然後添加其他的配置,比如是否需要添加 ​

​nodeSelector​

​​、容忍、親和性這些,這裡我們都是使用 ​

​toYaml​

​ 函數來控制空格,如下所示:

{{- 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 }}      

接下來當然就是鏡像的配置了,如果是私有倉庫還需要指定 ​

​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      

對應的 Values 值如下所示:

image:
  name: ghost
  tag: latest
  pullPolicy: IfNotPresent
  ## 如果是私有倉庫,需要指定 imagePullSecrets
  # pullSecrets:
  #   - myRegistryKeySecretName      

然後就是 ​

​resource​

​​ 資源聲明,這裡我們定義一個預設的 ​

​resources​

​​ 值,同樣用 ​

​toYaml​

​ 函數來控制空格:

resources:
{{ toYaml .Values.resources | indent 10 }}      

最後是健康檢查部分,雖然我們之前沒有做 ​

​livenessProbe​

​​,但是我們開發 Chart 模闆的時候就要盡可能考慮周全一點,這裡我們加上存活性和可讀性、啟動三個探針,并且根據 ​

​livenessProbe.enabled​

​​ 、​

​readinessProbe.enabled​

​​ 以及 ​

​startupProbe.enabled​

​ 三個 Values 值來判斷是否需要添加探針,探針對應的參數也都通過 Values 值來配置:

{{- 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 }}      

預設的 ​

​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 開啟資料持久化
persistence:
  enabled: true
  ## 是否使用 storageClass,如果不适用則補配置
  # storageClass: "xxx"
  ##
  ## 如果想使用一個存在的 PVC 對象,則直接傳遞給下面的 existingClaim 變量
  # existingClaim: your-claim
  accessMode: ReadWriteOnce  # 通路模式
  size: 1Gi  # 存儲容量

nodeSelector: {}

affinity: {}

tolerations: {}

resources: {}

startupProbe:
  enabled: false

livenessProbe:
  enabled: false

readinessProbe:
  enabled: false      

現在我們再去更新 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

k get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
pvc-d62828dd-56ba-4819-a67a-0cd67b65dcd2   1Gi        RWO            Delete           Bound    default/my-ghost   standard                7s

$ k get pv pvc-d62828dd-56ba-4819-a67a-0cd67b65dcd2 -oyaml
apiVersion: v1
kind: PersistentVolume
metadata:
  annotations:
    hostPathProvisionerIdentity: d798e478-e315-4720-abba-dd9e6af28464
    pv.kubernetes.io/provisioned-by: k8s.io/minikube-hostpath
  creationTimestamp: "2022-03-30T13:02:22Z"
  finalizers:
  - kubernetes.io/pv-protection
  name: pvc-d62828dd-56ba-4819-a67a-0cd67b65dcd2
  resourceVersion: "1594"
  uid: b1edac7a-c960-4152-bf3b-a1038790b3a8
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 1Gi
  claimRef:
    apiVersion: v1
    kind: PersistentVolumeClaim
    name: my-ghost
    namespace: default
    resourceVersion: "1591"
    uid: d62828dd-56ba-4819-a67a-0cd67b65dcd2
  hostPath:
    path: /tmp/hostpath-provisioner/default/my-ghost
    type: ""
  persistentVolumeReclaimPolicy: Delete
  storageClassName: standard
  volumeMode: Filesystem
status:
  phase: Bound

➜ kubectl get ingress -n default
NAME       CLASS   HOSTS             ADDRESS         PORTS   AGE
my-ghost   nginx   ghost.k8s.local   192.168.31.31   80      3h24m      

到這裡我們就基本完成了這個簡單的 Helm Charts 包的開發,當然以後可能還會有新的需求,我們需要不斷去疊代優化。

當我們設定​

​storageclass: ""​

​​或者注釋​

​storageclass​

​​時,minikube會自動一個hostpath本地pv

​​

​my-ghost/value.yaml​

​配置

當設定持久​

​enabled: false​

​,它為非持久化部署。

persistence:
  enabled: false      

9. 共享 Charts

Helm Charts 包開發完成了,如果别人想要使用我們的包,則需要我們共享出去,我們可以通過 Chart 倉庫來進行共享,Helm Charts 可以在遠端存儲庫或本地環境/存儲庫中使用,遠端存儲庫可以是公共的,如 Bitnami Charts 也可以是托管存儲庫,如 Google Cloud Storage 或 GitHub。為了示範友善,這裡我們使用 GitHub 來托管我們的 Charts 包。

我們可以使用 ​

​GitHub Pages​

​ 來建立 Charts 倉庫,GitHub 允許我們以兩種不同的方式提供靜态網頁:

  • 通過配置項目提供其​

    ​docs/​

    ​ 目錄的内容
  • 通過配置項目來服務特定的分支

這裡我們将采用第二種方法,首先在 GitHub 上建立一個代碼倉庫:​

​https://github.com/Ghostwritten/helm-demo​

​​,将上面我們建立的 ​

​my-ghost​

​ 包送出到倉庫 charts 目錄下,然後打包 chart 包:

$ helm package helm-demo/my-ghost
Successfully packaged chart and saved it to: /root/my-ghost-0.1.0.tgz      

我們可以将打包的壓縮包放到另外的目錄 ​

​repo/stable​

​ 中去,現在倉庫的結構如下所示:

$ tree .
.
├── charts
│   └── my-ghost
│       ├── Chart.yaml
│       ├── templates
│       │   ├── deployment.yaml
│       │   ├── _helpers.tpl
│       │   ├── ingress.yaml
│       │   ├── pvc.yaml
│       │   └── service.yaml
│       └── values.yaml
├── LICENSE
├── README.md
└── repo
    └── stable
        └── my-ghost-0.1.0.tgz

5 directories, 10 files      

執行如下所示指令生成 index 索引檔案:

helm repo index repo/stable --url https://raw.githubusercontent.com/cnych/helm101/main/repo/stable      

上述指令會在 ​

​repo/stable​

​​ 目錄下面生成一個如下所示的 ​

​index.yaml​

​ 檔案:

$ cat repo/stable/index.yaml 
apiVersion: v1
entries:
  my-ghost:
  - apiVersion: v2
    appVersion: 1.16.0
    created: "2022-03-30T21:39:37.202992379+08:00"
    description: A Helm chart for Kubernetes
    digest: 44d40c93d408f4d109a66b8ca61b14417c6d6b465cf636ae0a0767b73d5c6d13
    name: my-ghost
    type: application
    urls:
    - https://raw.githubusercontent.com/ghostwritten/helm-demo/main/repo/stable/my-ghost-0.1.0.tgz
    version: 0.1.0
generated: "2022-03-30T21:39:37.201867108+08:00"      

該 ​

​index.yaml​

​​ 檔案是我們通過倉庫擷取 Chart 包的關鍵。然後将代碼推送到 ​

​GitHub​

​​,​​如何學習本地項目上傳github請參閱​​:

$ yum -y install git
$ git config --global user.name "ghostwritten"
$ git config --global user.email "[email protected]"
$ git config list

#檢視 /root/.ssh/id_rsa.pub  并複制到github的設定頁面的SSH keys
$ cat /root/.ssh/id_rsa.pub  
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqgcbG1iD0m/6KWZPu4uJv+vM7ZBfrbnCib6egfz8+YGWMpTYnD8EgFZ1j/cH6G3odktP0ZjvUriFY+SZIYQtpIdvQ7ciG25HQC9WRREAchGbTcvyw0Jt4F2S5EJJVBzEYFwlhz2JH1iUqbzyPjyRH [email protected]      

并複制到github的設定頁面的SSH keys

如何開發一個完整的Helm charts應用執行個體
$ pwd
/root/helm-demo
$ git init 
$ git add *
$ git commit -m "add helm-demo"
$ git remote add origin  https://github.com/Ghostwritten/helm-demo.git
$ git push origin main      
如何開發一個完整的Helm charts應用執行個體

接下來為該倉庫設定 ​​

​GitHub Pages​

​​,首先在本地建立一個 ​

​gh-pages​

​​ 分支:

隻将 ​​

​repo/stable/index.yaml​

​ 檔案保留到根目錄下面,其他檔案忽略,然後推送到遠端倉庫:

$ tree
.
├── index.yaml
├── LICENSE
└── README.md

0 directories, 3 files      
$ git push origin gh-pages      

在 ​

​GitHub Pages​

​​ 頁面選擇使用 gh-pages 分支即可:

如何開發一個完整的Helm charts應用執行個體

現在我們就可以通過 ​​

​http://smoothies.com.cn/helm-demo/​

​ 來擷取我們的 Chart 包了。

使用如下所示指令添加 repo 倉庫:

$ helm repo add helm-demo http://smoothies.com.cn/helm-demo/
"helm-demo" has been added to your repositories      

我們也可以使用 ​

​helm search​

​​ 來搜尋倉庫中的 Chart 包,正常就包含上面我們的 ​

​my-ghost​

​ 了:

$ helm search repo helm-demo
NAME                CHART VERSION  APP VERSION  DESCRIPTION                
helm-demo/my-ghost  0.1.0          1.16.0      A Helm chart for Kubernetes      

接下來就可以正常使用 chart 包進行操作了,比如進行安裝:

helm install  hello-ghost  helm-demo/my-ghost      

繼續閱讀