Package Management & Templating (Quản lý gói & Tạo template)Package Management & Templating
Thuộc knowledge base theo Kubernetes Roadmap.
Tổng quan
Manifest Kubernetes thô là YAML tĩnh: một Deployment, một Service, một ConfigMap, mỗi cái hard-code giá trị của một môi trường. Điều đó ổn với một demo và sụp đổ ngay khi bạn có dev, staging và production — bạn kết cục với ba bản gần như y hệt của mỗi file, dần lệch nhau qua từng lần sửa tay, và tác vụ “đổi image tag trong tất cả môi trường” nghĩa là sửa cả tá file mà không được sót cái nào. Package management và templating tồn tại để giải chính vấn đề đó: giữ một nguồn sự thật (source of truth) và dẫn xuất các biến thể theo môi trường, theo release từ nó — nguyên tắc DRY áp dụng cho hạ tầng.
Hai công cụ thống trị, với hai triết lý về căn bản khác nhau. Helm là một công cụ templating và package manager: nó coi một tập manifest như một chart được versioned, tham số hóa được, với các placeholder Go-template ({{ .Values.image.tag }}) điền từ một values.yaml, và nó cài đặt chart thành các release được theo dõi mà bạn có thể upgrade và rollback. Kustomize là cấu hình overlay không template: nó lấy YAML thuần, hợp lệ làm base và áp các patch declarative trong các overlay theo môi trường, không cần ngôn ngữ templating nào — và nó được tích hợp thẳng vào kubectl. Helm là “tham số hóa bằng biến và phân phối như một gói”; Kustomize là “bắt đầu từ YAML thật và xếp lớp khác biệt lên trên.”
Không cái nào chắc chắn tốt hơn — chúng giải các vấn đề chồng lấn nhưng khác biệt, và các team trưởng thành thường kết hợp chúng (Helm cho các dependency bên thứ ba bạn cài, Kustomize để tinh chỉnh manifest của chính mình, hoặc pipeline helm template | kustomize). Ngoài hai cái này còn có các ngôn ngữ giàu hơn — jsonnet (với dòng dõi Tanka/ksonnet) và ytt (Carvel) — dùng khi team muốn cấu trúc lập trình thật (hàm, import, giá trị tính toán) thay vì template hay patch. Trang này bàn khi nào và cách dùng mỗi cái.
Vì sao không cứ copy YAML cho mỗi môi trường?
Cách copy-paste thất bại theo kiểu đoán trước được: giá trị lệch nhau (staging và prod âm thầm phân kỳ), thay đổi dễ lỗi (bạn quên một file), và không có khái niệm release nào (không có “cài toàn bộ app” atomic, không có rollback versioned). Templating cho bạn một định nghĩa chuẩn tắc duy nhất cộng một diff nhỏ, review được về những gì thực sự thay đổi theo môi trường — biến “trong 40 file YAML của tôi cái nào sai?” thành “đây là 6 giá trị khác biệt ở prod.”
Kiến thức nền tảng
Helm: charts, values, releases
Helm là package manager Kubernetes được dùng rộng rãi nhất. Các trừu tượng cốt lõi:
- Chart — một thư mục được đóng gói gồm các manifest được template cộng metadata. Nó là đơn vị phân phối (như một
.rpm/.debhay một gói npm cho Kubernetes). - Template — một file manifest dưới
templates/chứa các chỉ thị Go template tham chiếu tới values, các object dựng sẵn (.Release,.Chart,.Capabilities), và các hàm helper (thư viện Sprig:default,quote,toYaml,include). - Values — các tham số.
values.yamlgiữ mặc định; người dùng override bằng-f myvalues.yamlhoặc--set key=value. Thứ tự ưu tiên:--set> các file-f(cái sau thắng) > mặc định của chart. - Release — một lần cài đặt cụ thể của một chart vào một cluster, với một tên và một lịch sử revision. Helm lưu trạng thái release dưới dạng một Secret trong cluster, đó là thứ khiến
helm rollbackkhả thi. - Repository — một HTTP server (hoặc OCI registry) chứa các chart
.tgzđã đóng gói và mộtindex.yaml.helm repo add/helm pullfetch từ nó.
Cấu trúc thư mục chart:
myapp/
├── Chart.yaml # name, version, appVersion, dependencies
├── values.yaml # tham số mặc định
├── charts/ # các chart dependency được vendor (subchart)
├── crds/ # CRD được cài trước templates
└── templates/
├── _helpers.tpl # các snippet named template tái sử dụng
├── deployment.yaml
├── service.yaml
├── ingress.yaml
└── NOTES.txt # thông báo sau khi cài
# templates/deployment.yaml — một template của chart
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
# values.yaml — mặc định, được override theo từng môi trường
replicaCount: 2
image:
repository: myorg/myapp
tag: "" # rỗng → dùng Chart.AppVersion
service:
port: 8080
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 256Mi }
Các lệnh Helm thiết yếu:
helm repo add bitnami https://charts.bitnami.com/bitnami # thêm một repo
helm search repo postgres # tìm chart
helm install myrel ./myapp -f prod-values.yaml # cài một release
helm upgrade --install myrel ./myapp -f prod-values.yaml # idempotent: cài-hoặc-upgrade
helm template myrel ./myapp -f prod-values.yaml # render cục bộ, không cần cluster (tuyệt cho CI/diff)
helm lint ./myapp # kiểm tra chart
helm list # liệt kê release
helm history myrel # lịch sử revision
helm rollback myrel 3 # rollback atomic về revision 3
helm diff upgrade myrel ./myapp # (plugin) xem trước thay đổi trước khi áp
helm uninstall myrel
helm upgrade --install là lệnh idempotent chủ lực: nó cài release nếu chưa có và upgrade nếu đã có — an toàn để chạy lặp lại trong CI/CD mà không cần logic rẽ nhánh.
Helm hooks và chart dependencies
Hooks cho phép một chart chạy các resource tại các điểm định trước trong vòng đời release qua annotation helm.sh/hook: pre-install, post-install, pre-upgrade, post-upgrade, pre-delete, post-delete, test. Dùng phổ biến: một Job pre-upgrade chạy database migration trước khi các Pod mới roll out, hoặc một Job helm.sh/hook: test (chạy qua helm test) để smoke-test một release.
# templates/db-migrate-job.yaml — chạy migration trước mỗi lần upgrade
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "myapp.fullname" . }}-migrate
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "0" # thứ tự giữa các hook
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["/app/migrate", "up"]
Chart dependencies cho phép một chart khai báo subchart trong Chart.yaml (ví dụ app của bạn phụ thuộc một chart postgresql và redis). helm dependency update vendor chúng vào charts/, và bạn cấu hình chúng qua values.yaml của chart cha dưới alias của chúng. Condition và tag cho phép bật/tắt dependency theo môi trường.
# Chart.yaml
dependencies:
- name: postgresql
version: "15.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled # bật/tắt qua values
Kustomize: bases và overlays
Kustomize theo cách tiếp cận không template: các manifest của bạn giữ nguyên YAML thuần, hợp lệ, apply được, và bạn kết hợp các biến thể môi trường bằng cách xếp lớp. Nó được tích hợp sẵn vào kubectl (kubectl apply -k) và cũng có dạng binary kustomize độc lập (bản mới hơn).
- Base — một thư mục manifest thật cộng một
kustomization.yamlliệt kê chúng. - Overlay — một thư mục tham chiếu một hoặc nhiều base và áp patch, name prefix/suffix, common label/annotation, override image tag, số replica, và generator lên trên.
- Patches — strategic-merge patch (YAML một phần được merge vào target) hoặc JSON 6902 patch (chỉnh sửa
op/pathchính xác) để override các trường cụ thể mà không nhân bản cả resource. - Generators —
configMapGenerator/secretGeneratordựng ConfigMap/Secret từ file hoặc literal, và gắn một content hash vào tên để một thay đổi dữ liệu ép rolling update (và prune cái cũ).
myapp/
├── base/
│ ├── kustomization.yaml # resources: deployment.yaml, service.yaml
│ ├── deployment.yaml # YAML thuần, không placeholder
│ └── service.yaml
└── overlays/
├── staging/
│ └── kustomization.yaml # base + patch cho staging
└── production/
├── kustomization.yaml
└── replicas-patch.yaml
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
commonLabels:
app: myapp
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
namePrefix: prod-
resources:
- ../../base
images:
- name: myorg/myapp # override image tag cho prod
newTag: "1.4.2"
replicas:
- name: myapp
count: 5
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
- ENV=production
patches:
- path: replicas-patch.yaml # strategic-merge patch, hoặc patch inline
kubectl apply -k overlays/production # build + apply
kubectl kustomize overlays/production # render ra stdout (review/diff)
kustomize build overlays/production | kubectl apply -f -
Chuyển đổi tư duy mấu chốt: với Kustomize bạn không bao giờ template một giá trị vào tồn tại — base là một manifest hoàn chỉnh, chạy được, và overlay diễn đạt các khác biệt dưới dạng patch declarative. Không có cú pháp {{ }}, không logic, không hàm.
Khái niệm chính
Helm vs Kustomize: so sánh
| Chiều | Helm | Kustomize |
|---|---|---|
| Cách tiếp cận | Templating (Go template + values) | Overlay/patch trên YAML thuần |
| Ngôn ngữ templating | Có — {{ }}, điều kiện, vòng lặp, hàm | Không — chỉ merge declarative |
| Base manifest | Không apply trực tiếp được (chứa {{ }}) | Luôn là YAML hợp lệ, apply được |
| Đóng gói/phân phối | Hạng nhất: chart versioned, repo, OCI | Không có sẵn (chỉ là thư mục trong git) |
| Theo dõi release & rollback | Có — revision lưu trong cluster, helm rollback | Không — chỉ tạo ra YAML; rollback qua git + apply lại |
| Lifecycle hook | Có (pre/post install/upgrade, test) | Không |
| Dependencies | Có — subchart | Qua tham chiếu nhiều base (không versioning) |
| Tích hợp trong kubectl | Không (binary riêng) | Có (kubectl apply -k) |
| Độ khó / cạm bẫy | Đau đầu whitespace/indent template; logic trong YAML | Ngữ nghĩa patch; ít sức mạnh cho giá trị động |
| Giỏi nhất ở | Phân phối app tái sử dụng/bên thứ ba, config nhiều tham số | Quản lý manifest của chính bạn qua các môi trường với ít phép thuật nhất |
Khi nào dùng Helm: bạn đang phân phối một ứng dụng cho người khác cài; bạn phụ thuộc các chart bên thứ ba (Postgres, Prometheus, ingress-nginx); bạn cần rollback release, hook (migration), hoặc tham số hóa nặng qua nhiều “nút vặn”.
Khi nào dùng Kustomize: bạn sở hữu manifest và muốn YAML thuần, review được; bạn có vài môi trường khác nhau chỉ ở vài trường (image tag, replicas, resources, namespace); bạn muốn không cần tooling thêm (nó có trong kubectl) và không ngôn ngữ templating nào phải suy luận.
Khi nào kết hợp: một pattern rất phổ biến là Helm cho cái bạn tiêu thụ, Kustomize cho cái bạn sở hữu — cài các chart upstream bằng Helm, và giữ manifest app của bạn trong Kustomize. Bạn cũng có thể hậu-xử-lý output của một chart: helm template ./chart | kustomize (Kustomize thậm chí inflate được một Helm chart qua trường helmCharts của nó), cho phép áp các patch cấp tổ chức (label, security context) lên một chart bên thứ ba mà không fork nó. Các công cụ GitOps (Argo CD, Flux) hỗ trợ cả hai native, và có thể render Helm rồi áp patch Kustomize.
Cạm bẫy templating của Helm
- Whitespace và thụt lề (indentation) là nguồn đau đầu số một. Dùng
{{-/-}}để trim, vànindent N(thụt lề và thêm một newline phía trước) khi chèn các khối nhưtoYaml .Values.resources. Một cú indent sai âm thầm tạo ra YAML không hợp lệ mà Kubernetes từ chối với một lỗi khó hiểu. helm templatetrước khi apply. Render cục bộ bắt bug template mà không đụng tới cluster và cho bạn một artifact diff được để review/CI. Ghép với pluginhelm diffđể xem trước thay đổi lên một release đang chạy.- Logic phình ra. Go template có thể diễn đạt điều kiện và vòng lặp, nhưng logic phức tạp trong chuỗi YAML nhanh chóng trở nên khó đọc. Nếu bạn đang viết logic nghiêm túc, đó là tín hiệu nên cân nhắc jsonnet/CUE.
- Secret trong values.
values.yamllà plaintext; đừng commit secret. Dùnghelm-secrets(SOPS), các external secret operator, hoặc một secrets manager.
Ngữ nghĩa patch của Kustomize
- Strategic-merge patch merge một manifest một phần vào target bằng cách khớp kind/name; với list nó dùng merge key (ví dụ container theo
name), nên bạn patch một container mà không viết lại cả mảng. Đây là trường hợp phổ biến, dễ đọc. - JSON 6902 patch làm các thao tác chính xác (
add/replace/removetại một JSON path) — cần khi strategic merge không diễn đạt được (đổi thứ tự, xóa một phần tử list). - Hash của generator là một tính năng, không phải phiền toái: vì
configMapGeneratorgắn content hash vào tên và viết lại các tham chiếu, đổi config tự động kích hoạt rolling restart và để lại ConfigMap cũ cho rollback. Chỉ đặtdisableNameSuffixHash: truekhi bạn có lý do mạnh.
Công cụ khác: jsonnet và ytt
- jsonnet — một ngôn ngữ data-templating (superset của JSON) với biến, hàm, import, và composition object. Việc dùng trong Kubernetes được phổ biến bởi Tanka (Grafana) và hệ sinh thái kube-prometheus / jsonnet-bundler. Điểm mạnh: lập trình thật cho config tính toán, DRY (tuyệt khi bạn sinh nhiều object tương tự, ví dụ một dashboard mỗi service). Cái giá: một ngôn ngữ mới phải học và ít dễ khám phá hơn YAML.
- ytt (thuộc Carvel) — templating YAML và overlay được điều khiển bởi Starlark (một phương ngữ Python), với hỗ trợ schema/data-values mạnh. Nó kết hợp sức mạnh templating với mô hình patch/overlay và là engine templating đứng sau tooling GitOps
kapp/kapp-controllercủa Carvel. - CUE — một ngôn ngữ cấu hình với ràng buộc kiểu (type) và validation mạnh, ngày càng được dùng để định nghĩa và validate config Kubernetes với các đảm bảo mà template không có được.
Chúng đáng dùng khi cả string templating của Helm lẫn patch tĩnh của Kustomize đều đụng tường — thường là các platform lớn sinh nhiều object gần y hệt một cách lập trình. Với hầu hết team, Helm và/hoặc Kustomize là đủ.
Best Practices
- Giữ một nguồn sự thật; dẫn xuất các môi trường từ nó. Dù qua Helm values hay Kustomize overlay, đừng bao giờ duy trì các bản manifest đầy đủ song song theo môi trường — điều đó bảo đảm drift. Toàn bộ điểm là một diff nhỏ, review được theo từng môi trường.
- Ưu tiên
helm upgrade --installtrong automation. Nó idempotent (cài-hoặc-upgrade) và an toàn để chạy lặp lại trong CI/CD mà không cần rẽ nhánh xem release đã tồn tại chưa. - Luôn render/diff trước khi apply.
helm template(và pluginhelm diff) hoặckubectl kustomizerender cục bộ để bạn review YAML thực sự được sinh ra trong code review và CI — bắt bug template/patch trước khi tới cluster. - Pin version của chart và dependency. Pin
version:cho subchart và các chart/image tag bạn deploy; đừng bao giờ dựa vào “latest”. Build tái lập được đòi hỏi version chính xác trongChart.yamlvàChart.lockđược khóa. - Lint và validate trong CI.
helm lint,kustomize build, cộng schema validation (kubeconform/kubeval) và kiểm tra policy (OPA/Kyverno/conftest) trên output đã render bắt lỗi trước khi deploy. - Không bao giờ commit secret plaintext vào values hay generator. Dùng SOPS/helm-secrets, sealed-secrets, hoặc một external-secrets operator.
values.yamlvà các file Kustomize sống trong git và không phải nơi an toàn cho credential. - Dùng
nindent/trim whitespace cẩn thận trong template Helm. Bug thụt lề là failure mode hàng đầu của Helm; chèn khối bằngtoYaml … | nindent N, và render template để kiểm YAML hợp lệ trước khi apply. - Giữ template đơn giản; đẩy logic ra khỏi YAML. Nếu logic Go-template của một chart trở nên khó đọc, đó là mùi xấu — đơn giản hóa giao diện values, hoặc chuyển sang jsonnet/CUE cho config thực sự lập trình.
- Dùng Helm hook cho tác vụ lifecycle, không phải logic app. Job
pre-upgradecho DB migration và Jobhelm testcho smoke test là cách dùng đúng; đặthook-delete-policyđể resource hook được dọn dẹp. - Để generator của Kustomize hash config. Content-hash suffix trên ConfigMap/Secret được sinh khiến thay đổi config tự động kích hoạt rolling update và giữ bản cũ cho rollback — đừng tắt nó mà không có lý do.
- Ưu tiên strategic-merge patch để dễ đọc; dành JSON 6902 cho cái merge không làm được. Merge patch đọc giống resource nó sửa; JSON patch chính xác nhưng khó hiểu — chỉ dùng cho phẫu thuật list hoặc xóa.
- Kết hợp Helm và Kustomize có chủ đích. Tiêu thụ app bên thứ ba dưới dạng Helm chart; quản lý manifest của chính bạn bằng Kustomize; xếp lớp các patch cấp tổ chức (label, security context) lên chart đã render thay vì fork chúng.
- Version và lưu output đã render hoặc source trong git (GitOps). Argo CD/Flux render Helm/Kustomize từ một git commit, cho bạn dấu vết audit và rollback dựa trên git — coi repo, không phải cluster, là nguồn sự thật.
- Tài liệu hóa giao diện values và cung cấp mặc định hợp lý.
values.yamlcủa một chart là API của nó: mặc định về các giá trị an toàn cho production, comment từng nút vặn, và validate bằng mộtvalues.schema.jsonđể input xấu fail sớm. - Test upgrade và rollback, không chỉ install. Kiểm tra
helm rollbackhoạt động và migration/hook có thể đảo ngược hoặc idempotent; một release bạn không rollback an toàn được là một gánh nặng khi sự cố. - Đừng over-engineer. Với vài môi trường khác nhau chút ít, Kustomize overlay thắng một chart tham số hóa nặng; chỉ dùng sức mạnh đóng gói của Helm (hoặc jsonnet) khi nhu cầu phân phối hoặc lập trình là thật.
Tài liệu tham khảo
- roadmap.sh — Kubernetes Roadmap
- Helm documentation · Helm — Charts
- Helm — Chart Template Guide · Built-in Objects
- Helm — Values Files · Chart Dependencies
- Helm — Chart Hooks · Chart Tests
- Helm — Chart Best Practices ·
helm upgrade - Kustomize documentation · Kustomize.io
- Kubernetes — Declarative Management with Kustomize
- Kustomize — Overlays & Bases · Patches
- Kustomize — configMapGenerator/secretGenerator
- Jsonnet · Grafana Tanka · jsonnet-bundler
- Carvel ytt · Carvel kapp · CUE
- Argo CD — Helm & Kustomize support · Flux — Kustomize & Helm
- SOPS · helm-secrets · External Secrets Operator
Part of the Kubernetes Roadmap knowledge base.
Overview
Raw Kubernetes manifests are static YAML: a Deployment, a Service, a ConfigMap, each hard-coding one environment’s values. That works for a demo and collapses the moment you have dev, staging, and production — you end up with three near-identical copies of every file, drifting apart with every hand edit, and a “change the image tag in all environments” task that means editing a dozen files without missing one. Package management and templating exist to solve exactly this: keep one source of truth and derive per-environment, per-release variants from it — the DRY principle applied to infrastructure.
Two tools dominate, with fundamentally different philosophies. Helm is a templating and package manager: it treats a set of manifests as a versioned, parameterizable chart with Go-template placeholders ({{ .Values.image.tag }}) filled from a values.yaml, and it installs charts as tracked releases you can upgrade and roll back. Kustomize is template-free overlay configuration: it takes plain, valid YAML as a base and applies declarative patches in per-environment overlays, without any templating language — and it’s built directly into kubectl. Helm is “parameterize with variables and distribute as a package”; Kustomize is “start from real YAML and layer differences on top.”
Neither is strictly better — they solve overlapping but distinct problems, and mature teams often combine them (Helm for third-party dependencies you install, Kustomize for tailoring your own manifests, or helm template | kustomize pipelines). Beyond these two sit richer languages — jsonnet (with the Tanka/ksonnet lineage) and ytt (Carvel) — used where teams want real programming constructs (functions, imports, computed values) rather than templates or patches. This page covers when and how to reach for each.
Why not just copy YAML per environment?
The copy-paste approach fails predictably: values drift (staging and prod diverge silently), changes are error-prone (you forget one file), and there’s no release concept (no atomic “install this whole app”, no versioned rollback). Templating gives you a single canonical definition plus a small, reviewable diff of what actually changes per environment — turning “which of my 40 YAML files is wrong?” into “here are the 6 values that differ in prod.”
Fundamentals
Helm: charts, values, releases
Helm is the most widely used Kubernetes package manager. Its core abstractions:
- Chart — a packaged directory of templated manifests plus metadata. It’s the unit of distribution (like an
.rpm/.debor an npm package for Kubernetes). - Template — a manifest file under
templates/containing Go template directives that reference values, built-in objects (.Release,.Chart,.Capabilities), and helper functions (Sprig library:default,quote,toYaml,include). - Values — the parameters.
values.yamlholds defaults; users override with-f myvalues.yamlor--set key=value. Precedence:--set>-ffiles (last wins) > chart defaults. - Release — a specific installation of a chart into a cluster, with a name and a revision history. Helm stores release state as a Secret in the cluster, which is what makes
helm rollbackpossible. - Repository — an HTTP server (or OCI registry) hosting packaged
.tgzcharts and anindex.yaml.helm repo add/helm pullfetch from it.
Chart directory layout:
myapp/
├── Chart.yaml # name, version, appVersion, dependencies
├── values.yaml # default parameters
├── charts/ # vendored dependency charts (subcharts)
├── crds/ # CRDs installed before templates
└── templates/
├── _helpers.tpl # reusable named template snippets
├── deployment.yaml
├── service.yaml
├── ingress.yaml
└── NOTES.txt # post-install message
# templates/deployment.yaml — a chart template
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
# values.yaml — defaults, overridden per environment
replicaCount: 2
image:
repository: myorg/myapp
tag: "" # empty → falls back to Chart.AppVersion
service:
port: 8080
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 256Mi }
Essential Helm commands:
helm repo add bitnami https://charts.bitnami.com/bitnami # add a repo
helm search repo postgres # find charts
helm install myrel ./myapp -f prod-values.yaml # install a release
helm upgrade --install myrel ./myapp -f prod-values.yaml # idempotent install-or-upgrade
helm template myrel ./myapp -f prod-values.yaml # render locally, no cluster (great for CI/diff)
helm lint ./myapp # validate chart
helm list # list releases
helm history myrel # revision history
helm rollback myrel 3 # atomic rollback to revision 3
helm diff upgrade myrel ./myapp # (plugin) preview changes before applying
helm uninstall myrel
helm upgrade --install is the workhorse idempotent command: it installs the release if absent and upgrades it otherwise — safe to run repeatedly in CI/CD without branching logic.
Helm hooks and chart dependencies
Hooks let a chart run resources at defined points in the release lifecycle via the helm.sh/hook annotation: pre-install, post-install, pre-upgrade, post-upgrade, pre-delete, post-delete, test. Common use: a pre-upgrade Job that runs a database migration before the new Pods roll out, or a helm.sh/hook: test Job (run via helm test) that smoke-tests a release.
# templates/db-migrate-job.yaml — run a migration before each upgrade
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "myapp.fullname" . }}-migrate
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "0" # ordering among hooks
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["/app/migrate", "up"]
Chart dependencies let a chart declare subcharts in Chart.yaml (e.g. your app depends on a postgresql and redis chart). helm dependency update vendors them into charts/, and you configure them through the parent’s values.yaml under their alias. Conditions and tags let you toggle dependencies on/off per environment.
# Chart.yaml
dependencies:
- name: postgresql
version: "15.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled # toggle via values
Kustomize: bases and overlays
Kustomize takes a template-free approach: your manifests stay plain, valid, apply-able YAML, and you compose environment variants by layering. It’s built into kubectl (kubectl apply -k) and shipped as a standalone kustomize binary (newer versions).
- Base — a directory of real manifests plus a
kustomization.yamllisting them. - Overlay — a directory that references one or more bases and applies patches, name prefixes/suffixes, common labels/annotations, image tag overrides, replica counts, and generators on top.
- Patches — strategic-merge patches (partial YAML merged into the target) or JSON 6902 patches (precise
op/pathedits) that override specific fields without duplicating the whole resource. - Generators —
configMapGenerator/secretGeneratorbuild ConfigMaps/Secrets from files or literals, and append a content hash to the name so a data change forces a rolling update (and prunes the old one).
myapp/
├── base/
│ ├── kustomization.yaml # resources: deployment.yaml, service.yaml
│ ├── deployment.yaml # plain YAML, no placeholders
│ └── service.yaml
└── overlays/
├── staging/
│ └── kustomization.yaml # base + patches for staging
└── production/
├── kustomization.yaml
└── replicas-patch.yaml
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
commonLabels:
app: myapp
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
namePrefix: prod-
resources:
- ../../base
images:
- name: myorg/myapp # override the image tag for prod
newTag: "1.4.2"
replicas:
- name: myapp
count: 5
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
- ENV=production
patches:
- path: replicas-patch.yaml # strategic-merge patch, or inline patch
kubectl apply -k overlays/production # build + apply
kubectl kustomize overlays/production # render to stdout (review/diff)
kustomize build overlays/production | kubectl apply -f -
The key mental shift: with Kustomize you never template a value into existence — the base is a complete, working manifest, and overlays express the differences as declarative patches. There is no {{ }} syntax, no logic, no functions.
Key Concepts
Helm vs Kustomize: the comparison
| Dimension | Helm | Kustomize |
|---|---|---|
| Approach | Templating (Go templates + values) | Overlays/patches on plain YAML |
| Templating language | Yes — {{ }}, conditionals, loops, functions | None — declarative merges only |
| Base manifests | Not directly applyable (contain {{ }}) | Always valid, applyable YAML |
| Packaging/distribution | First-class: versioned charts, repos, OCI | None built-in (just directories in git) |
| Release tracking & rollback | Yes — revisions stored in-cluster, helm rollback | No — it only produces YAML; rollback is via git + re-apply |
| Lifecycle hooks | Yes (pre/post install/upgrade, tests) | No |
| Dependencies | Yes — subcharts | Via referencing multiple bases (no versioning) |
| Built into kubectl | No (separate binary) | Yes (kubectl apply -k) |
| Learning curve / footguns | Templating whitespace/indent pain; logic in YAML | Patch semantics; less power for dynamic values |
| Best at | Distributing reusable/third-party apps, parameter-heavy configs | Managing your own manifests across environments with minimal magic |
When to use Helm: you’re distributing an application for others to install; you depend on third-party charts (Postgres, Prometheus, ingress-nginx); you need release rollback, hooks (migrations), or heavy parameterization across many knobs.
When to use Kustomize: you own the manifests and want plain, reviewable YAML; you have a handful of environments differing by a few fields (image tag, replicas, resources, namespace); you want zero extra tooling (it’s in kubectl) and no templating language to reason about.
When to combine them: a very common pattern is Helm for what you consume, Kustomize for what you own — install upstream charts with Helm, and keep your app’s manifests in Kustomize. You can also post-process a chart’s output: helm template ./chart | kustomize (Kustomize can even inflate a Helm chart via its helmCharts field), letting you apply org-wide patches (labels, security context) to a third-party chart without forking it. GitOps tools (Argo CD, Flux) natively support both, and can render Helm and then apply Kustomize patches.
Helm templating pitfalls
- Whitespace and indentation are the number-one source of pain. Use
{{-/-}}to trim, andnindent N(indent and prepend a newline) when injecting blocks liketoYaml .Values.resources. A wrong indent silently produces invalid YAML that Kubernetes rejects with a cryptic error. helm templatebefore you apply. Rendering locally catches template bugs without touching the cluster and gives you a diff-able artifact for review/CI. Pair with thehelm diffplugin to preview changes to a live release.- Logic creep. Go templates can express conditionals and loops, but complex logic in YAML strings becomes unreadable fast. If you’re writing serious logic, that’s a signal to consider jsonnet/CUE instead.
- Secrets in values.
values.yamlis plaintext; don’t commit secrets. Usehelm-secrets(SOPS), external secret operators, or a secrets manager.
Kustomize patch semantics
- Strategic-merge patch merges a partial manifest into the target by matching kind/name; for lists it uses merge keys (e.g. containers by
name), so you patch one container without rewriting the array. This is the readable, common case. - JSON 6902 patch does precise operations (
add/replace/removeat a JSON path) — needed when strategic merge can’t express the edit (reordering, removing a list element). - Generator hashing is a feature, not a nuisance: because
configMapGeneratorappends a content hash to the name and rewrites references, changing config automatically triggers a rolling restart and leaves the old ConfigMap for rollback. SetdisableNameSuffixHash: trueonly if you have a strong reason.
Other tools: jsonnet and ytt
- jsonnet — a data-templating language (superset of JSON) with variables, functions, imports, and object composition. Kubernetes usage is popularized by Tanka (Grafana) and the kube-prometheus / jsonnet-bundler ecosystem. Strength: real programming for computed, DRY config (great when you generate lots of similar objects, e.g. one dashboard per service). Cost: a new language to learn and less discoverable than YAML.
- ytt (part of Carvel) — YAML templating and overlays driven by Starlark (a Python dialect), with strong schema/data-values support. It combines templating power with a patch/overlay model and is the templating engine behind Carvel’s
kapp/kapp-controllerGitOps tooling. - CUE — a configuration language with powerful type constraints and validation, increasingly used to define and validate Kubernetes config with guarantees templates can’t provide.
These are worth reaching for when Helm’s string templating and Kustomize’s static patches both hit a wall — typically large platforms generating many near-identical objects programmatically. For most teams, Helm and/or Kustomize is enough.
Best Practices
- Keep one source of truth; derive environments from it. Whether via Helm values or Kustomize overlays, never maintain parallel full copies of manifests per environment — that guarantees drift. The whole point is a small, reviewable per-environment diff.
- Prefer
helm upgrade --installin automation. It’s idempotent (install-or-upgrade) and safe to run repeatedly in CI/CD without branching on whether the release already exists. - Always render/diff before applying.
helm template(and thehelm diffplugin) orkubectl kustomizerender locally so you review the actual generated YAML in code review and CI — catching template/patch bugs before they reach the cluster. - Pin chart and dependency versions. Pin
version:for subcharts and the chart/image tags you deploy; never rely on “latest”. Reproducible builds require exact versions inChart.yamland lockedChart.lock. - Lint and validate in CI.
helm lint,kustomize build, plus schema validation (kubeconform/kubeval) and policy checks (OPA/Kyverno/conftest) on the rendered output catch errors before deploy. - Never commit plaintext secrets to values or generators. Use SOPS/helm-secrets, sealed-secrets, or an external-secrets operator.
values.yamland Kustomize files live in git and are not a safe place for credentials. - Use
nindent/whitespace-trim carefully in Helm templates. Indentation bugs are the top Helm failure mode; inject blocks withtoYaml … | nindent N, and template-render to verify the YAML is valid before applying. - Keep templates simple; push logic out of YAML. If a chart’s Go-template logic becomes hard to read, that’s a smell — simplify the values interface, or move to jsonnet/CUE for genuinely programmatic config.
- Use Helm hooks for lifecycle tasks, not app logic.
pre-upgradeJobs for DB migrations andhelm testJobs for smoke tests are the right use; sethook-delete-policyso hook resources are cleaned up. - Let Kustomize generators hash config. The content-hash suffix on generated ConfigMaps/Secrets makes config changes trigger a rolling update automatically and keeps the old version for rollback — don’t disable it without a reason.
- Prefer strategic-merge patches for readability; reserve JSON 6902 for what merge can’t do. Merge patches read like the resource they modify; JSON patches are precise but opaque — use them only for list surgery or removals.
- Combine Helm and Kustomize intentionally. Consume third-party apps as Helm charts; manage your own manifests with Kustomize; layer org-wide patches (labels, security contexts) onto rendered charts rather than forking them.
- Version and store rendered output or the source in git (GitOps). Argo CD/Flux render Helm/Kustomize from a git commit, giving you an audit trail and git-based rollback — treat the repo, not the cluster, as the source of truth.
- Document your values interface and provide sane defaults. A chart’s
values.yamlis its API: default to production-safe values, comment each knob, and validate with avalues.schema.jsonso bad inputs fail early. - Test upgrades and rollbacks, not just installs. Verify
helm rollbackworks and that migrations/hooks are reversible or idempotent; a release you can’t safely roll back is a liability during an incident. - Don’t over-engineer. For a couple of environments with small differences, Kustomize overlays beat a heavily-parameterized chart; reach for Helm’s packaging power (or jsonnet) only when the distribution or programmability need is real.
References
- roadmap.sh — Kubernetes Roadmap
- Helm documentation · Helm — Charts
- Helm — Chart Template Guide · Built-in Objects
- Helm — Values Files · Chart Dependencies
- Helm — Chart Hooks · Chart Tests
- Helm — Chart Best Practices ·
helm upgrade - Kustomize documentation · Kustomize.io
- Kubernetes — Declarative Management with Kustomize
- Kustomize — Overlays & Bases · Patches
- Kustomize — configMapGenerator/secretGenerator
- Jsonnet · Grafana Tanka · jsonnet-bundler
- Carvel ytt · Carvel kapp · CUE
- Argo CD — Helm & Kustomize support · Flux — Kustomize & Helm
- SOPS · helm-secrets · External Secrets Operator