❤️ 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.
Ở phần 3, mình đã đi qua cấu trúc của một Helm chart: Chart.yaml, values.yaml, templates/ và thư mục charts/. Sang phần này, mình tạm rời phần “anatomy” để chui vào chỗ thú vị hơn: engine render template. Đây là đoạn khiến Helm mạnh, nhưng cũng là chỗ dễ gây bối rối nhất nếu bạn mới làm quen với Go templating.
Nếu bạn từng nhìn vào một file Deployment của Helm và thấy đầy {{ }}, if, range, include, cảm giác bình thường thôi. Helm không khó vì YAML. Nó khó vì YAML cộng thêm logic render, scope biến và thứ tự override values. Hiểu được mấy mảnh ghép này thì bạn đọc chart người khác đỡ ngợp, tự viết chart cũng bớt cảnh sửa một dấu cách rồi ngồi nhìn YAML parse error như nhìn đề thi toán.

Built-in objects trong Helm: chỗ Helm đưa dữ liệu vào template
Khi Helm render file trong thư mục templates/, nó đưa vào một context gồm nhiều object có sẵn. Quan trọng nhất là .Values, nhưng ngoài nó ra còn có .Chart, .Release, .Template và .Capabilities. Nếu nắm được mấy object này, bạn sẽ hiểu vì sao cùng một chart nhưng khi cài ở cluster khác hoặc release name khác thì output lại khác nhau.
.Values
Đây là object được dùng nhiều nhất. Nó chứa toàn bộ values sau khi Helm hợp nhất từ values.yaml của chart, values từ parent chart, file -f và tham số --set. Bạn có thể hiểu nhanh là toàn bộ dữ liệu cấu hình mà chart được cấp lúc render.
# values.yaml
image:
repository: nginx
tag: "1.27.0"
service:
type: ClusterIP
port: 80
# templates/deployment.yaml
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
.Chart
Object này lấy dữ liệu từ Chart.yaml. Nó hữu ích khi bạn muốn đưa tên chart, version, appVersion vào labels hoặc annotations để trace manifest về đúng package gốc.
labels:
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
.Release
.Release chứa thông tin của lần cài đặt hiện tại, ví dụ .Release.Name, .Release.Namespace, .Release.Service. Dùng object này để sinh ra resource name, labels hoặc namespace-aware config.
metadata:
name: "{{ .Release.Name }}-web"
namespace: {{ .Release.Namespace }}
.Template
Object này cho biết file template hiện tại đang được render là file nào. Không phải chart nào cũng dùng, nhưng nó có ích khi debug hoặc ghi annotations.
annotations:
rendered-from: {{ .Template.Name | quote }}
.Capabilities
Đây là object nhiều người bỏ qua lúc mới học Helm. Nó cho bạn biết cluster đang hỗ trợ API nào, version Kubernetes nào. Nhờ vậy chart có thể render khác nhau tùy môi trường, ví dụ chọn networking.k8s.io/v1 hay API cũ hơn cho Ingress.
{{- if .Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress" }}
apiVersion: networking.k8s.io/v1
{{- else }}
apiVersion: networking.k8s.io/v1beta1
{{- end }}
ℹ️ Chỗ dễ nhầm nhất là scope của dấu chấm .. Khi bạn đi vào with hoặc range, dấu chấm có thể đổi context. Nếu cần quay lại root context, hãy lưu $ hoặc dùng $ trực tiếp.
Các hàm template dùng nhiều: viết gọn hơn, ít lỗi hơn

Helm dùng Go template và thêm nhiều hàm từ Sprig. Trong thực tế, bạn không cần thuộc hết. Có một nhóm đủ dùng cho phần lớn chart cơ bản và trung cấp.
default
Hàm này trả về giá trị mặc định nếu giá trị bên trái rỗng hoặc không tồn tại. Nó rất hợp cho các field không bắt buộc.
spec:
type: {{ .Values.service.type | default "ClusterIP" }}
quote
Dùng để ép output thành chuỗi có dấu ngoặc kép. Cái này nhỏ thôi nhưng cứu bạn khỏi nhiều lỗi YAML ngớ ngẩn, nhất là với version, port được truyền dưới dạng string hoặc giá trị chứa ký tự đặc biệt.
env:
- name: APP_VERSION
value: {{ .Chart.AppVersion | quote }}
nindent và toYaml
Đây là cặp bài trùng. toYaml biến map hoặc list thành YAML, còn nindent thêm xuống dòng và căn lề đúng mức. Nếu bạn render block như resources, nodeSelector, tolerations mà không dùng cặp này, kiểu gì cũng có lúc dính indent sai.
resources:
{{- toYaml .Values.resources | nindent 2 }}
if/else
Dùng để bật tắt block theo điều kiện. Ví dụ chỉ render Ingress nếu người dùng bật ingress.enabled.
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
{{- else }}
# ingress is disabled
{{- end }}
with
with giúp rút gọn đường dẫn object khi làm việc với một nhánh con. Nó cũng tự bỏ qua block nếu giá trị rỗng. Điểm cần nhớ là bên trong with, dấu chấm . đổi sang object mới.
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}
range
Dùng để lặp qua list hoặc map. Bạn sẽ gặp nó khi render nhiều env vars, nhiều port, nhiều hosts, nhiều volume mounts.
env:
{{- range .Values.env }}
- name: {{ .name | quote }}
value: {{ .value | quote }}
{{- end }}
Thứ tự ưu tiên values: cùng một key, ai thắng?

Đây là phần rất hay gây hiểu nhầm khi chart render ra giá trị khác với thứ bạn mong. Helm hợp nhất values theo thứ tự ưu tiên từ thấp lên cao như sau:
values.yamlcủa chartvalues.yamlcủa parent chart nếu chart đang là subchart- file truyền qua
-fhoặc--values - tham số truyền qua
--set,--set-string,--set-file
Nói ngắn gọn là tầng càng về sau càng có quyền ghi đè tầng trước. Nếu chart mặc định dùng ClusterIP nhưng bạn chạy:
helm install demo ./mychart \
-f values-prod.yaml \
--set service.type=LoadBalancer
thì service.type cuối cùng sẽ là LoadBalancer. File values-prod.yaml vẫn có tác dụng với các key khác, nhưng key bị --set chạm vào thì --set thắng.
💡 Với CI/CD, mình thường giữ cấu hình lớn trong file -f, còn chỉ dùng --set cho giá trị rất ngắn như tag image, release channel hoặc một cờ bật tắt. Làm vậy đỡ thành mớ command khó đọc.
Lab: render một chart nhỏ để nhìn rõ dữ liệu chạy như thế nào
Mình dùng Helm 3.15 trên macOS và một chart demo rất nhỏ. Mục tiêu của lab này không phải deploy app thật mà để nhìn rõ template được render ra sao.
helm create demo-web
cd demo-web
rm -f templates/tests/test-connection.yaml
Sửa values.yaml thành bản gọn hơn:
replicaCount: 2
image:
repository: nginx
tag: "1.27.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
env:
- name: LOG_LEVEL
value: info
- name: APP_MODE
value: demo
resources:
limits:
cpu: 250m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
Trong templates/deployment.yaml, bạn có thể chèn thêm vài dòng để dùng các function vừa nói:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
{{- range .Values.env }}
- name: {{ .name | quote }}
value: {{ .value | quote }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 6 }}
Render thử:
helm template demo ./
Khi nhìn output, bạn sẽ thấy các biểu thức {{ }} biến mất, thay bằng YAML thuần. Đây là điểm rất nên nhớ: Kubernetes không hiểu template Helm. Kubernetes chỉ nhận manifest đã render xong. Vì vậy lỗi của bạn thường thuộc 1 trong 2 nhóm, hoặc template render sai, hoặc manifest sau render không hợp lệ với Kubernetes.
Quy trình debug: đừng sửa mò, render ra trước đã

Khi chart lỗi, cách nhanh nhất là tách bài toán ra từng lớp. Mình thường đi theo thứ tự này.
1) Chạy helm lint trước
helm lint ./demo-web
helm lint bắt được khá nhiều lỗi cơ bản: chart metadata sai, template có vấn đề rõ ràng, values không hợp logic. Nó không thay thế hoàn toàn việc render, nhưng là bước lọc nhanh rất đáng làm.
2) Render chi tiết với helm template –debug
helm template demo ./demo-web --debug
Lệnh này hữu ích vì nó cho bạn thấy output cuối cùng, kèm thông tin debug bổ sung. Nếu YAML sau render bị lỗi indent hoặc field đặt sai vị trí, bạn nhìn ra ngay.
3) Dò values thực tế đang được dùng
Nhiều lỗi không nằm trong template mà nằm ở dữ liệu đầu vào. Ví dụ bạn tưởng resources là map, nhưng CI lại truyền thành string. Hoặc key nằm ở .Values.app.image.tag nhưng template lại gọi .Values.image.tag. Chỉ lệch một level là đủ nổ.
4) Kiểm tra scope khi dùng with và range
Đây là lỗi kinh điển. Ví dụ bên trong range .Values.env, dấu chấm . lúc này trỏ vào từng item env. Nếu bạn gọi .Release.Name trực tiếp trong đó, Helm sẽ không thấy vì context đã đổi. Cách sửa là dùng $.Release.Name.
{{- range .Values.env }}
- name: {{ .name | quote }}
value: {{ .value | quote }}
# muốn gọi root context thì dùng $
# release: {{ $.Release.Name | quote }}
{{- end }}
5) Dùng dry-run trước khi install thật
helm install demo ./demo-web --dry-run --debug
Lệnh này gần giống tình huống cài thật hơn. Nếu chart có hooks, lookup hoặc logic phụ thuộc release metadata, bạn sẽ nhìn ra sớm hơn so với chỉ dùng helm template.

Một vài lỗi thường gặp
- nil pointer evaluating interface {}: thường do key không tồn tại hoặc bạn đang ở sai scope.
- YAML parse error: đa phần là lỗi indent sau khi
toYaml,nindenthoặc blockif/rangerender ra. - wrong type for value: chart chờ map/list nhưng values truyền vào lại là string.
- template not found: gọi sai tên helper trong
includehoặc quêndefine.
Nâng cao hơn một chút: include, template và named templates
Khi chart bắt đầu có nhiều file, bạn sẽ không muốn lặp lại cùng một đoạn labels, fullname hay selector ở khắp nơi. Đây là lúc named templates phát huy tác dụng, thường được đặt trong templates/_helpers.tpl.
{{- define "demo-web.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end -}}
Sau đó gọi lại ở nơi khác bằng include:
metadata:
labels:
{{ include "demo-web.labels" . | nindent 4 }}
include thường linh hoạt hơn template vì nó trả về string, cho phép bạn pipe tiếp qua nindent, trim hoặc các function khác. template vẫn dùng được, nhưng trong chart thực tế, mình thấy include xuất hiện nhiều hơn.
⚠️ Named template nên đặt tên có prefix theo chart, ví dụ demo-web.labels thay vì chỉ labels. Làm vậy tránh đụng tên khi chart lớn hoặc có subchart.
Best practices để chart gọn và dễ debug
- Tách helper dùng chung vào
_helpers.tpl, đừng copy labels hoặc fullname nhiều lần. - Giữ
values.yamlrõ ràng, có comment vừa đủ. Key sâu quá sẽ khó đọc và dễ gõ nhầm. - Dùng
defaultcho field optional, nhưng đừng lạm dụng tới mức che mất lỗi cấu hình quan trọng. - Ưu tiên
include ... | nindentkhi chèn block YAML nhiều dòng. - Nếu một block có thể tắt mở, gom điều kiện bằng
ifở ngoài cùng thay vì rải nhiều điều kiện nhỏ. - Giữ tên helper theo namespace của chart, ví dụ
mychart.fullname,mychart.labels. - Render thử bằng
helm templatemỗi khi sửa template đáng kể. Đừng đợi đến lúc install mới biết chart hỏng. - Khi chart cần tương thích nhiều version Kubernetes, kiểm tra bằng
.Capabilitiesthay vì hardcode API version.
Nếu phần 3 giúp bạn đọc được cấu trúc chart, thì phần này giúp bạn đọc được “ý đồ” của chart lúc render. Hai phần đi cùng nhau. Còn ở phần 5, mình sẽ chuyển sang mảng thực chiến hơn: cách dùng public charts, thêm repo, tìm chart phù hợp và override values an toàn khi triển khai từ chart có sẵn.
FAQ về template debugging trong Helm
Khi nào nên dùng helm lint, khi nào nên dùng helm template –debug?
helm lint hợp để bắt lỗi nhanh ở mức chart. helm template --debug hợp để nhìn output YAML cuối cùng. Thường mình chạy cả hai, lint trước rồi render sau.
Vì sao chart render được nhưng apply vào cluster vẫn lỗi?
Vì Helm chỉ chịu trách nhiệm render template. Sau đó Kubernetes mới validate manifest. Có những manifest YAML hợp lệ nhưng sai schema của resource hoặc dùng API version cluster không hỗ trợ.
Lỗi nil pointer thường xử lý theo hướng nào?
Kiểm tra 3 thứ: key có tồn tại không, kiểu dữ liệu có đúng không, và dấu chấm . hiện tại đang trỏ vào đâu. Rất nhiều case là do đang ở trong range hoặc with nên context bị đổi.
include khác gì template?
include trả về chuỗi nên dễ pipe với nindent hoặc function khác. template render trực tiếp. Với YAML nhiều dòng, include thường tiện hơn.
Có nên nhét quá nhiều logic vào template Helm không?
Không nên. Template nên đủ để render linh hoạt, nhưng nếu logic quá rối thì chart sẽ khó debug, khó maintain. Phần lớn trường hợp, values rõ ràng và helper gọn gàng sẽ tốt hơn việc biến template thành một mini programming language.
Helm mạnh ở chỗ tái sử dụng và chuẩn hoá manifest. Muốn tận dụng được cái mạnh đó thì phải hiểu template engine của nó đang làm gì. Khi bạn quen mắt với .Values, default, toYaml, range, include và quy trình debug ở trên, việc đọc chart của người khác sẽ nhẹ đầu hơn hẳn.
Có thể bạn cần xem thêm
- Cấu trúc Helm Chart: Chart.yaml, values.yaml, templates và thành phần quan trọng (Phần 3/12)
- Tự đóng gói ứng dụng Node.js cơ bản thành Helm chart (Phần 6/12)
- Helm là gì? Vì sao Kubernetes vẫn cần Helm? (Phần 1/12)
- Cài đặt Helm và dựng lab Kubernetes local với kind/OrbStack (Phần 2/12)
- NodeJS Full-Stack trong Helm phần 1: Kiến trúc chart cho React/Next.js + Express (Phần 7/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.