Kustomize K8S 原生的配置管理工具

Kustomize K8S 原生的配置管理工具

今天來介紹一下我在前綠色公司部屬 Kubernetes 應用程式很常使用的工具:Kustomize。

甚麼是 Kustomize ?

一般我們在部屬一些簡單的 Kubernetes 應用程式的時候,例如一個 Nginx 服務器,通常的做法就是把所有的 K8S 資源都寫在一個檔案裡像這樣:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app.kubernetes.io/name: nginx
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app.kubernetes.io/name: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx
spec:
  rules:
  - http:
      paths:
      - pathType: Prefix
        path: /
        backend:
          service:
            name: nginx
            port:
              number: 80

在應用程式還不多的情況下這樣子寫還可以,不過現在的應用程式基本上都是拆很多微服務,或是你一套微服務需要在測試、預覽、正式環境做部屬,那用這種單純的 yaml 檔案就只能一直複製貼上,長久下來很難管理,這個時候 Kustomize 就可以幫我們解決這個問題。

Kustomize - Kubernetes native configuration management

像上面的這張官網的示例圖片,透過 Kustomize 把一個基底的 yaml 檔案 patch 成兩個分別給 staging、production 環境使用,我們可以看到兩個環境主要差異在 replica 的數量,用 Kustomize 就只要寫出有差異的部分就可以直接把其他基底的 yaml 檔案內容複製過來並套用差異,下面會舉一個簡單的例子帶大家快速了解 Kustomize 有甚麼功能。

簡單的範例

這個範例的檔案大家可以在這裡下載自己試著改改看,稍微玩一下就能理解它運作的邏輯了。

安裝

brew install kustomize # mac
choco install kustomize # windows
kubectl apply -k ./folder # native support by kubectl

通用的目錄結構

在開始介紹前,先介紹一下通常在組織 Kustomize 檔案時會遵循的資料夾結構,具體如下。

  • base
    • 會是放基底的 yaml 檔,原則上就是每個環境都是相同的部分。
  • overlays
    • 放每個環境有差異的檔案引用或是 patch

base 資料夾

我們先來看 kustomize.yaml 檔案的內容:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./resources/deployment.yaml
- ./resources/service.yaml
- ./resources/ingress.yaml

基本上就是把文章一開始的單一個 yaml 檔案依照資源的種類拆分,然後在 resources 的字段引用它們。

接著我們在 base 目錄下執行 kustomize build 的命令,可以看到像這樣的輸出:

apiVersion: v1
kind: Service
metadata:
  name: nginx
...略
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: nginx
  name: nginx
...略
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx
spec:
  rules:
...略

基本上跟文章開頭的整份 yaml 是一樣的

overlays 資料夾

我們先看一下 overlay/test/kustomize.yaml 這個檔案

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: test
resources:
- ../../base

這邊可以看到我們在 resources 字段,利用相對路徑引用了 base 資料夾,只要像這樣子寫,base 資料夾下 kustomize.yaml 裡定義的所有資源都會被引用。
然後我們還額外寫了一個叫 namespace 的字段,這個是用來把每一個資源都更新成指定的 namespce。

接著我們在 overlays/test 目錄下執行 kustomize build 的命令,來看一下效果:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: test # 這裡被新增了!!!
spec:
...略

非常神奇,我只要寫一行 kustomize 就會自己幫我更新所有的資源,此外如果資源是 ClusterRoleBinding 你對應的 ServiceAccount 的 namespace 也會自動被更新,大家可以試試看。

更新 replicas

我們假設一個情境,我的 Production 環境需要高可用,所以我需要至少兩個以上的副本,那用 Kustomize 我可以這樣寫:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
resources:
- ../../base
replicas:
- name: nginx
  count: 2

然後生成的 yaml 會長這樣:

kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: nginx
  name: nginx
  namespace: prod
spec:
  replicas: 2 # replica 被更新了!!!
...略

透過 replicas 這個字段可以更新 Deployment、ReplicationController、ReplicaSet、StatefulSet 這四種資源,像這樣寫,我只要閱讀 kustomize.yaml 檔案就可以清楚知道開了幾個副本。

更新資源

另一個情況,我們在 Production 環境,需要給定容器需要消費的資源,那我們可以這樣子寫,首先創建一個檔案 overlays/prod/patchs/deployment-nginx.yaml 內容放你需要修改的資源中差異的部分。注意 apiVersion、kind、metadata.name字段不能省略,邏輯大概是你用肉眼要找到有差異部分途中的每個字段都要保留,而無關的部分 labels、selector 之類的就可以省略,具體如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
      - name: nginx
        resources: # 新增的部分
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 250m
            memory: 64Mi

接著,在 kustomize.yaml 中新增 patchesStrategicMerge 字段做引用,如下:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
resources:
- ../../base
replicas:
- name: nginx
  count: 2
patchesStrategicMerge: # 加這個
- ./patchs/deployment-nginx.yaml

最後,把整份檔案渲染出來看一下效果:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: nginx
  name: nginx
  namespace: prod
spec:
  replicas: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:1.14.2
        name: nginx
        ports:
        - containerPort: 80
        resources: # 我們新增的部分
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 250m
            memory: 64Mi
...略

引用設定檔

有時候我們會需要從設定檔掛載環境變數到容器內,例如資料庫的帳密、API 密鑰之類的。那我從 Kubernetes 官方文件常見的作法是跑一個這樣的指令 kubectl create secret generic prod-db-secret --from-literal=username=produser --from-literal=password=Y4nys7f11 去事先創建 Secret 然後再引用,這個指令很長每次都記不住,那我們一樣可以用 Kustomize 來自動幫我們產生 Secret。

先建立環境變數列表在 overlay/prod/configs/env.txt

username=admin
password=1234

接著更新 kustomize.yaml 添加 secretGenerator 字段,如下:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
resources:
- ../../base
replicas:
- name: nginx
  count: 2
patchesStrategicMerge:
- ./patchs/deployment-nginx.yaml
secretGenerator: # 新增這段
- name: env
  type: Opaque
  envs:
  - ./configs/env.txt

最後擴展一下前一節提到的 patchesStrategicMerge 的內容:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    spec:
      containers:
      - name: nginx
        envFrom: # 添加環境變數從 Secret 的引用
        - secretRef:
            name: env
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 250m
            memory: 64Mi

一樣渲染一下看效果:

apiVersion: v1 # Secret 自動從引用的檔案內容生成
data:
  password: MTIzNA==
  username: YWRtaW4=
kind: Secret
metadata:
  name: env-thg8697gm8 # 注意這個名稱
  namespace: prod
type: Opaque
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: nginx
  name: nginx
  namespace: prod
spec:
  replicas: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - envFrom:
        - secretRef:
            name: env-thg8697gm8 # 注意這個名稱
        image: nginx:1.14.2
...略

可以看到 Kustomize 自動幫我們生成了 Secret,眼尖的人可能會發現,產生的 Secret 名稱後面被加了一段後墜,這是 Kustomize 自動根據我們 secretGenerator 字段引用的檔案內容計算出來的亂數 hash,只要檔案內容改變 hash 也會改變並且自動更新到所有引用他們資源上。
以這個例子來說,只要我環境變數檔案改了,就會因為 hash 的改變觸發 Deployment 的滾動升級,這樣即使我的程式沒有實作自動重讀環境變數,也可以馬上套用新的變更。

Kustomize, Helm 的最後一哩路

看完前面簡單的例子後,我們來介紹一下我之前工作上真正大量使用 Kustomize 的地方。大家可能也聽過 Helm Chart 這個東西,甚至是比 Kustomize 還要熟悉,目前市面上要在 Kubernetes 中裝一些應用大家上網搜尋都會找到 Helm Chart。

Artifact Hub
Find, install and publish Kubernetes packages
nginx 13.2.9 · bitnami/bitnami
NGINX Open Source is a web server that can be also used as a reverse proxy, load balancer, and HTTP cache. Recommended for high-demanding sites due to its ability to provide faster content.

例如我在上面這個網站,搜尋 nginx 也可以找到對應的 Chart,那通常大家用 Helm Chart 就像是在 Ubuntu 上裝軟體一樣 Apt install 就可以了。Helm Chart 跟 Kustomize 不同的地方是它支援 Go Template 的樣板語言,支援迴圈或是條件判斷,可以組織出比較複雜的邏輯,使用者只需要輸入一個 Chart 作者定義好的 values.yaml 檔案,裡面有一些定義的字段,例如 image.tag 之類的,就能渲染出整個應用需要的 yaml 檔案,對比 Kustomize 功能上更全面,但是需要仰賴 Chart 作者有把可以提供訂製的字段開放出來才可以修改。

一個實際的例子

範例檔案,大家可以下載回去玩玩看,kustomize  會依賴電腦上裝的 helm 指令才可以使用,沒有裝要記得裝一下。

為甚麼需要 Kustomize 搭配 Helm 做使用,實際上當你安裝 Helm Chart 的時候有可能一些新版本的 Chart 配套的資源例如:Ingress 版本太新,你沒有辦法裝,這個時候你可以透過 Kustomize 來渲染你的 Chart 然後像前面提到的範例一樣,去上額外的 patch 來做你想要的更改,而不用把別人開發好的 Chart 自己分叉一個版本增加維護的負擔。

首先一樣創建一個 kustomize.yaml 檔案,這個範例就是純粹示範 helm 功能,就不創立 base, overlays 資料夾:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: cert-manager
helmGlobals:
  chartHome: ./charts # kustomize 會到下面聲明的 Chart Repo 下載 Chart 放到這個資料夾
helmCharts:
- repo: https://charts.bitnami.com/bitnami # Chart Repo 位置
  name: nginx # Chart 名稱
  version: 13.2.9 # Chart 版本
  releaseName: my-nginx # 同 helm install 的 release name
  namespace: default # 同 helm install 的 namespace
  valuesFile: ./values.yaml # helm install 的 values.yaml 檔案位置

接著我們創建一個 values.yaml 檔案,具體內容如下,我們裝完 nginx 要順便創立 ingress 資源:

ingress:
  enabled: true
  hostname: example.com

接著一樣用 Kustomize 指令渲染出來,注意要使用 Kustomize 渲染 Helm Chart 需要多加一個 --enable-helm 的 flag,像這樣 kustomize build --enable-helm

看一下渲染的結果:

apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/instance: my-nginx
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: nginx
    helm.sh/chart: nginx-13.2.9
  name: my-nginx
  namespace: default
spec:
  externalTrafficPolicy: Cluster
  ports:
  - name: http
    port: 80
    targetPort: http
  selector:
    app.kubernetes.io/instance: my-nginx
    app.kubernetes.io/name: nginx
  sessionAffinity: None
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/instance: my-nginx
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: nginx
    helm.sh/chart: nginx-13.2.9
  name: my-nginx
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/instance: my-nginx
      app.kubernetes.io/name: nginx
  strategy:
    rollingUpdate: {}
    type: RollingUpdate
  template:
    metadata:
      annotations: null
      labels:
        app.kubernetes.io/instance: my-nginx
        app.kubernetes.io/managed-by: Helm
        app.kubernetes.io/name: nginx
        helm.sh/chart: nginx-13.2.9
    spec:
      affinity:
        nodeAffinity: null
        podAffinity: null
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - podAffinityTerm:
              labelSelector:
                matchLabels:
                  app.kubernetes.io/instance: my-nginx
                  app.kubernetes.io/name: nginx
              namespaces:
              - default
              topologyKey: kubernetes.io/hostname
            weight: 1
      automountServiceAccountToken: false
      containers:
      - env:
        - name: BITNAMI_DEBUG
          value: "false"
        - name: NGINX_HTTP_PORT_NUMBER
          value: "8080"
        envFrom: null
        image: docker.io/bitnami/nginx:1.23.1-debian-11-r29
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 6
          initialDelaySeconds: 30
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: http
          timeoutSeconds: 5
        name: nginx
        ports:
        - containerPort: 8080
          name: http
        readinessProbe:
          failureThreshold: 3
          initialDelaySeconds: 5
          periodSeconds: 5
          successThreshold: 1
          tcpSocket:
            port: http
          timeoutSeconds: 3
        resources:
          limits: {}
          requests: {}
        volumeMounts: null
      hostIPC: false
      hostNetwork: false
      initContainers: null
      serviceAccountName: default
      shareProcessNamespace: false
      volumes: null
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  labels:
    app.kubernetes.io/instance: my-nginx
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: nginx
    helm.sh/chart: nginx-13.2.9
  name: my-nginx
  namespace: default
spec:
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          service:
            name: my-nginx
            port:
              name: http
        path: /
        pathType: ImplementationSpecific

可以看到渲染出來的結果,像是 namespace, labels 欄位都有照我們在 kustomize 裡設定的值去做渲染。

接著就來實際來解決我們遇到的問題:Kubernetes 版本太舊不支援 networking.k8s.io/v1 的 Ingress。

我們添加 patches 欄位在 kustomize.yaml 上:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
helmGlobals:
  chartHome: ./charts
helmCharts:
- repo: https://charts.bitnami.com/bitnami
  name: nginx
  version: 13.2.9
  releaseName: my-nginx
  namespace: default
  valuesFile: ./values.yaml
patches: # 添加這邊的內容
- target:
    kind: Ingress
    name: my-nginx
  patch: |-
    - op: replace
      path: /apiVersion
      value: networking.k8s.io/v1beta1
    - op: replace
      path: /spec/rules/0/http/paths/0/backend/service
      value:
        serviceName: my-nginx
        servicePort: http
    - op: remove
      path: /spec/rules/0/http/paths/0/pathType

上面的寫法是遵循 RFC6902 JSON Patch 的規格來寫,基本上就是對符合 patchs.#.target 的資源,套用 patchs.#.patch 裡定義的 patch 操作。下面是實際渲染出來的樣子:

...略
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  labels:
    app.kubernetes.io/instance: my-nginx
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: nginx
    helm.sh/chart: nginx-13.2.9
  name: my-nginx
  namespace: default
spec:
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          service:
            serviceName: my-nginx
            servicePort: http
        path: /

可以看到我們的 Ingress 照著我們寫的 JSON Patch 被改成舊版兼容的格式了。

總結

文件補充

除了上面提到的幾個例子之外,完整的 Kustomize 功能都可以在官方文件上看到

kustomization.yaml
kustomization.yaml fields and API

Kustomize 搭配 Helm 的使用在官方文件上目前還是沒有完整的說明,可以參考原始碼旁的註解

kustomize/chart.md at master · kubernetes-sigs/kustomize
Customization of kubernetes YAML configurations. Contribute to kubernetes-sigs/kustomize development by creating an account on GitHub.
kustomize/helmchartargs.go at master · kubernetes-sigs/kustomize
Customization of kubernetes YAML configurations. Contribute to kubernetes-sigs/kustomize development by creating an account on GitHub.

最後

Kustomize 適合組織簡單的微服務的部屬清單,而 Helm 有 Template 功能適合做出可以適應多種部屬需求的 Chart,我目前透過 Argo CD 來管理我自己家裡的 Kubernetes 集群上的應用也是透過 Kustomize Helm 的組合技來使用,兩者互相補足對方的缺點,之後有機會在寫一篇有關 Argo CD 的文章介紹怎麼樣使用,那本篇文章就到這邊結束,希望大家有所收穫,謝謝大家。