使用變數與 Template,告別 YAML 地獄。
在深入實作之前,先認識一個 Kubernetes 界的超級好幫手——Helm。
簡單來說,Helm 就是 Kubernetes 的「套件管理工具」(Package Manager)。如果你用過 Linux,可以想像成 apt 或 yum;寫 Python 的話就像 pip;寫 Node.js 就像 npm。只不過 Helm 管的是 Kubernetes 應用程式。
在 K8S 部署一個完整應用,要寫一大堆 YAML(Deployment、Service、Ingress、ConfigMap...)。每次部署都手動改版本號、改密碼、改 Port,不僅眼花容易出錯,長期下來根本沒辦法維護。
values.yaml):會變動的值(映像檔版本、副本數、域名)抽出來統一放 values.yaml,部署時 Helm 自動填入所有模板。helm rollback 無痛退回上一版。沒有 Helm 的話,裝個 MySQL 要自己找一堆 YAML 複製貼上慢慢套用(光想就心累);有了 Helm 就是一行:
helm install my-database bitnami/mysql
回到我們的專案。helm/wafer-bi/ 的結構長這樣:
helm/wafer-bi/
├── Chart.yaml # Chart 的身分證(名稱、版本)
├── values.yaml # 預設參數(開發環境)
├── values-production.yaml # 生產環境覆寫
└── templates/
├── namespace.yaml
├── wafer-backend.yaml
├── wafer-frontend.yaml
├── user-service.yaml
├── api-gateway.yaml
├── postgres.yaml
├── ingress.yaml
├── network-policy.yaml
└── ...(共 19 個模板)
values.yaml 把所有會變的東西集中管理,例如:
waferBackend:
replicas: 1
image:
repository: ghcr.io/darkschneider1024/wafer-bi-backend
tag: a840fafd1ce53d38268e0f67ef001b4c2394cf09 # CI 會自動更新這行
pullPolicy: Always
resources:
requests:
cpu: 100m
memory: 256Mi
那模板長什麼樣?直接把 templates/wafer-backend.yaml 整份貼出來——它就是 Day 8 那份 Deployment,只是把會變的地方全部挖成變數:
apiVersion: apps/v1
kind: Deployment
metadata:
name: wafer-backend
namespace: {{ .Values.global.namespace }}
labels:
{{- include "wafer-bi.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.waferBackend.replicas }}
selector:
matchLabels:
app: wafer-backend
template:
metadata:
labels:
app: wafer-backend
env: {{ .Values.global.environment }}
annotations:
deployed-at: {{ now | date "2006-01-02 15:04:05" | quote }}
spec:
{{- if .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml .Values.global.imagePullSecrets | nindent 8 }}
{{- end }}
containers:
- name: backend
image: "{{ .Values.waferBackend.image.repository }}:{{ .Values.waferBackend.image.tag }}"
imagePullPolicy: {{ .Values.waferBackend.image.pullPolicy }}
ports:
- containerPort: 8000
resources:
{{- toYaml .Values.waferBackend.resources | nindent 10 }}
---
apiVersion: v1
kind: Service
metadata:
name: wafer-backend-svc
namespace: {{ .Values.global.namespace }}
labels:
{{- include "wafer-bi.labels" . | nindent 4 }}
spec:
selector:
app: wafer-backend
ports:
- protocol: TCP
port: 8000
targetPort: 8000
type: ClusterIP
短短四十幾行,其實把 Helm 模板語法該會的幾乎都用過一輪了。逐個拆:
① 最基本的取值:{{ .Values.xxx }}
{{ .Values.waferBackend.replicas }} 就是去 values.yaml 撈 waferBackend.replicas 的值填進來。image 那行則是把 repository 和 tag 兩個變數用冒號拼起來——這一行就是 Day 19 GitOps 閉環的關鍵零件:CI 只要改掉 values.yaml 裡的 tag,這裡渲染出來的 image 就變了,ArgoCD 比對到差異就會自動部署。
② 減號 {{- 是在修剪空白
{{- if ... }} 的減號代表「把左邊的空白和換行吃掉」。這不是龜毛,是 YAML 的生存問題——YAML 靠縮排決定結構,模板留下的空行和多餘縮排會直接讓渲染結果變成無效的 YAML。這也是寫 Helm 最容易卡住的地方,helm template 渲染出來一堆縮排錯誤,八成是減號沒放對。
③ include + nindent:共用片段與縮排控制
{{- include "wafer-bi.labels" . | nindent 4 }} 引用的是 templates/_helpers.tpl 裡定義的具名模板:
{{- define "wafer-bi.labels" -}}
app.kubernetes.io/part-of: wafer-bi
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end -}}
底線開頭的檔案(_helpers.tpl)不會被當成 K8S 資源渲染,專門放這種可重複使用的片段。19 個模板全都 include 同一段 labels,要改標籤策略只要改這裡一次。
nindent 4 的意思是「先換行,再把整段內容縮排 4 格」。因為 include 出來的是一段多行文字,直接塞進 YAML 會全部貼在同一層,縮排必須自己算好。
④ toYaml:整個區塊原封不動搬過去
resources:
{{- toYaml .Values.waferBackend.resources | nindent 10 }}
resources 底下有 requests / limits、各有 cpu / memory,共四個值。與其寫四行 {{ .Values... }},不如用 toYaml 把 values 裡那整個結構轉成 YAML 直接塞進來。好處是之後想在 values 加 ephemeral-storage,模板一個字都不用改。
⑤ {{- if }}:條件區塊
imagePullSecrets 只有在 values 有設定時才會出現。本機開發拉的是公開 image 不需要它,雲端拉私有 registry 才要——同一份模板同時服務兩種環境,這正是 Chart 的價值。
⑥ 函式與管線 |
{{ now | date "2006-01-02 15:04:05" | quote }} 讀作:取現在時間 → 格式化 → 加上引號。跟 shell 的 pipe 是同一個直覺,Helm 內建了幾十個這種函式(default、upper、b64enc、required…)。
順帶一提,
now這個看起來很貼心的設計後來變成一顆雷:每次渲染出來的值都不一樣,導致 ArgoCD 永遠覺得有差異。Day 17 會講怎麼用ignoreDifferences跟它和解。
⑦ --- 分隔多個資源
Deployment 和 Service 寫在同一個檔案,用 --- 隔開。習慣上會把「一個服務相關的資源」放在一起,比拆成 wafer-backend-deployment.yaml + wafer-backend-service.yaml 好維護。
講再多不如直接看結果。helm template 可以只渲染單一檔案(-s 參數),不用連叢集:
helm template wafer-bi helm/wafer-bi -s templates/wafer-backend.yaml
上面那份模板,套進 values.yaml 之後吐出來的 Deployment 是這樣(Service 部分省略):
apiVersion: apps/v1
kind: Deployment
metadata:
name: wafer-backend
namespace: k8sdemo
labels:
app.kubernetes.io/part-of: wafer-bi
app.kubernetes.io/managed-by: Helm
helm.sh/chart: wafer-bi-1.0.0
spec:
replicas: 1
selector:
matchLabels:
app: wafer-backend
template:
metadata:
labels:
app: wafer-backend
env: development
annotations:
deployed-at: "2026-08-12 10:32:27"
spec:
imagePullSecrets:
- name: ocirsecret
containers:
- name: backend
image: "ghcr.io/darkschneider1024/wafer-bi-backend:a840fafd1ce53d38268e0f67ef001b4c2394cf09"
imagePullPolicy: Always
ports:
- containerPort: 8000
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
所有 {{ }} 都不見了,剩下的是純粹的 K8S YAML——這就是 ArgoCD 實際拿去 apply 的東西(Day 17 會提到 ArgoCD 底下沒有 Helm release,就是因為它做的正是這件事)。對照幾個點:include 的 labels 攤成三行、toYaml 的 resources 完整長出四個值、{{- if }} 判定為真所以 imagePullSecrets 出現了、now 被算成一個具體時間。
最後看 Chart 真正的價值。同一份模板疊上 values-production.yaml 再渲染一次,然後跟開發環境版本 diff:
$ diff <(helm template wafer-bi helm/wafer-bi -s templates/wafer-backend.yaml) \
<(helm template wafer-bi helm/wafer-bi -f helm/wafer-bi/values.yaml \
-f helm/wafer-bi/values-production.yaml -s templates/wafer-backend.yaml)
32c32
< replicas: 1
---
> replicas: 2
40c40
< env: development
---
> env: production
48 行的模板一個字都沒改,換一個 values 檔就得到另一套環境的配置。 這就是 Day 10 開頭說的「告別 YAML 地獄」——地獄的定義不是 YAML 很多,是同一份 YAML 被複製成好幾份、然後開始各自演化。
Chart 寫完先體檢。helm lint 檢查語法與結構,helm template 在本地渲染出最終 YAML(不用連叢集,CI 裡跑超方便):

▲ helm lint 通過 + helm template 渲染出 34 個 K8S 資源
一個 Chart 渲染出 34 個 K8S 資源——想像一下沒有 Helm 的話,這 34 份 YAML 要手動維護,版本號要一個一個改……不敢想,哭啊。
幾個實用指令整理:
helm lint helm/wafer-bi # 語法體檢
helm template wafer-bi helm/wafer-bi # 本地渲染(乾看結果)
helm upgrade --install wafer-bi helm/wafer-bi # 安裝或升級(冪等,CI 最愛)
helm upgrade --install wafer-bi helm/wafer-bi \
-f helm/wafer-bi/values-production.yaml # 疊加生產環境參數
helm rollback wafer-bi 1 # 回滾到 revision 1
最後那行 helm rollback 先打個預防針:它的前提是「這個 release 是你自己用 helm upgrade --install 裝的」。等 Day 17 把部署交給 ArgoCD 之後,Helm 會退化成純粹的模板引擎,叢集裡不再有 release 歷史,這行指令會直接回你 release: not found——Day 20 講回滾時會把這個坑挖開來看。
我們的 Chart 裡有一張 ClusterIssuer(cert-manager 的資源,Day 11 SSL 憑證會用到)。如果目標叢集還沒裝 cert-manager,helm install 會直接報 no matches for kind "ClusterIssuer"——Helm 只認叢集裡已存在的 CRD。所以部署順序是:先裝 cert-manager,再裝我們的 Chart。這種「Chart 之間的隱形依賴」在拆模板時要特別留意。
從今天起,Wafer BI 的部署文件正式從「一堆散裝 YAML」升級成「一個版本化的 Chart」。明天來處理流量的門面:Nginx Ingress Controller 與 SSL 憑證,讓服務用漂亮的 HTTPS 域名見人。