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 把一個基底的 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。
例如我在上面這個網站,搜尋 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 功能都可以在官方文件上看到
Kustomize 搭配 Helm 的使用在官方文件上目前還是沒有完整的說明,可以參考原始碼旁的註解
最後
Kustomize 適合組織簡單的微服務的部屬清單,而 Helm 有 Template 功能適合做出可以適應多種部屬需求的 Chart,我目前透過 Argo CD 來管理我自己家裡的 Kubernetes 集群上的應用也是透過 Kustomize Helm 的組合技來使用,兩者互相補足對方的缺點,之後有機會在寫一篇有關 Argo CD 的文章介紹怎麼樣使用,那本篇文章就到這邊結束,希望大家有所收穫,謝謝大家。