[雲原生微服務架構](九)入門HELM

程式設計師的貓發表於2022-08-17

實際生產中,微服務專案可能有十幾個模組,若還需要進行安全訪問和控制,那麼需要建立諸如Role、ServiceAccount等資源。部署和版本升級時也往往需要修改或新增配置檔案中的一些引數(例如:服務佔用的CPU、記憶體、副本數、埠等),維護大量的yaml檔案極不方便。

那麼在CNCF的體系中是否存在這樣的強力“工具”,能夠簡化我們部署安裝過程呢?答案是存在的,Helm就是這樣一款工具。

二、什麼是Helm

作為CNCF的畢業專案。它的官方的定義是:Helm是一個為K8s進行包管理的工具。Helm將yaml作為一個整體管理並實現了這些yaml的高效複用,就像Linux中的yum或apt-get,它使我們能夠在K8s中方便快捷的安裝、管理、解除安裝K8s應用。

Helm基於go模板語言,使用者只要提供規定的目錄結構和模板檔案。在真正部署時Helm模板引擎便可以將其渲染成真正的K8s資源配置檔案,並按照正確的順序將它們部署到節點上。

Helm中有三個重要概念,分別為Chart、Repository和Release。

Chart代表中Helm包。它包含在K8s叢集內部執行應用程式,工具或服務所需的所有資源定義。可以類比成yum中的RPM。

Repository就是用來存放和共享Chart的地方,可以類比成Maven倉庫。

Release是執行在K8s叢集中的Chart的例項,一個Chart可以在同一個叢集中安裝多次。Chart就像流水線中初始化好的模板,Release就是這個“模板”所生產出來的各個產品。

Helm作為K8s的包管理軟體,每次安裝Charts 到K8s叢集時,都會建立一個新的 release。你可以在Helm 的Repository中尋找需要的Chart。Helm對於部署過程的最佳化的點在於簡化了原先完成配置檔案編寫後還需使用一串kubectl命令進行的操作、統一管理了部署時的可配置項以及方便了部署完成後的升級和維護。

三、Helm的架構

最新版本Helm的整體架構大致如下:

Helm客戶端使用REST+JSON的方式與K8s中的apiserver進行互動,進而管理deployment、service等資源,並且客戶端本身並不需要資料庫,它會把相關的資訊儲存在K8s叢集內的Secrets中。

Helm的目錄結構

假設我們的Chart名稱叫做myChart,我們可以使用命令:

$ Helm create myChart

建立一個初始模板工程,那麼在名為myChart的目錄下包含了以下目錄和檔案:

圖 1-1 Helm安裝包起始目錄結構

其中關鍵的目錄和檔案作用如下:

★ templates/ 目錄包含了模板檔案。Helm會透過模板渲染引擎渲染所有該目錄下的檔案來生成Chart,之後將收集到的模板渲染結果傳送給K8s。

★ values.yaml 檔案對於模板也非常重要。這個檔案包含了對於一個Chart的預設值 。這些值可以在使用者執行Helm install 或 Helm upgrade時指定新的值來進行覆蓋。

★ Chart.yaml 檔案包含對於該Chart後設資料描述。這些描述資訊可以在模板中被引用。

★ _helper.tpl 包含了一些可以在Chart中進行復用的模板定義。

★ 其他諸如deployment.yaml、service.yaml、ingress.yaml檔案,就是我們用於生成K8s配置檔案的模板,Helm預設會按照如下的順序將生成資源配置傳送給K8s:

Namespace -> NetworkPolicy -> ResourceQuota -> LimitRange -> PodSecurityPolicy --> PodDisruptionBudget -> ServiceAccount -> Secret -> SecretList -> ConfigMap -> StorageClass -> PersistentVolume -> PersistentVolumeClaim -> CustomResourceDefinition -> ClusterRole -> ClusterRoleList -> ClusterRoleBinding -> ClusterRoleBindingList -> Role -> RoleList -> RoleBinding -> RoleBindingList -> Service -> DaemonSet -> Pod -> ReplicationController -> ReplicaSet -> Deployment -> HorizontalPodAutoscaler -> StatefulSet -> Job -> CronJob -> Ingress -> APIService

四、Helm的命令

透過在Helm的客戶端中鍵入helm –help,我們可以看到如下截圖:

其中比較常用的命令如下:

★helm create 建立一個Helm Chart初始安裝包工程

★helm search 在Helm倉庫中查詢應用

★helm install 安裝Helm

★helm list 羅列K8s叢集中的部署的Release列表

★helm lint 對一個Helm Chart進行語法檢查和校驗

五、Helm Chart實戰

準備工作和目標

首先我們準備一個基於spring-boot工程的映象,我們無需關注工程本身的業務功能,重點介紹下這個映象在K8s叢集下資源配置。假設這個映象已經存在於K8s的映象倉庫中,映象名稱為cnp-order,標籤為1.0.0,如下圖所示

圖1-2 演示映象情況截圖

映象在K8s下的deployment.yaml配置檔案內容如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cnp-order
  namespace: paas-cnp
spec:
  # 此處為程式將要啟動的 pod 副本數量
  # 當副本為 1 時,pod 啟動在 K8S 的 master 節點上
  replicas: 1
  selector:
    # 此處為 K8S 的 selector label
    matchLabels:
      app: cnp-order
  template:
    metadata:
      labels:
        app: cnp-order
    spec:
      volumes:
        - name: order-application
          configMap:
            # 指定該 volume 對應到 K8S 叢集中相應的configMap name
            name: order-application
      containers:
        - name: cnp-order
          image: cnp-order:1.0.0
          imagePullPolicy: IfNotPresent
          # 宣告 volume 到 container 的卷載入資訊
          volumeMounts:
             # 此處對應 volume name 為 order-application 的卷載入資訊
            - mountPath: /apps/conf/cnp-order/application.yml
              name: order-application
              subPath: application.yml

從上面的配置中,我們可以看到這個deployment存在於名為paas-cnp的namespace下,並且spring-boot依賴的配置檔案application.yml被設定成了一個名為order-application的configmap,deployment掛載了這個configmap。同時,我們發現配置中一些配置值存在著對應關係(如volumes第一個的name和volumeMounts第一個的name), replicaCount被固定設定成了1(但它應該根據不同的環境在安裝時被修改)。

我們在K8s中部署安裝該應用過程是:首先建立namespace,再建立application.yaml檔案並修改其內容,生成建立configmap,最後再建立deployment。

我們的目標是透過Helm,把上述安裝過程簡化,同時更好的管理配置中存在關聯關係的配置值。最後,deployment的後期維護和升級也能透過Helm命令進行。

附上應用的application.yaml內容:

server:
  port: 9094
  servlet:
    context-path: /api/order/v1
spring:
  application:
    # 服務的例項名, 服務間的呼叫透過此名字呼叫
    name: cnp-order
  datasource:
    url: jdbc:mysql:// {ip}/cnp-order?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ********

備註:置中username和password只是用作示例說明

初始化工程語法分析

初始化工程中deployment.yaml的內容涵蓋了我們在製作Chart時會使用到的大部分語法,在開始創作我們的Chart前,我們有必要對其進行分析,瞭解基本的語法使用。deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myChart.fullname" . }}
  labels:
    {{- include "myChart.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "myChart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      labels:
        {{- include "myChart.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "myChart.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}

圖1-3紅框部分,在常規K8s配置檔案中containers的image值被替換成了模板變數{{ .Values.image.repository }},這是go模板指令語法,其中Values是Helm的內建物件,此處“.Values”可理解為工程內的values.yaml檔案,image.repository表示訪問其中image片段下的repository值。我們可以根據以上含義在values.yaml內找到圖1-4的片段,Helm在渲染時會把該變數替換成nginx。

圖1-3 示例配置片段1

圖1-4 values.yaml配置片段1

圖1-5紅框部分,我們觀察到其中的變數寫法變得更加的複雜,我們首先在values.yaml中找到resources的片段,如圖1-6所示。表示式前面還有一個指令toYaml,這是Helm中函式的用法,語法為“函式名 變數”,此處toYaml .values.resources表示把獲取到的變數以yaml的形式原封不動進行回顯(更多函式可以在官網查詢到),渲染後的結果應該為:

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 100m
    memory: 128Mi

這明顯不符合我們的期望,Helm中支援類似linux中管道的語法,即表示式中 | nindent 12部分,nindent 12是函式作用是在內容前面增加12個空格,toYaml .Values.resources | nindent 12表示把獲取到內容透過管道傳給nindent 12函式處理。所以整個表示式綜合起來渲染後的結果類似:

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 100m
    memory: 128Mi

而最前面的“-”表示去除表示式之前的空行。

圖1-5 例項配置片段2

圖1-6 values.yaml配置片段2

圖1-7紅框部分,include “myChart.selectorLabels”表示獲取_helper.tpl中myChart.selectorLabels模板定義的內容,透過圖1-8我們可以看到它返回的是一個類似:

app.kubernetes.io/name: {{ include "myChart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}

的格式,第二句中{{ .Release.Name }}表示獲取內建物件Release的名稱,這個值是在我們安裝Chart的時候命令列輸入的。第一句{{ include “myChart.name” . }}又是一個類似於1-7的語句,我們同樣可以在_helper.tpl中找到”myChart.name”的定義,如圖1-9所示。它的語句意思是首先使用values.yaml中的nameOverride的值作為返回值,若配置的nameOverride為空,則把當前Chart的Name屬性當做返回值。

圖1-7 例項配置片段3

圖1-8 _helper.tpl配置片段1

圖1-9 _helper.tpl配置片段2

開始創作

透過上一節的內容介紹,我們已經瞭解了Helm模板的一些基本語法,回顧我們在“準備工作和目標”章節中的目標:簡化安裝過程,同時更好的管理配置中存在關聯關係的配置值。最後,deployment的後期維護和升級也能透過Helm命令進行。既是把“準備工作和目標”章節中所列的K8s配置標紅部分,透過模板語法進行替換,再增加namespace.yaml模板配置,我們的Chart安裝包就製作完成了。

具體步驟:1. 首先刪除myChart工程下template/目錄下的所有目錄和檔案;2. 在template目錄下建立名為namesapce.yaml、configmap.yaml、deployment.yaml模板配置檔案,內容如下:

namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: {{ .Values.namespace }}

configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Values.volumes.application }}
  namespace: {{ .Values.namespace }}
data:
  application.yml: |
    server:
      port: {{ .Values.service.targetPort }}
      servlet:
        context-path: /api/order/v1
    spring:
      application:
        # 服務的例項名, 服務間的呼叫透過此名字呼叫
        name: {{ .Values.nameOverride }}
      datasource:
        url: jdbc:mysql://{{ .Values.config.mysql.server }}/cnp-order?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: {{ .Values.config.mysql.user }}
        password: {{ .Values.config.mysql.password }}

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.nameOverride }}
  namespace: {{ .Values.namespace }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      app: cnp-order 
  template:
    metadata:
      labels:
        app: cnp-order
    spec:
      volumes:
        - name: {{ .Values.volumes.application }}
          configMap:
            name: {{ .Values.volumes.application }}
      containers:
        - name: {{ .Values.nameOverride }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          volumeMounts:
            - mountPath: /apps/conf/cnp-order/application.yml
              name: {{ .Values.volumes.application }}
              subPath: application.yml

3. 編輯工程下的values.yaml檔案,內容如下:

replicaCount: 1
namespace: paas-cnp
volumes:
  application: order-application
image:
  repository: cnp-order
  # Overrides the image tag whose default is the Chart appVersion.
  tag: 1.0.0
  pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: cnp-order
fullnameOverride: ""
autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 5
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

nodeSelector:
  app: cnp-order

config:
  mysql:
    server: {ip}
    user: root
    password: root

除錯校驗

我們可以在myChart/同級目錄,使用命令:

$ Helm lint myChart/

進行語法校驗,若我們使用上述的所有配置,執行後結果如圖1-10所示:

圖1-10 語法校驗結果

我們在values.yaml中增加如下配置:

service:
  targetPort: 9094

再次執行Helm lint myChart/ 校驗透過,結果如圖1-11所示:

圖1-11 語法校驗透過圖

我們也可以透過命令:

$ Helm template myChart/ --output-dir ./result

該命令會在myChart同級目錄建立一個result目錄,該目錄內會存在模板渲染之後的配置檔案,我們可以透過檢查這些檔案內容結合命令列的提示資訊判斷錯誤發生位置。

接著,可以進行一個預安裝校驗,命令如下:

$ Helm install --dry-run --debug cnp-order ./myChart

預安裝會模擬正式的安裝流程,但不會在環境上真正部署安裝包。

安裝和後續升級

透過預安裝之後,當我們想要部署我們的工程時,只需要使用命令:

$ Helm install [release-name] myChart

即可完成應用的安裝了。

但有個遺留問題,values.yaml 中包含了預設的安裝引數,但是諸如資料庫的ip、username、password,若我們不想去修改安裝包,如何在安裝的時候進行覆蓋呢?我們只要在 install 時使用 set 選項,設定想要覆蓋的引數值即可。

$ Helm install myChart-test myChart--set config.mysql.server=100.71.32.11

使用者也可以在安裝時指定自己的values.yaml配置。例如使用者在升級的時候用 upgrade 命令,指定新的引數配置檔案,即可實現在原有部署好的應用的基礎上變更配置。命令如下:

$ Helm install myChart-test02 myChart -f my-values.yaml
$ Helm upgrade myChart-test02 myChart -f my-new-values.yaml

六、總結

我們可以看出Helm作為K8s的“yum工具”,確實可以解決前言背景所提到的使用者和實施在部署和升級應用時所遇到的問題,並且研發人員在K8s配置檔案已經存在的基礎上再進行Helm安裝包的製作,並不會有太多的改造量。透過Helm把一個應用所包含的yaml進行成套的管理,為我們後續安裝維護帶來了巨大的便利性。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
你還差得遠吶!

相關文章