一個服務上 K8s,你要寫 Deployment、Service、Ingress、ConfigMap、Secret、HPA、PDB、ServiceAccount、NetworkPolicy——九個 YAML 檔。三個服務就是 27 個。dev / staging / prod 三個環境就是 81 個。你不會想一個一個手寫的。Helm 就是來解決這個問題。

先講結論

Helm 是 K8s 的 apt-get / brew。Chart = 一組模板化的 YAML,values.yaml = 參數。helm install 一行就能部署 Prometheus + Grafana + Alertmanager + node-exporter,不用自己寫 20 個 YAML。自己寫的服務也能包成 Chart,用 values override 切換 dev / staging / prod。另一個選擇是 Kustomize(覆蓋式,不用模板語法),兩者各有適用場景。


Chart 結構

mychart/
├── Chart.yaml          # Chart 的 metadata(name, version, dependencies)
├── values.yaml         # 預設參數
├── templates/
│   ├── _helpers.tpl    # 共用 template 片段
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── hpa.yaml
│   └── serviceaccount.yaml
└── charts/             # dependency charts

Chart.yaml

apiVersion: v2
name: my-api
description: My API service Helm chart
type: application
version: 0.1.0        # Chart 版本
appVersion: "1.2.0"   # App 版本(顯示用)
dependencies:
  - name: postgresql
    version: ~15.0.0
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled

values.yaml(預設值)

replicaCount: 2
 
image:
  repository: myapp/api
  tag: "1.2.0"
  pullPolicy: IfNotPresent
 
service:
  type: ClusterIP
  port: 80
 
ingress:
  enabled: true
  className: nginx
  hosts:
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: api-tls
      hosts:
        - api.example.com
 
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi
 
autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
 
env:
  APP_ENV: production
  LOG_LEVEL: info
 
secrets:
  DATABASE_URL: ""    # 安裝時再指定
 
postgresql:
  enabled: false      # 預設不裝內建 DB

模板語法

templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-api.fullname" . }}
  labels:
    {{- include "my-api.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "my-api.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
      labels:
        {{- include "my-api.selectorLabels" . | nindent 8 }}
    spec:
      serviceAccountName: {{ include "my-api.serviceAccountName" . }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef:
                name: {{ include "my-api.fullname" . }}-config
          env:
            {{- range $key, $value := .Values.secrets }}
            {{- if $value }}
            - name: {{ $key }}
              valueFrom:
                secretKeyRef:
                  name: {{ include "my-api.fullname" . }}-secret
                  key: {{ $key }}
            {{- end }}
            {{- end }}
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            periodSeconds: 5
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

templates/_helpers.tpl

{{/*
完整名稱(release name + chart name)
*/}}
{{- define "my-api.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
 
{{/*
共用 labels
*/}}
{{- define "my-api.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
 
{{/*
Selector labels
*/}}
{{- define "my-api.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
 
{{/*
ServiceAccount name
*/}}
{{- define "my-api.serviceAccountName" -}}
{{ include "my-api.fullname" . }}-sa
{{- end }}

關鍵模板語法

語法用途範例
{{ .Values.xxx }}讀 values.yaml 的值{{ .Values.replicaCount }}
{{ include "name" . }}引用 _helpers.tpl{{ include "my-api.fullname" . }}
{{- if .Values.xxx }}條件判斷只在 enabled 時產出 YAML
{{- range }}迴圈迴圈產出多個 env var
{{ toYaml . | nindent N }}把 YAML 物件轉成文字並縮排resources 整塊搬進去
{{ sha256sum }}計算 hashConfigMap 改了自動觸發 rollout

常用指令

# 安裝
helm install my-release ./mychart \
  -f values-prod.yaml \
  --set secrets.DATABASE_URL="postgresql://..." \
  --namespace production \
  --create-namespace
 
# 升級(改 image tag 或 values)
helm upgrade my-release ./mychart \
  -f values-prod.yaml \
  --set image.tag="1.3.0"
 
# 回滾到上一版
helm rollback my-release 1
 
# 看歷史
helm history my-release
 
# 列出所有 release
helm list --all-namespaces
 
# 預覽 render 結果(不實際部署)
helm template my-release ./mychart -f values-prod.yaml
 
# 只看差異
helm diff upgrade my-release ./mychart -f values-prod.yaml

helm template 是最有用的 debug 工具。它把模板 render 成最終的 YAML 輸出,你可以檢查有沒有語法錯誤或值填錯。


常用社群 Chart

這些 Chart 一個指令就能裝好一整套基礎設施:

# Prometheus + Grafana + Alertmanager(見 06-k8s-monitoring)
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace
 
# Nginx Ingress Controller(見 02-k8s-networking)
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace
 
# cert-manager(自動 TLS 憑證)
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set crds.enabled=true
 
# Sealed Secrets(見 04-k8s-config-rbac)
helm install sealed-secrets sealed-secrets/sealed-secrets \
  --namespace kube-system
 
# ArgoCD(見 08-k8s-gitops)
helm install argocd argo/argo-cd \
  --namespace argocd --create-namespace

每一個都可以用 -f custom-values.yaml 覆蓋預設參數。不用從頭寫 20 個 YAML。


多環境管理

mychart/
├── values.yaml              # 預設值(共用的基礎設定)
├── values-dev.yaml          # dev override
├── values-staging.yaml      # staging override
└── values-prod.yaml         # production override

values-dev.yaml

replicaCount: 1
 
image:
  tag: "dev-latest"
 
ingress:
  hosts:
    - host: api-dev.example.com
      paths:
        - path: /
          pathType: Prefix
 
resources:
  requests:
    cpu: 50m
    memory: 64Mi
  limits:
    cpu: 200m
    memory: 256Mi
 
autoscaling:
  enabled: false

values-prod.yaml

replicaCount: 3
 
image:
  tag: "1.2.0"
 
ingress:
  hosts:
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: api-prod-tls
      hosts:
        - api.example.com
 
resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: "1"
    memory: 1Gi
 
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20
# 部署 dev
helm upgrade --install api-dev ./mychart -f values-dev.yaml -n dev
 
# 部署 prod
helm upgrade --install api-prod ./mychart -f values-prod.yaml -n production

Helm vs Kustomize

面向HelmKustomize
哲學模板化(Go template)覆蓋式(patch / overlay)
學習曲線中(要學模板語法)低(只要會寫 YAML)
社群生態巨大(幾千個 Chart)kubectl 內建
適合場景包裝成套件給別人用自己的服務多環境管理
缺點模板太複雜時很難讀不能產生新的資源結構

Kustomize 範例

k8s/
├── base/
│   ├── kustomization.yaml
│   ├── deployment.yaml
│   └── service.yaml
├── overlays/
│   ├── dev/
│   │   ├── kustomization.yaml
│   │   └── patch-replicas.yaml
│   └── prod/
│       ├── kustomization.yaml
│       └── patch-replicas.yaml
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
 
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
patches:
  - path: patch-replicas.yaml
namePrefix: prod-
namespace: production
 
# overlays/prod/patch-replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 5
# 預覽
kubectl kustomize overlays/prod
 
# 部署
kubectl apply -k overlays/prod

我的建議

  • 給社群/團隊共用的東西 → Helm Chart
  • 自己的服務多環境部署 → 都可以,看團隊偏好
  • 兩者可以混用:用 Helm 裝社群 Chart,用 Kustomize 管理自己的 YAML

常見踩坑

helm upgrade 失敗卡在 pending-upgradehelm rollback 回到上一版,修好再 upgrade。

values 層級搞混--set ingress.hosts[0].host=xxx 的陣列語法很容易寫錯。建議用 -f values.yaml 覆蓋,不要用 --set 處理複雜結構。

dependency chart 版本衝突Chart.lockChart.yaml 不一致。跑 helm dependency update 重新產生。

模板 render 出空白行:Go template 的 {{- 會吃掉前面的空白,-}} 吃掉後面的。善用這個語法避免輸出不必要的空行。


Helm 不完美——Go template 語法寫複雜了會想翻桌。但在 K8s 的世界裡,手寫 81 個 YAML 更想翻桌。挑你比較能接受的那個。


延伸閱讀


本系列文章