Complete helm toolkit with generation and validation capabilities
94
94%
Does it follow best practices?
Impact
Pending
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