Hình dung một cửa hàng giữ một cuốn sổ ghi mọi lần nhập hàng: "hôm qua nhập 20 hộp sữa lên kệ số 3". Cuốn sổ ấy rất hữu ích — nó cho biết đáng ra trên kệ có gì. Nhưng nếu đêm qua ai đó lấy trộm sạch 20 hộp, cuốn sổ vẫn ghi 20 hộp. Nó không biết, vì nó chỉ chép lại việc đã làm, không phải việc đang có. Muốn biết kệ thật sự còn gì thì phải đi ra kệ mà đếm. Helm chính là cuốn sổ đó: helm status đọc lại biên nhận của chính nó, không nhìn vào cụm. Bài này đo hai thứ — Helm thêm gì vào manifest, và nó không biết gì.
Helm là cách phổ biến nhất để cài phần mềm vào Kubernetes.
Nguồn nhiều hơn kết quả
helm create sinh ra chart mẫu:
templates/*.yaml 219 dòng
values.yaml 161 dòng
---------
380 dòng nguồn
helm template biến chúng thành:
100 dòng YAML, 4 đối tượng
Tôi tưởng tỷ lệ đi theo chiều ngược lại — bản mẫu ngắn sinh ra nhiều manifest. Thực tế là 3,8 dòng nguồn cho mỗi dòng kết quả. Điều đó không có nghĩa Helm vô ích; nó có nghĩa giá trị của Helm không nằm ở chỗ viết ít đi. Giá trị nằm ở tham số hoá — cùng một chart cài được vào dev, staging, production với ba tệp values.yaml khác nhau.
Với một ứng dụng chỉ triển khai đúng một nơi, một tệp YAML 100 dòng đọc dễ hơn hẳn 380 dòng bản mẫu. Đây là câu hỏi nên tự đặt trước khi đóng gói mọi thứ thành chart.
Helm thêm gì
Mọi đối tượng nhận thêm năm nhãn và hai chú thích:
app.kubernetes.io/managed-by = Helm
app.kubernetes.io/instance = thu
app.kubernetes.io/name = thu
app.kubernetes.io/version = 1.16.0
helm.sh/chart = thu-0.1.0
meta.helm.sh/release-name = thu
meta.helm.sh/release-namespace = hm
Hai chú thích meta.helm.sh là cách Helm nhận lại đồ của mình. Cài chart lên một đối tượng đã tồn tại mà thiếu hai chú thích đó thì Helm từ chối, vì nó không dám giành quyền sở hữu thứ nó không tạo ra. Đó cũng là mẹo tiếp quản một đối tượng đang có: gắn tay hai chú thích và nhãn managed-by vào, rồi helm upgrade --install sẽ nhận nó.
Nhãn app.kubernetes.io/instance hữu dụng khi gỡ rối: nó cho biết đối tượng này thuộc release nào, kể cả khi nhiều release cùng một chart nằm chung namespace.
Trạng thái release nằm ở đâu
sh.helm.release.v1.thu.v1 kiểu helm.sh/release.v1 11.820 byte
Một Secret trong chính namespace đó. Bên trong là base64 hai lần rồi gzip rồi JSON:
kubectl get secret -n <ns> sh.helm.release.v1.<ten>.v1 \
-o jsonpath='{.data.release}' | base64 -d | base64 -d | gunzip | jq .
Các khoá: apply_method, chart, config, hooks, info, manifest, name, namespace, version.
Chú ý khoá chart: Helm giữ cả mã nguồn chart — đủ 9 tệp bản mẫu — chứ không chỉ giữ kết quả. Đó là lý do một Secret 1,9 KB manifest lại nặng gần 12 KB. Cũng vì vậy, helm rollback không cần tải lại chart từ đâu cả: mọi thứ cần thiết đã nằm trong cụm.
Mỗi lần upgrade thêm một Secret nữa — ba revision đo được ba Secret. Mặc định giữ 10 (--history-max). Chart lớn thì đây là một khoản chiếm chỗ đáng kể trong etcd, và nó âm thầm.
Chỗ thứ nhất Helm không biết: trôi cấu hình
Tôi xoá tay một Service do Helm tạo:
kubectl get svc -> 0
helm status -> STATUS: deployed
Helm vẫn báo mọi thứ ổn. Lý do đơn giản: Helm không phải bộ điều khiển. Nó không chạy trong cụm, không theo dõi gì cả. helm status chỉ đọc lại Secret release — tức là đọc lại cái Helm đã làm lần trước, không phải cái đang có thật. Đúng cuốn sổ ghi 20 hộp sữa.
helm upgrade dựng lại được Service, vì nó áp lại toàn bộ manifest. Nên hệ quả thực tế là: Helm một mình không chống được trôi cấu hình. Ai đó kubectl edit một Deployment do Helm quản lý thì thay đổi đó sống cho tới lần upgrade sau. Muốn tự động đồng bộ thì cần một bộ điều khiển GitOps (Argo CD, Flux) chạy thường trực — Helm chỉ là công cụ dựng manifest và bấm áp.
Cách phát hiện trôi bằng chính Helm:
helm get manifest <ten> -n <ns> > mong-muon.yaml
kubectl diff -f mong-muon.yaml
Chỗ thứ hai: uninstall để lại thứ nó không tạo
PVC khai thẳng trong bản mẫu thì helm uninstall xoá sạch — đo được 0 PVC còn lại. Nhưng với StatefulSet:
sau uninstall
StatefulSet -> bị xoá
PVC -> CÒN LẠI, vẫn Bound
PVC đó do bộ điều khiển StatefulSet tạo từ volumeClaimTemplates, không nằm trong manifest của Helm. Helm không biết nó tồn tại, nên không đụng tới. Đây là hành vi đúng — dữ liệu không nên biến mất theo một lệnh gỡ cài đặt — nhưng nó gây hai chuyện bất ngờ:
- Gỡ rồi cài lại cùng tên thì StatefulSet gắn lại đúng PVC cũ, kèm dữ liệu cũ. Rất tốt khi bạn muốn thế, rất khó hiểu khi bạn không.
- Namespace tích dần PVC mồ côi sau vài lần thử nghiệm, và chúng vẫn chiếm chỗ thật.
Cùng loại như vậy: CRD trong thư mục crds/ không bao giờ bị Helm xoá, và cũng không được cập nhật khi upgrade. Nâng cấp chart có CRD thì phải tự kubectl apply CRD trước.
Bốn lệnh đáng nhớ
helm template <ten> <chart> -f values.yaml # xem YAML thật, không cài
helm get manifest <ten> -n <ns> # cái đã cài lần trước
helm diff upgrade ... # plugin, xem trước khác biệt
helm upgrade --install --atomic --timeout 5m # hỏng thì tự lùi lại
helm template là thói quen nên có trước mọi lần cài chart lạ: nó cho xem chính xác cái gì sắp vào cụm. Chart của bên thứ ba có thể chứa ClusterRole rất rộng hoặc hostPath — đọc trước tốn hai phút.
--atomic đáng bật cho mọi lần upgrade ở production: hỏng giữa chừng thì tự rollback, thay vì để lại release ở trạng thái pending-upgrade mà lần sau không upgrade tiếp được.
Mẫu số chung: một bản ghi việc-đã-làm không phải là quan-sát-cái-đang-có
Hai chỗ mù của Helm là cùng một bài học đội hai lốt, và nó đúng ở khắp nơi. Lốt thứ nhất: trạng thái do một công cụ tự lưu là ảnh chụp ý định lúc nó hành động, không phải cửa sổ nhìn vào thực tại lúc này. Bất cứ công cụ nào báo cáo tình trạng bằng cách đọc nhật ký của chính nó — thay vì đi quan sát hệ thống đích — đều sẽ tự tin báo một con số cũ khi thực tại đã đổi. Khoảng cách giữa "sổ ghi" và "kệ hàng thật" chính là cấu hình trôi (drift), và chỉ một tác nhân liên tục quan sát mới khép được khoảng đó.
Trong Terraform, state file nói "resource này tồn tại", nhưng ai đó xoá nó trên console thì terraform plan chỉ lộ ra sau khi refresh đối chiếu với thực tại — không refresh thì state nói dối y như helm status. Với trình quản lý gói, cơ sở dữ liệu dpkg/rpm ghi "đã cài" dù bạn đã rm mất tệp; muốn biết thật phải dpkg --verify. Một giá trị được cache/memo hoá là ảnh chụp của nguồn tại một thời điểm, dùng tiếp sau khi nguồn đã đổi là phục vụ dữ liệu cũ. git status phải đọc cây làm việc thật mới đúng — nếu nó chỉ tin index thì cũng sai.
Lốt thứ hai: một công cụ chỉ quản được thứ chính nó tạo ra. PVC do bộ điều khiển StatefulSet sinh, CRD khai ngoài luồng — Helm không biết chúng, nên không dọn. Đây đúng là chuyện cascade delete của một ORM chỉ xoá được những bảng con nó có ánh xạ; hàng do trigger phía CSDL hay quan hệ không khai báo sẽ sống sót và làm bạn ngạc nhiên. rm -rf build/ không xoá thứ một bước build đã ghi ra chỗ khác.
Mẫu số chung: đừng tin một công cụ báo "ổn" khi cái nó đọc là bản ghi việc mình đã làm chứ không phải kết quả quan sát hệ thống — và đừng cho rằng gỡ/dọn bằng công cụ đó là sạch, vì nó chỉ thấy phần nó tạo ra. Muốn biết thực tại, luôn có một bước đối chiếu điều-mong-muốn với điều-đang-có: kubectl diff, terraform plan -refresh, dpkg --verify. Câu hỏi đúng không phải "công cụ báo gì" mà là "con số này đến từ quan sát thực tại hay từ trí nhớ của công cụ".
Muốn tự tìm release nào đang trôi khỏi cấu hình khai báo:
for r in $(helm list -A -q --output json 2>/dev/null | python3 -c '
import sys, json
for x in json.load(sys.stdin): print(x["namespace"] + "/" + x["name"])
'); do
ns=${r%%/*}; n=${r##*/}
d=$(helm get manifest "$n" -n "$ns" 2>/dev/null | kubectl diff -f - 2>/dev/null | wc -l)
[ "$d" -gt 0 ] && echo "$d dong khac biet: $r"
done
Vài dòng khác biệt là bình thường — HPA sửa replicas, bộ điều khiển thêm chú thích. Hàng trăm dòng thì có ai đó đã sửa tay và lần helm upgrade sau sẽ xoá sạch thay đổi đó mà không hỏi.
Phần sau: Kustomize — cùng bài toán tham số hoá, cách giải khác hẳn, và chỗ nó thắng Helm rõ rệt.