❤️ AZDIGI chính thức cập nhật hệ thống blog mới hoàn chỉnh. Tuy nhiên có thể một số bài viết bị sai lệch hình ảnh, hãy ấn nút Báo cáo bài viết ở cuối bài để AZDIGI cập nhật trong thời gian nhanh nhất. Chân thành cám ơn.
Ở bài 1, mình đã nói về chuyện Helm giải quyết pain point nào của Kubernetes. Sang bài 2, mình dựng xong lab local với kind/OrbStack để có chỗ test lệnh thật. Đến bài này, mình sẽ tháo chart ra xem nó được lắp như thế nào, từ Chart.yaml, values.yaml cho tới templates/ và helper template.
ℹ️ Lab trong bài được verify bằng Helm 4.1.3, chart demo tạo trực tiếp bằng helm create anatomy-demo.
Khi mới học Helm, nhiều người thường nhảy thẳng vào viết template rồi vài phút sau bắt đầu thấy mọi thứ rối như canh hẹ: biến lấy ở đâu, label sinh từ đâu, file nào ảnh hưởng tới file nào, vì sao sửa values.yaml mà manifest render ra không như mong đợi, và tại sao có chart cài được còn có chart chỉ dùng để làm dependency.
Đó là lý do trước khi viết những template phức tạp hơn, bạn nên hiểu rõ anatomy của một Helm chart. Khi nắm được cấu trúc chart, bạn sẽ đọc chart người khác nhanh hơn, sửa scaffold do helm create sinh ra đỡ vỡ hơn, và quan trọng nhất là biết nên đặt logic ở đâu cho gọn.
Trong bài này, mình sẽ đi theo hướng thực hành: tạo chart mới bằng helm create, render thử bằng helm template, chỉnh một vài file để thấy rõ vai trò của từng phần, thêm values.schema.json để kiểm tra input, và cuối bài sẽ mở một chart public để xem cách người khác tổ chức chart thật ngoài đời.
Lab trong bài được verify bằng Helm 4.1.3, chart demo tạo trực tiếp bằng
helm create anatomy-demo.
Vì sao nên hiểu cấu trúc chart trước khi viết template phức tạp?
Helm chart không chỉ là một đống YAML đặt cạnh nhau. Nó là một package có quy ước rất rõ:
Chart.yamlmô tả chart là gì, version bao nhiêu, có dependency nào.values.yamllà nơi khai báo input mặc định.templates/là nơi sinh manifest Kubernetes._helpers.tpllà chỗ gom reusable template snippets.NOTES.txtlà thông báo sau khi install.charts/chứa chart dependency đã kéo về.crds/chứa CRD cần cài cùng chart.values.schema.jsongiúp validate input trước khi render/install.
Nếu chưa hiểu các vai trò này mà đã cố viết template phức tạp, bạn rất dễ gặp những lỗi kiểu:
- nhét quá nhiều logic vào một file trong
templates/ - hard-code giá trị thay vì lấy từ
values.yaml - lặp lại labels/names nhiều chỗ thay vì dùng helper
- tăng
appVersionnhưng quênversion, hoặc ngược lại - thêm dependency vào
Chart.yamlnhưng quên update để sinh racharts/
Nói ngắn gọn, hiểu anatomy của chart giúp bạn bớt sửa bằng cảm tính.
Bắt đầu lab: tạo chart mới bằng helm create

Tạo chart demo:
helm create anatomy-demo
Cấu trúc scaffold ban đầu Helm sinh ra sẽ gần như sau:
anatomy-demo/
├── Chart.yaml
├── values.yaml
├── charts/
└── templates/
├── NOTES.txt
├── _helpers.tpl
├── deployment.yaml
├── hpa.yaml
├── httproute.yaml
├── ingress.yaml
├── service.yaml
├── serviceaccount.yaml
└── tests/
Ngay sau khi tạo, mình render thử chart mặc định:
helm template anatomy-demo ./anatomy-demo
Output thực tế cho thấy Helm đã sinh sẵn ServiceAccount, Service, Deployment và pod test trong templates/tests/test-connection.yaml. Đây là điểm khá hay của helm create: nó không tạo chart “trắng tinh”, mà cho bạn một bộ khung đủ để nhìn thấy mối liên hệ giữa values.yaml, helper template và manifest cuối cùng.
Chart.yaml: căn cước của chart
File Chart.yaml trong lab ban đầu có các trường quan trọng:
apiVersion: v2
name: anatomy-demo
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.16.0"
Sau đó mình chỉnh thành:
apiVersion: v2
name: anatomy-demo
description: Demo chart để phân tích anatomy của một Helm chart
type: application
version: 0.2.0
appVersion: "1.27.0"
dependencies:
- name: common
version: 2.31.4
repository: https://charts.bitnami.com/bitnami
version vs appVersion khác nhau thế nào?
Đây là chỗ người mới rất hay nhầm.
version: version của chart package. Bạn sửa template, đổi default value, thêm dependency, đổi schema… thì thường tăng cái này.appVersion: version của ứng dụng được deploy. Ví dụ chart đang deploy NGINX 1.27.0 thì đặtappVersion: "1.27.0".
Trong output helm template, bạn sẽ thấy appVersion thường được đưa vào label app.kubernetes.io/version, còn version xuất hiện ở label kiểu helm.sh/chart: anatomy-demo-0.2.0.
Thực tế vận hành, nếu app không đổi nhưng bạn fix logic chart, thì chỉ tăng version là hợp lý. Nếu app đổi bản mới, đa phần bạn sẽ tăng cả hai, nhưng đó là hai khái niệm khác nhau.
type: application vs library
application: chart có thể render ra resource và cài được.library: chart không nhằm deploy trực tiếp, mà chủ yếu chứa helper/template dùng chung cho chart khác.
Trong lab này, chart demo là application. Ngoài ra mình có tạo thêm ví dụ file library chart tối giản:
apiVersion: v2
name: demo-lib
description: Helper library chart example
type: library
version: 0.1.0
appVersion: ""
Nếu bạn làm một bộ helper chung cho nhiều chart nội bộ, library là kiểu phù hợp. Còn chart deploy web app, database, exporter… thì hầu hết là application.
values.yaml: nơi nhận input mặc định

Nếu Chart.yaml là căn cước, thì values.yaml là bảng điều khiển đầu vào của chart.
Trong lab, mình chỉnh values.yaml để dễ minh hoạ hơn:
replicaCount: 2
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "1.27.0"
service:
type: ClusterIP
port: 80
uiMessage: "Demo anatomy chart"
config:
logLevel: info
featureFlags:
enableMetrics: true
Ý chính của values.yaml là: đừng bắt người dùng sửa trực tiếp template nếu chỉ muốn đổi vài tham số như image tag, replica, port, ingress hay resource.
Khi bạn đọc một chart có sẵn từ repo, values.yaml gần như luôn là nơi nên mở đầu tiên. Chỉ cần lướt file này là bạn biết chart đó cho phép tuỳ biến những gì, độ phức tạp ra sao, và triết lý thiết kế của tác giả có “friendly” với người vận hành hay không.
templates/: nơi Helm render manifest Kubernetes
Thư mục templates/ mới là nơi Helm dùng Go template để sinh ra manifest cuối cùng.
Trong chart scaffold mặc định, bạn sẽ thấy các file như:
deployment.yamlservice.yamlserviceaccount.yamlingress.yamlhpa.yamlhttproute.yamltests/test-connection.yaml
Để nhìn rõ mối liên hệ giữa values.yaml và manifest, mình thêm một ConfigMap mới:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "anatomy-demo.fullname" . }}
labels:
{{- include "anatomy-demo.labels" . | nindent 4 }}
data:
app-message: {{ .Values.uiMessage | quote }}
log-level: {{ .Values.config.logLevel | quote }}
metrics-enabled: {{ .Values.config.featureFlags.enableMetrics | quote }}
Sau đó render lại:
helm template demo-release ./anatomy-demo
Output thực tế cho thấy ConfigMap đã nhận đúng giá trị từ values.yaml:
data:
app-message: "Demo anatomy chart"
log-level: "info"
metrics-enabled: "true"
Đây là cách Helm làm việc hằng ngày: template lấy dữ liệu từ .Values, .Chart, .Release, rồi render ra YAML thuần để Kubernetes hiểu.
_helpers.tpl: chỗ gom những mảnh template dùng lại nhiều lần

Nếu bạn thấy trong các file template có những đoạn kiểu:
{{ include "anatomy-demo.fullname" . }}
{{ include "anatomy-demo.labels" . }}
thì định nghĩa của chúng nằm trong templates/_helpers.tpl.
File scaffold mặc định đã có sẵn các helper để sinh:
- tên chart
- fullname của release
- label chung
- selector labels
- tên service account
Trong lab, mình thêm một helper nhỏ để minh hoạ việc tái sử dụng label custom:
{{- define "anatomy-demo.tutorialLabels" -}}
azdigi.io/tutorial: helm-anatomy
app.kubernetes.io/part-of: helm-series
{{- end }}
rồi include nó vào deployment.yaml.
Kết quả render ra thực tế:
labels:
helm.sh/chart: anatomy-demo-0.2.0
app.kubernetes.io/name: anatomy-demo
app.kubernetes.io/instance: demo-release
app.kubernetes.io/version: "1.27.0"
app.kubernetes.io/managed-by: Helm
azdigi.io/tutorial: helm-anatomy
app.kubernetes.io/part-of: helm-series
Khi chart bắt đầu lớn, _helpers.tpl gần như là nơi cứu bạn khỏi việc copy-paste tên, label, annotation và logic fullname khắp mọi file.
NOTES.txt: file nhỏ nhưng hữu ích sau khi install
Nhiều người bỏ qua templates/NOTES.txt, nhưng đây là chỗ rất hợp để in hướng dẫn sau khi helm install xong.
Mình đổi NOTES.txt thành nội dung đơn giản hơn:
1. Chart name: {{ .Chart.Name }}
2. Chart version: {{ .Chart.Version }}
3. App version: {{ .Chart.AppVersion }}
4. Release name: {{ .Release.Name }}
Kiểm tra nhanh manifest đã render:
helm template {{ .Release.Name }} {{ .Chart.Name }}
Kiểm tra ConfigMap sau khi install:
kubectl get configmap {{ include "anatomy-demo.fullname" . }} -o yaml
Với chart nội bộ trong team, NOTES.txt rất đáng dùng để nhắc lệnh kubectl port-forward, URL truy cập, hoặc bước verify sau deploy.
charts/: nơi dependency được kéo về

Trong Chart.yaml, mình thêm dependency tới Bitnami Common:
dependencies:
- name: common
version: 2.31.4
repository: https://charts.bitnami.com/bitnami
Sau đó chạy:
helm dependency update
Helm sẽ tải dependency về thư mục charts/. Ý nghĩa thực tế của charts/ là chart của bạn không phải lúc nào cũng đứng một mình. Nó có thể dùng thư viện helper, subchart, hoặc phụ thuộc một chart khác.
Người mới hay thêm dependency xong rồi quên update, dẫn tới chart không package/render đúng như mong muốn. Cứ nhớ một quy tắc đơn giản: sửa dependency trong Chart.yaml thì chạy lại helm dependency update.
crds/: nơi dành cho CustomResourceDefinition
Nếu chart của bạn liên quan operator hoặc custom resource, crds/ là thư mục cần biết.
Trong lab, mình tạo file crds/demodatabases.yaml là một CRD mẫu DemoDatabase.
Khi render thông thường bằng helm template demo-release ./anatomy-demo, bạn sẽ chưa thấy CRD. Nhưng khi thêm --include-crds:
helm template demo-release ./anatomy-demo --include-crds
output đầu tiên sẽ là:
# Source: anatomy-demo/crds/demodatabases.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: demodatabases.demo.azdigi.local
Điểm cần nhớ là crds/ không hoạt động giống templates/. Đây là khu vực đặc biệt của Helm, nên khi đọc chart có sẵn, nếu liên quan operator mà bạn không thấy CRD trong templates/, hãy kiểm tra crds/ trước.
values.schema.json: lớp bảo vệ rất đáng dùng
Đây là phần nhiều chart nhỏ bỏ qua, nhưng thật ra rất hữu ích, nhất là khi chart có nhiều input.
Mình thêm values.schema.json để ép:
replicaCountphải là integer và >= 1service.typechỉ được làClusterIP,NodePort,LoadBalancerservice.portphải nằm trong khoảng hợp lệconfig.logLevelchỉ nhậndebug,info,warn,error
Sau đó thử cố tình truyền sai:
helm template demo-release ./anatomy-demo --set service.port=70000
Helm trả lỗi ngay trước khi render/install:
values don't meet the specifications...
- at '/service/port': maximum: got 70,000, want 65,535
Đây là lợi ích rất thực tế của schema: chặn lỗi từ đầu, thay vì đợi đến lúc manifest apply fail hoặc ứng dụng chạy sai mới đi debug.
Cách đọc một chart có sẵn từ repo để hiểu nó
Để bài không chỉ xoay quanh chart tự tạo, mình kéo chart public bitnami/nginx về bằng:
helm pull bitnami/nginx --untar --untardir public-charts
Cấu trúc của chart này cho thấy một chart ngoài đời thật sẽ “đậm đặc” hơn scaffold rất nhiều:
- có
Chart.yaml,values.yaml,templates/ - có
charts/commonlà dependency đã được kéo về - có nhiều template hơn như
networkpolicy,pdb,servicemonitor,prometheusrules
Khi đọc chart public, mình thường đi theo thứ tự này:
- **Mở
Chart.yaml** để xem chart làm gì, version nào, có dependency gì. - **Mở
values.yaml** để xem các knob có thể chỉnh. - **Mở
_helpers.tpl** để hiểu naming convention, labels, helper logic. - **Mở file template chính** như
deployment.yaml,service.yaml,ingress.yaml. - **Render thử bằng
helm template** với vài--setphổ biến.
Trong Chart.yaml của Bitnami NGINX, mình thấy ngay những thông tin đáng chú ý:
appVersion: 1.29.7version: 22.6.10- dependency
common - source repo rõ ràng
Chỉ nhìn vậy thôi là bạn đã hiểu chart này tách rõ version chart và version app, đồng thời dùng chung thư viện helper của Bitnami.
Những lỗi người mới hay gặp khi sửa chart scaffold
Sau khi lab với chart anatomy-demo, có vài lỗi rất điển hình:
1. Sửa values.yaml nhưng template không đọc giá trị đó
Khai báo trong values.yaml không tự động có tác dụng. Template phải tham chiếu đúng .Values.xxx thì manifest mới đổi.
2. Hard-code tên resource
Nếu bạn viết thẳng tên trong deployment.yaml thay vì dùng helper như {{ include "anatomy-demo.fullname" . }}, chart sẽ khó tái sử dụng và dễ đụng tên khi đổi release.
3. Nhầm version với appVersion
Cập nhật image tag nhưng lại chỉ tăng version, hoặc sửa template mà lại chỉ tăng appVersion. Hai trường này liên quan nhưng không thay nhau được.
4. Thêm dependency nhưng quên helm dependency update
Kết quả là charts/ chưa có nội dung mới, chart package không đồng bộ với Chart.yaml.
5. Quên validate input
Chart ban đầu chạy ổn với default value, nhưng khi người khác truyền --set linh tinh thì phát sinh lỗi khó hiểu. values.schema.json giúp giảm mạnh kiểu lỗi này.
6. Nhét quá nhiều logic vào một template file
Khi logic name/label/annotation lặp đi lặp lại, hãy đưa sang _helpers.tpl. Đừng biến deployment.yaml thành bãi chiến trường.
7. Không render thử trước khi cài
Thói quen tốt là luôn chạy ít nhất một trong các lệnh sau trước khi install/upgrade:
helm lint ./chart-name
helm template release-name ./chart-name
Lab này mình cũng đã chạy helm lint và chart pass bình thường.
Kết luận
Một Helm chart nhìn qua có vẻ đơn giản, nhưng mỗi file trong đó đều có vai trò riêng. Khi hiểu Chart.yaml, values.yaml, templates/, _helpers.tpl, NOTES.txt, charts/, crds/ và cả values.schema.json, bạn sẽ thấy Helm dễ kiểm soát hơn nhiều.
Điểm quan trọng nhất không phải là nhớ định nghĩa từng file như học thuộc lòng, mà là hiểu luồng làm việc:
- chart metadata nằm ở đâu
- input đi vào ở đâu
- manifest được render ở đâu
- logic dùng chung nên để ở đâu
- dependency và CRD nằm chỗ nào
- validate đầu vào bằng cách nào
Khi nắm anatomy của chart, bạn sẽ đọc chart người khác nhanh hơn, sửa chart scaffold tự tin hơn và viết template phức tạp theo cách gọn gàng hơn.
Ở bài 4, mình sẽ đi tiếp vào phần thực chiến hơn: cách viết template Helm “đúng bài” với các hàm thường dùng, pipeline xử lý giá trị, điều kiện if/else, vòng lặp range, và những mẹo debug khi template render ra không như mong đợi.
—
Assets từ lab thực tế
Các asset/output đã lưu trong workspace để tiện trích dẫn hoặc chụp màn hình:
temp/helm-anatomy-lab/assets/01-include-crds.txttemp/helm-anatomy-lab/assets/02-schema-validation.txttemp/helm-anatomy-lab/assets/03-helm-lint.txttemp/helm-anatomy-lab/assets/04-bitnami-nginx-structure.txttemp/helm-anatomy-lab/assets/05-bitnami-nginx-chart.yaml.txttemp/helm-anatomy-lab/assets/06-bitnami-nginx-values-head.txttemp/helm-anatomy-lab/assets/07-library-chart-example.yaml
Mã nguồn lab:
temp/helm-anatomy-lab/anatomy-demo/- Bài viết nháp hoàn chỉnh:
temp/helm-anatomy-lab/article.md
FAQ về cấu trúc Helm Chart
Chart.yaml dùng để làm gì trong Helm chart?
Chart.yaml chứa metadata của chart như tên chart, version, appVersion, type và dependency. Đây là file đầu tiên nên mở khi bạn muốn hiểu một chart đang làm gì.
values.yaml khác gì với templates/?
values.yaml là nơi khai báo giá trị đầu vào mặc định, còn templates/ là nơi dùng các giá trị đó để render ra manifest Kubernetes. Nôm na là một bên giữ input, một bên sinh output.
_helpers.tpl có bắt buộc không?
Không bắt buộc, nhưng gần như chart nào nghiêm túc cũng nên có. File này giúp gom logic lặp lại như fullname, labels, selectorLabels hoặc annotation để tránh copy-paste khắp nơi.
Khi nào nên dùng values.schema.json?
Nên dùng ngay khi chart có nhiều input hoặc được nhiều người cùng dùng. values.schema.json giúp validate giá trị trước khi render hoặc cài đặt, từ đó chặn lỗi sớm hơn nhiều.
Có thể bạn cần xem thêm
- Templates, Values, Functions và Debugging: Làm chủ Go templating trong Helm (Phần 4/12)
- Helm là gì? Vì sao Kubernetes vẫn cần Helm? (Phần 1/12)
- NodeJS Full-Stack trong Helm phần 1: Kiến trúc chart cho React/Next.js + Express (Phần 7/12)
- Cài đặt Helm và dựng lab Kubernetes local với kind/OrbStack (Phần 2/12)
- Tự đóng gói ứng dụng Node.js cơ bản thành Helm chart (Phần 6/12)
- Deploy ứng dụng từ public repository: Bitnami, Artifact Hub và best practices (Phần 5/12)
Về tác giả
Trần Thắng
Chuyên gia tại AZDIGI với nhiều năm kinh nghiệm trong lĩnh vực web hosting và quản trị hệ thống.