Expert guidance for authoring and maintaining Helm charts following standardized conventions, global registry support, templating best practices, and Kubernetes deployment patterns.
This skill provides standardized conventions for authoring and maintaining Helm charts, with a focus on:
.Values.global.image.registryimage: blocks for all containersActivate this skill when:
All charts must support the top-level configuration for global image settings.
global:
image:
registry: registry.mycompany.com
This enables centralized control of image sources across all dependencies and microservices.
All charts should follow a consistent image: block for every containerized application.
Fields should be templated for registry, repository, tag, pullPolicy, and pullSecrets for all containers.
Every chart must define all image values with reasonable defaults in values.yaml:
prometheus:
image:
registry: docker.io
repository: prom/prometheus
tag: v2.52.0
pullPolicy: IfNotPresent
Use a registry value at the top of the template. This pattern ensures ability to use internal registries (e.g., registry.mycompany.com) for air-gapped environments or mirrored image sources:
{{- $registry := .Values.prometheus.image.registry | default .Values.global.image.registry | default "docker.io" -}}
image:
registry: {{ $registry }}
repository: {{ .Values.prometheus.image.repository }}
tag: {{ .Values.prometheus.image.tag }}
pullPolicy: {{ .Values.prometheus.image.pullPolicy }}
Template only when necessary. Keep templates readable and manageable by avoiding over-templating.
Template:
Avoid Templating:
values.yaml unless dynamically constructedhelm lint before commitshelm template for rendering checksvalues.yaml and Chart.yaml are fully in sync with templated expectationsUse the chart-testing tool (ct) to automate linting, installation, and upgrade checks for charts.
Install ct (chart-testing) locally:
brew install helm/chart-testing/ct
# or via Docker:
# docker pull quay.io/helmpack/chart-testing
ct lint --config charts/your-chart/ct.yaml
ct install --config charts/your-chart/ct.yaml
Typical workflow:
ct lint --allct install --allct lint --charts charts/your-chartct lint and ct install before submitting a PRct.yaml is up to date with chart locations and test settingsct into CI pipelines for automated validationHelmfile enables declarative management of multiple Helm charts and environments.
brew install helmfile
# or via Docker:
# docker run --rm -v $PWD:/apps -w /apps ghcr.io/helmfile/helmfile:latest helmfile --help
helmfile.yaml or helmfile.d/*.yamlvalues: blocks to layer configuration and support overrides per environmenthelmfile lint to validate all releases and valueshelmfile apply (dry-run with --dry-run first)helmfile sync to ensure all releases match the desired statevalues-prod.yaml, values-dev.yaml)secrets: for sensitive values, leveraging helm-secrets if neededhelmfile diff before applying changes to preview impacthelmfile lint and test deployments in CI where possibleChart names must be lower case letters and numbers. Words may be separated with dashes (-).
Neither uppercase letters nor underscores can be used in chart names. Dots should not be used in chart names.
YAML files should be indented using two spaces (and never tabs).
When working with Custom Resource Definitions (CRDs):
kind: CustomResourceDefinition)apiVersion and kind)For a CRD, the declaration must be registered before any resources of that CRD's kind(s) can be used.
With Helm 3, use the special crds directory in your chart to hold your CRDs. These CRDs are not templated, but will be installed by default when running helm install. If the CRD already exists, it will be skipped with a warning. Use --skip-crds flag to skip CRD installation.
Note: There is no support for upgrading or deleting CRDs using Helm.
The following labels are recommended for Helm charts:
| Name | Status | Description |
|---|---|---|
app.kubernetes.io/name |
REC | App name, usually {{ template "name" . }} |
helm.sh/chart |
REC | Chart name and version: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} |
app.kubernetes.io/managed-by |
REC | Always set to {{ .Release.Service }} |
app.kubernetes.io/instance |
REC | Set to {{ .Release.Name }} |
app.kubernetes.io/version |
OPT | App version: {{ .Chart.AppVersion }} |
app.kubernetes.io/component |
OPT | Component role, e.g., frontend |
app.kubernetes.io/part-of |
OPT | Top-level application when multiple charts work together |
An item of metadata should be a label if:
If an item of metadata is not used for querying, it should be set as an annotation instead.
A container image should use a fixed tag or the SHA of the image. Never use latest, head, canary, or other "floating" tags.
All PodTemplate sections should specify a selector:
selector:
matchLabels:
app.kubernetes.io/name: MyName
template:
metadata:
labels:
app.kubernetes.io/name: MyName
This makes the relationship between the set and the pod explicit and prevents breaking changes when labels change.
RBAC and ServiceAccount configuration should happen under separate keys:
rbac:
# Specifies whether RBAC resources should be created
create: true
serviceAccount:
# Specifies whether a ServiceAccount should be created
create: true
# The name of the ServiceAccount to use
# If not set and create is true, a name is generated using the fullname template
name:
For complex charts with multiple ServiceAccounts:
someComponent:
serviceAccount:
create: true
name:
anotherComponent:
serviceAccount:
create: true
name:
rbac.create should default to true. Users who wish to manage RBAC access controls themselves can set this to false.
{{/*
Create the name of the service account to use
*/}}
{{- define "mychart.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "mychart.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}
The templates/ directory should be structured as follows:
.yaml if they produce YAML output.tpl may be used for template files that produce no formatted contentmy-example-configmap.yaml), not camelcasefoo-pod.yaml, bar-svc.yaml)All defined template names should be namespaced to avoid collisions with subcharts:
Correct:
{{- define "nginx.fullname" }}
{{/* ... */}}
{{ end -}}
Incorrect:
{{- define "fullname" -}}
{{/* ... */}}
{{ end -}}
Templates should be indented using two spaces (never tabs).
Template directives should have whitespace after the opening braces and before the closing braces:
Correct:
{{ .foo }}
{{ print "foo" }}
{{- print "bar" -}}
Incorrect:
{{.foo}}
{{print "foo"}}
{{-print "bar"-}}
.yaml extension (or .tpl for helpers)global.image.registry overridevalues.yamlrbac.create defaults to truehelm lintct lintvalues.yaml and Chart.yaml are in sync