❤️ 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.

Luồng render template trong Helm

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.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

Ví dụ hàm template trong Helm

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?

Thứ tự ưu tiên values trong Helm

Đâ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.yaml của chart
  • values.yaml của parent chart nếu chart đang là subchart
  • file truyền qua -f hoặ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 đã

Quy trình debug template Helm

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.

Lỗi template Helm thường gặp và cách xử lý

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, nindent hoặc block if / range render 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 include hoặc quên define.

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.yaml rõ ràng, có comment vừa đủ. Key sâu quá sẽ khó đọc và dễ gõ nhầm.
  • Dùng default cho 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 ... | nindent khi 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 template mỗ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 .Capabilities thay 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.

Chia sẻ:
Bài viết đã được kiểm duyệt bởi AZDIGI Team

Về tác giả

Trần Thắng

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.

Hơn 10 năm phục vụ 80.000+ khách hàng

Bắt đầu dự án web của bạn với AZDIGI