← Kubernetes← Kubernetes
KubernetesKubernetes19 Th7, 2026Jul 19, 202617 phút đọc14 min read

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, stagingproduction — 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:

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

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ềuHelmKustomize
Cách tiếp cậnTemplating (Go template + values)Overlay/patch trên YAML thuần
Ngôn ngữ templatingCó — {{ }}, điều kiện, vòng lặp, hàmKhông — chỉ merge declarative
Base manifestKhông apply trực tiếp được (chứa {{ }})Luôn là YAML hợp lệ, apply được
Đóng gói/phân phốiHạng nhất: chart versioned, repo, OCIKhông có sẵn (chỉ là thư mục trong git)
Theo dõi release & rollbackCó — revision lưu trong cluster, helm rollbackKhông — chỉ tạo ra YAML; rollback qua git + apply lại
Lifecycle hookCó (pre/post install/upgrade, test)Không
DependenciesCó — subchartQua tham chiếu nhiều base (không versioning)
Tích hợp trong kubectlKhông (binary riêng)Có (kubectl apply -k)
Độ khó / cạm bẫyĐau đầu whitespace/indent template; logic trong YAMLNgữ 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

Ngữ nghĩa patch của Kustomize

Công cụ khác: jsonnet và ytt

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

  1. 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.
  2. Ưu tiên helm upgrade --install trong 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.
  3. Luôn render/diff trước khi apply. helm template (và plugin helm diff) hoặc kubectl kustomize render 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.
  4. 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 trong Chart.yamlChart.lock được khóa.
  5. 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.
  6. 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.yaml và các file Kustomize sống trong git và không phải nơi an toàn cho credential.
  7. 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ằng toYaml … | nindent N, và render template để kiểm YAML hợp lệ trước khi apply.
  8. 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.
  9. Dùng Helm hook cho tác vụ lifecycle, không phải logic app. Job pre-upgrade cho DB migration và Job helm test cho smoke test là cách dùng đúng; đặt hook-delete-policy để resource hook được dọn dẹp.
  10. Để 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.
  11. Ư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.
  12. 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.
  13. 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.
  14. Tài liệu hóa giao diện values và cung cấp mặc định hợp lý. values.yaml củ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ột values.schema.json để input xấu fail sớm.
  15. Test upgrade và rollback, không chỉ install. Kiểm tra helm rollback hoạ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ố.
  16. Đừ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

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

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

DimensionHelmKustomize
ApproachTemplating (Go templates + values)Overlays/patches on plain YAML
Templating languageYes — {{ }}, conditionals, loops, functionsNone — declarative merges only
Base manifestsNot directly applyable (contain {{ }})Always valid, applyable YAML
Packaging/distributionFirst-class: versioned charts, repos, OCINone built-in (just directories in git)
Release tracking & rollbackYes — revisions stored in-cluster, helm rollbackNo — it only produces YAML; rollback is via git + re-apply
Lifecycle hooksYes (pre/post install/upgrade, tests)No
DependenciesYes — subchartsVia referencing multiple bases (no versioning)
Built into kubectlNo (separate binary)Yes (kubectl apply -k)
Learning curve / footgunsTemplating whitespace/indent pain; logic in YAMLPatch semantics; less power for dynamic values
Best atDistributing reusable/third-party apps, parameter-heavy configsManaging 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

Kustomize patch semantics

Other tools: jsonnet and ytt

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

  1. 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.
  2. Prefer helm upgrade --install in automation. It’s idempotent (install-or-upgrade) and safe to run repeatedly in CI/CD without branching on whether the release already exists.
  3. Always render/diff before applying. helm template (and the helm diff plugin) or kubectl kustomize render locally so you review the actual generated YAML in code review and CI — catching template/patch bugs before they reach the cluster.
  4. 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 in Chart.yaml and locked Chart.lock.
  5. 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.
  6. Never commit plaintext secrets to values or generators. Use SOPS/helm-secrets, sealed-secrets, or an external-secrets operator. values.yaml and Kustomize files live in git and are not a safe place for credentials.
  7. Use nindent/whitespace-trim carefully in Helm templates. Indentation bugs are the top Helm failure mode; inject blocks with toYaml … | nindent N, and template-render to verify the YAML is valid before applying.
  8. 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.
  9. Use Helm hooks for lifecycle tasks, not app logic. pre-upgrade Jobs for DB migrations and helm test Jobs for smoke tests are the right use; set hook-delete-policy so hook resources are cleaned up.
  10. 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.
  11. 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.
  12. 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.
  13. 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.
  14. Document your values interface and provide sane defaults. A chart’s values.yaml is its API: default to production-safe values, comment each knob, and validate with a values.schema.json so bad inputs fail early.
  15. Test upgrades and rollbacks, not just installs. Verify helm rollback works and that migrations/hooks are reversible or idempotent; a release you can’t safely roll back is a liability during an incident.
  16. 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