Complete helm toolkit with generation and validation capabilities
72
90%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
This reference provides comprehensive best practices for creating, maintaining, and validating Helm charts.
Every Helm chart must have:
mychart/
Chart.yaml # Chart metadata
values.yaml # Default configuration values
templates/ # Template directoryUse apiVersion v2 for Helm 3+ charts:
apiVersion: v2
name: mychart
description: A Helm chart for Kubernetes
type: application # or 'library' for helper charts
version: 0.1.0 # Chart version (SemVer)
appVersion: "1.16.0" # Version of the app
# Optional but recommended
keywords:
- web
- application
home: https://github.com/example/mychart
sources:
- https://github.com/example/mychart
maintainers:
- name: Your Name
email: your.email@example.com
# Dependencies
dependencies:
- name: postgresql
version: "~11.6.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
# Kubernetes version constraint
kubeVersion: ">=1.21.0-0"Define reusable templates in templates/_helpers.tpl:
Good:
{{- define "mychart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}Usage:
metadata:
name: {{ include "mychart.fullname" . }}include Instead of templateGood:
metadata:
labels:
{{- include "mychart.labels" . | nindent 4 }}Bad:
metadata:
labels:
{{- template "mychart.labels" . }}Why: include allows piping the output to other functions like nindent, indent, quote, etc.
Good:
env:
- name: DATABASE_HOST
value: {{ .Values.database.host | quote }}Bad:
env:
- name: DATABASE_HOST
value: {{ .Values.database.host }}Why: Prevents YAML parsing issues with special characters and ensures strings are treated as strings.
required for Critical ValuesGood:
apiVersion: v1
kind: Secret
metadata:
name: {{ include "mychart.fullname" . }}
data:
password: {{ required "A valid .Values.password is required!" .Values.password | b64enc }}Why: Fails early with a helpful error message if required values are missing.
default FunctionGood:
replicas: {{ .Values.replicaCount | default 1 }}Why: Makes charts more resilient and easier to use with minimal configuration.
nindent for Proper IndentationGood:
spec:
template:
metadata:
labels:
{{- include "mychart.labels" . | nindent 8 }}Bad:
spec:
template:
metadata:
labels:
{{ include "mychart.labels" . | indent 8 }}Why: nindent adds a newline before indenting, which is usually what you want in YAML.
toYaml for Complex StructuresGood:
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}Why: Allows users to specify complex YAML structures in values without template complexity.
Good:
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mychart.fullname" . }}
# ... ingress definition
{{- end }}Why: Allows users to optionally enable/disable resources.
with for ScopingGood:
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 2 }}
{{- end }}Why: Changes the scope of . to the specified value, making templates cleaner.
Good:
{{- /*
This helper creates the fullname for resources.
It supports nameOverride and fullnameOverride values.
*/ -}}
{{- define "mychart.fullname" -}}
{{- end }}Bad:
# This creates the fullname
{{- define "mychart.fullname" -}}
{{- end }}Why: Template comments ({{- /* */ -}}) are removed during rendering, while YAML comments (#) remain in output.
Good (Simple):
replicaCount: 1
imagePullPolicy: IfNotPresentGood (Related Settings):
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "1.21.0"Bad (Overly Nested):
app:
deployment:
pod:
container:
image:
repository: nginxGood:
# replicaCount is the number of pod replicas for the deployment
replicaCount: 1
# image configures the container image
image:
# image.repository is the container image registry and name
repository: nginx
# image.pullPolicy is the image pull policy
pullPolicy: IfNotPresent
# image.tag overrides the image tag (default is chart appVersion)
tag: "1.21.0"Why: Makes charts self-documenting and easier to use.
Good:
replicaCount: 1
service:
type: ClusterIP
port: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128MiWhy: Charts should work out of the box with minimal configuration.
Good:
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []Why: Makes it clear when features are optional and how to enable them.
Good:
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80Good:
nodeSelector: {}
tolerations: []
affinity: {}Why: Shows users what optional configurations are available.
Good:
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128MiWhy: Ensures proper scheduling and prevents resource exhaustion.
Good:
metadata:
labels:
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ include "mychart.chart" . }}Why: Follows Kubernetes recommended labels for better tooling integration.
Good:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
capabilities:
drop:
- ALL
readOnlyRootFilesystem: trueWhy: Improves security posture.
Good:
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5Why: Ensures Kubernetes can properly manage application health.
Good:
# ConfigMap for non-sensitive config
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}
data:
app.conf: |
{{- .Values.config | nindent 4 }}# Secret for sensitive data
apiVersion: v1
kind: Secret
metadata:
name: {{ include "mychart.fullname" . }}
type: Opaque
data:
password: {{ .Values.password | b64enc }}quote - Quote a stringsquote - Single quote a stringtrim - Remove whitespacetrimSuffix - Remove suffixtrimPrefix - Remove prefixupper - Convert to uppercaselower - Convert to lowercasetitle - Title casetrunc - Truncate stringrepeat - Repeat stringsubstr - Substringnospace - Remove all whitespacetoYaml - Convert to YAMLfromYaml - Parse YAMLtoJson - Convert to JSONfromJson - Parse JSONtoString - Convert to stringtoStrings - Convert list to stringsdefault - Provide default valuerequired - Require a valuefail - Fail with error messagecoalesce - Return first non-empty valuelist - Create a listdict - Create a dictionarymerge - Merge dictionariespick - Pick keys from dictionaryomit - Omit keys from dictionarykeys - Get dictionary keysvalues - Get dictionary valuesb64enc - Base64 encodeb64dec - Base64 decodesha256sum - SHA256 hashGood:
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "mychart.fullname" . }}-test-connection"
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "mychart.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: NeverRun tests:
helm test <release-name># Test with production values
helm template my-release ./mychart -f values-prod.yaml
# Test with development values
helm template my-release ./mychart -f values-dev.yaml
# Test with overrides
helm template my-release ./mychart --set replicaCount=3# Lint the chart
helm lint ./mychart
# Dry-run install
helm install my-release ./mychart --dry-run --debug
# Validate against cluster
helm install my-release ./mychart --dry-runBad:
data:
password: cGFzc3dvcmQ= # Don't do this!Good:
data:
password: {{ .Values.password | b64enc }}Good:
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "mychart.serviceAccountName" . }}
{{- end }}Good:
securityContext:
runAsNonRoot: true
runAsUser: 1000Good:
securityContext:
capabilities:
drop:
- ALL.helmignoreExclude unnecessary files from the chart package:
# .helmignore
.git/
.gitignore
*.md
.DS_Store
*.swp
test/Good:
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}Bad:
resources:
{{- if .Values.resources.limits }}
limits:
{{- if .Values.resources.limits.cpu }}
cpu: {{ .Values.resources.limits.cpu }}
{{- end }}
{{- if .Values.resources.limits.memory }}
memory: {{ .Values.resources.limits.memory }}
{{- end }}
{{- end }}
# ... more nested conditionalsDon't repeat the same template logic in multiple places - extract it to a helper.
MAJOR.MINOR.PATCHDocument changes between versions.
git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0- for Whitespace ControlBad:
{{ if .Values.enabled }}
key: value
{{ end }}Good:
{{- if .Values.enabled }}
key: value
{{- end }}Kubernetes resource names must be <= 63 characters:
Bad:
name: {{ .Release.Name }}-{{ .Chart.Name }}-deploymentGood:
name: {{ include "mychart.fullname" . | trunc 63 | trimSuffix "-" }}template Instead of includeUse include when you need to pipe the output to other functions.
Bad:
replicas: {{ .Values.replicaCount }}Good:
replicas: {{ required "replicaCount is required" .Values.replicaCount }}Bad:
image: nginx:1.21.0Good:
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"metadata:
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": hook-succeededAvailable hooks:
pre-installpost-installpre-upgradepost-upgradepre-deletepost-deletepre-rollbackpost-rollbacktest# Show what would change
helm diff upgrade my-release ./mychart
# Dry-run upgrade
helm upgrade my-release ./mychart --dry-run --debugEnsure your charts support rollback by not using hooks that delete critical resources.
Include:
Add comments to your _helpers.tpl:
{{- /*
mychart.fullname generates a fully qualified application name.
It supports fullnameOverride and nameOverride values.
Maximum length is 63 characters per DNS naming spec.
Usage: {{ include "mychart.fullname" . }}
*/ -}}
{{- define "mychart.fullname" -}}
{{- end }}Create templates/NOTES.txt with post-installation instructions:
Thank you for installing {{ .Chart.Name }}.
Your release is named {{ .Release.Name }}.
To learn more about the release, try:
$ helm status {{ .Release.Name }}
$ helm get all {{ .Release.Name }}
{{- if .Values.ingress.enabled }}
Application URL:
{{- range .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
{{- end }}
{{- end }}helm package ./mychart# Create index
helm repo index .
# Serve repository
helm serveCreate artifacthub-repo.yml in your repository:
repositoryID: <uuid>
owners:
- name: Your Name
email: your.email@example.comBefore releasing a chart, verify:
helm lint passeshelm template renders successfully