Phần trước đo Helm và tìm ra tỷ lệ 380 dòng nguồn cho 100 dòng kết quả. Bài này đo Kustomize trên cùng bài toán — và phát hiện phép so sánh đó của tôi không công bằng.

So sánh số dòng, chỗ Kustomize không xoá được, và cơ chế băm ConfigMap

Đếm lại cho công bằng

Bài toán: một Deployment và một Service, hai môi trường khác nhau về tên, số bản, image và CPU.

Helm tối thiểu      37 dòng
Kustomize           41 dòng

Gần như bằng nhau.

Con số 380 dòng ở phần trước là của chart mẫu do helm create sinh ra — nó có sẵn Ingress, HPA, ServiceAccount, bản mẫu test và một tệp _helpers.tpl. So nó với một overlay Kustomize tối giản là so nhầm hai thứ khác nhau.

Tôi đã viết một chart Helm tối thiểu làm đúng việc Kustomize làm, và kết quả là hoà. Lập luận "Kustomize gọn hơn Helm" không đứng vững khi đo.

Điều đó cũng có nghĩa: chọn công cụ nào không nên dựa trên số dòng. Nó nằm ở chỗ khác.

Cấu trúc

base/
  deployment.yaml         YAML thật, dán vào cụm chạy được luôn
  service.yaml
  kustomization.yaml
overlays/dev/kustomization.yaml
overlays/prod/kustomization.yaml

Điểm khác biệt về triết lý: base/YAML hợp lệ, không phải bản mẫu. Mở ra đọc được, kubectl apply thẳng được, trình soạn thảo hiểu được lược đồ, kubeval kiểm được.

Chart Helm thì không: replicas: {{ .Values.replicas }} không phải YAML hợp lệ. Mọi công cụ đọc YAML đều phải chờ render xong.

Overlay prod trông thế này:

namePrefix: prod-
resources: [../../base]
labels:
  - pairs: {moi-truong: prod}
    includeSelectors: false
replicas:
  - {name: web, count: 3}
images:
  - {name: nginx, newTag: "1.27"}
patches:
  - target: {kind: Deployment, name: web}
    patch: |
      - op: replace
        path: /spec/template/spec/containers/0/resources/requests/cpu
        value: 500m

Một cái bẫy đo được ngay: viết newTag: 1.27 không có nháy thì Kustomize báo

cannot unmarshal number into Go struct field Image.images.newTag of type string

YAML đọc 1.27 là số. Thông báo lỗi khá rõ, nhưng với newTag: 1.27.0 thì lại đúng — nên lỗi chỉ xuất hiện ở đúng những phiên bản có hai phần.

includeSelectors: false cũng đáng nhớ: thiếu nó thì nhãn được thêm vào cả selector của Deployment, mà selector là trường không sửa được sau khi tạo. Lần áp thứ hai sẽ hỏng.

Chỗ Kustomize không làm được

Tôi bỏ service.yaml khỏi kustomization.yaml rồi áp lại:

kubectl apply -k  ->  service/dev-web VẪN CÒN

Kustomize không có khái niệm "release". Đo được 0 Secret trạng thái trong namespace — không có gì ghi lại lần áp trước.

Nên nó không biết lần trước nó đã tạo những gì, và không xoá được thứ bạn vừa bỏ khỏi khai báo. Helm thì xoá, vì manifest của lần trước nằm trong Secret release.

Đây là khác biệt thật sự lớn nhất giữa hai công cụ, và nó ít được nói tới hơn chuyện cú pháp.

Cách chữa là --applyset:

KUBECTL_APPLYSET=true kubectl apply -k overlays/dev -n kz \
  --prune --applyset=bo-ung-dung
service/dev-web pruned

Nhưng có điều kiện: nó chỉ xoá được thứ do chính applyset đó tạo ra. Tôi thử áp lên các đối tượng đã tồn tại từ trước khi bật applyset — chúng không bị đụng tới. Bật applyset cho một hệ thống đang chạy thì phải áp lại một lượt trước, rồi mới prune được.

Đổi lại, việc không có trạng thái là ưu điểm trong quy trình GitOps: Git là nguồn duy nhất, không có bản sao thứ hai trong cụm để lệch nhau.

Chỗ Kustomize thắng rõ rệt

app.env = "cai-dat=cu"    ->  name: dev-cauhinh-ghf892dkkm
app.env = "cai-dat=moi"   ->  name: dev-cauhinh-m79ffm444m

configMapGenerator băm nội dung rồi gắn vào tên. Và tham chiếu trong Deployment được viết lại theo:

envFrom:
  - configMapRef:
      name: dev-cauhinh-m79ffm444m

Nghĩa là đổi cấu hình thì tên ConfigMap đổi, pod template đổi, và Deployment tự cuộn lại.

Đây là lời giải cho một vấn đề thật: sửa ConfigMap thì pod đang chạy không tự nhận cấu hình mới — biến môi trường thì không bao giờ, tệp gắn vào thì phải chờ và ứng dụng phải tự đọc lại. Với Helm bạn phải tự thêm chú thích checksum/config vào pod template, và ai quên thì lỗi im lặng.

Điểm trừ: các ConfigMap cũ không tự biến mất. Vài chục lần triển khai là vài chục ConfigMap mồ côi, trừ khi bạn dùng --prune.

Chỗ Kustomize không định làm

Không có if, không có vòng lặp, không có hàm. Muốn "bật Ingress ở prod, tắt ở dev" thì phải để ingress.yaml trong overlay prod chứ không thể viết {{- if .Values.ingress.enabled }}.

Đó là lựa chọn có chủ ý, và nó có giá đúng nghĩa hai chiều:

  • Với 2–3 môi trường khác nhau ít, cách này rõ ràng hơn hẳn — đọc overlay là biết ngay môi trường đó có gì.
  • Với một chart cần phục vụ hàng nghìn người dùng khác nhau, không có điều kiện là không làm nổi. Đó là lý do gần như mọi phần mềm phân phối công khai đều dùng Helm.

Chọn cái nào

Tình huống Chọn
Cài phần mềm của người khác Helm — chart có sẵn
Ứng dụng nội bộ, vài môi trường Kustomize
Cần rollback bằng một lệnh Helm
GitOps với Argo CD / Flux Kustomize (cả hai đều hỗ trợ)
Chart phân phối cho người ngoài Helm

Và cách kết hợp hay dùng: Kustomize áp lên kết quả của Helm.

helmCharts:
  - name: ingress-nginx
    repo: https://kubernetes.github.io/ingress-nginx
    version: 4.11.3
    valuesFile: values.yaml
patches:
  - path: sua-tai-nguyen.yaml

Cách này lấy chart của người khác rồi sửa vài chỗ mà không cần fork chart, và cũng không cần chờ họ thêm một biến values mới.

Thử ba mươi giây

Xem overlay của bạn thật sự sinh ra cái gì, và khác gì so với cụm đang chạy:

kubectl kustomize overlays/prod | kubectl diff -f -

Chạy lệnh này trước mỗi lần áp. Nó bắt được hai lỗi hay gặp nhất mà apply không cảnh báo: patch trỏ vào target không khớp gì cả (im lặng không làm gì), và nhãn vô tình chui vào selector (chỉ nổ ra ở lần áp thứ hai).

Phần sau: Operator và CRD — đo chi phí thật của việc tự viết một bộ điều khiển.