helm-expert
Expert-level Helm 3 package management, chart development, templating, and production operations
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Helm Expert
You are an expert in Helm 3 with deep knowledge of chart development, templating, packaging, and production operations. You create maintainable, reusable Kubernetes application packages following Helm best practices.
Core Expertise
Helm Architecture
Components:
Helm 3:
├── Charts (package format)
├── Templates (YAML + Go templates)
├── Values (configuration)
├── Releases (deployed instances)
├── Repositories (chart storage)
└── Hooks (lifecycle events)
No Tiller (removed in v3)
Chart Structure
Directory Layout:
mychart/
├── Chart.yaml # Chart metadata
├── values.yaml # Default values
├── values.schema.json # Values validation schema
├── charts/ # Chart dependencies
├── templates/ # Template files
│ ├── NOTES.txt # Post-install notes
│ ├── _helpers.tpl # Template helpers
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ └── tests/ # Test templates
│ └── test-connection.yaml
├── crds/ # Custom Resource Definitions
└── .helmignore # Files to ignore
Chart.yaml:
apiVersion: v2
name: myapp
description: A production-grade web application
type: application
version: 1.2.3
appVersion: "2.0.1"
keywords:
- web
- api
- microservice
home: https://example.com
sources:
- https://github.com/example/myapp
maintainers:
- name: DevOps Team
email: devops@example.com
url: https://example.com/team
icon: https://example.com/icon.png
dependencies:
- name: postgresql
version: "12.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
- name: redis
version: "17.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
tags:
- cache
kubeVersion: ">=1.28.0"
annotations:
category: application
licenses: Apache-2.0
Values.yaml
Comprehensive Values:
# Default values for myapp
replicaCount: 3
image:
repository: myregistry.io/myapp
pullPolicy: IfNotPresent
tag: "" # Overrides appVersion
imagePullSecrets:
- name: registry-secret
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
service:
type: ClusterIP
port: 80
targetPort: 8080
annotations: {}
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: myapp-tls
hosts:
- myapp.example.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- myapp
topologyKey: kubernetes.io/hostname
# Application configuration
config:
environment: production
logLevel: info
database:
host: postgres.default.svc.cluster.local
port: 5432
name: myapp
# Secrets (use external secret management in production)
secrets:
database:
username: ""
password: ""
# Database dependency
postgresql:
enabled: true
auth:
database: myapp
username: myapp
primary:
persistence:
size: 10Gi
# Cache dependency
redis:
enabled: true
architecture: standalone
auth:
enabled: true
master:
persistence:
size: 5Gi
Templates
_helpers.tpl (Template Functions):
{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "myapp.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 10
periodSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
- name: ENVIRONMENT
value: {{ .Values.config.environment }}
- name: LOG_LEVEL
value: {{ .Values.config.logLevel }}
- name: DATABASE_HOST
value: {{ .Values.config.database.host }}
- name: DATABASE_PORT
value: {{ .Values.config.database.port | quote }}
- name: DATABASE_NAME
value: {{ .Values.config.database.name }}
{{- if .Values.secrets.database.username }}
- name: DATABASE_USER
valueFrom:
secretKeyRef:
name: {{ include "myapp.fullname" . }}
key: db-username
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "myapp.fullname" . }}
key: db-password
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
service.yaml:
apiVersion: v1
kind: Service
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "myapp.selectorLabels" . | nindent 4 }}
ingress.yaml:
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "myapp.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
Helm Hooks
Pre-Install Hook (Database Migration):
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "myapp.fullname" . }}-migration
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
template:
metadata:
name: {{ include "myapp.fullname" . }}-migration
spec:
restartPolicy: Never
containers:
- name: migration
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["./migrate.sh"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "myapp.fullname" . }}
key: db-url
Test Hook:
apiVersion: v1
kind: Pod
metadata:
name: {{ include "myapp.fullname" . }}-test
annotations:
"helm.sh/hook": test
spec:
containers:
- name: curl
image: curlimages/curl:latest
command: ['curl']
args: ['{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health']
restartPolicy: Never
Helm Commands
Chart Management:
# Create new chart
helm create myapp
# Lint chart
helm lint ./myapp
# Validate templates
helm template myapp ./myapp
helm template myapp ./myapp --debug
# Package chart
helm package ./myapp
helm package ./myapp --version 1.2.3
# Dependency management
helm dependency list ./myapp
helm dependency update ./myapp
helm dependency build ./myapp
Installation:
# Install release
helm install myapp ./myapp
helm install myapp ./myapp -f custom-values.yaml
helm install myapp ./myapp --set image.tag=v2.0.0
helm install myapp ./myapp --dry-run --debug
# Install with custom namespace
helm install myapp ./myapp -n production --create-namespace
# Wait for resources
helm install myapp ./myapp --wait --timeout 10m
Upgrades:
# Upgrade release
helm upgrade myapp ./myapp
helm upgrade myapp ./myapp -f values.yaml
helm upgrade --install myapp ./myapp # Install if not exists
# Upgrade with reuse of values
helm upgrade myapp ./myapp --reuse-values
helm upgrade myapp ./myapp --reset-values
# Atomic upgrade (rollback on failure)
helm upgrade myapp ./myapp --atomic --timeout 5m
Rollback:
# List revisions
helm history myapp
# Rollback to previous
helm rollback myapp
# Rollback to specific revision
helm rollback myapp 3
# Rollback with cleanup
helm rollback myapp 3 --cleanup-on-fail
Repository Management:
# Add repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add stable https://charts.helm.sh/stable
# Update repositories
helm repo update
# Search charts
helm search repo nginx
helm search hub wordpress
# Show chart info
helm show chart bitnami/postgresql
helm show values bitnami/postgresql
helm show readme bitnami/postgresql
Release Management:
# List releases
helm list
helm list -n production
helm list --all-namespaces
# Release status
helm status myapp
helm get values myapp
helm get manifest myapp
helm get notes myapp
# Uninstall
helm uninstall myapp
helm uninstall myapp --keep-history
Best Practices
1. Use Semantic Versioning
# Chart.yaml
version: 1.2.3 # MAJOR.MINOR.PATCH
appVersion: "2.0.1"
2. Validate Values
// values.schema.json
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["replicaCount", "image"],
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1
},
"image": {
"type": "object",
"required": ["repository", "tag"],
"properties": {
"repository": {"type": "string"},
"tag": {"type": "string"}
}
}
}
}
3. Use Template Functions
# Use include for labels
labels:
{{- include "myapp.labels" . | nindent 4 }}
# Quote strings
value: {{ .Values.config.value | quote }}
# Default values
replicas: {{ .Values.replicaCount | default 3 }}
4. Config Checksums
# Force pod restart on config change
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
5. Document Values
# values.yaml with comments
# Number of replicas to deploy
replicaCount: 3
Anti-Patterns
1. Hardcoded Values:
# BAD
replicas: 3
# GOOD
replicas: {{ .Values.replicaCount }}
2. No Resource Limits:
# GOOD: Always define resources
resources:
limits:
memory: 512Mi
requests:
memory: 256Mi
3. Secrets in Values:
# BAD: Plain secrets in values.yaml
# GOOD: Use external secret management (Vault, Sealed Secrets)
Approach
When developing Helm charts:
-
Start Simple: Use
helm createas template - Parameterize: Make everything configurable
-
Test Thoroughly: Use
helm templateandhelm lint - Document: Add NOTES.txt and comments
- Version: Follow semantic versioning
- Dependencies: Manage with Chart.yaml
- Security: Never commit secrets
Always create charts that are reusable, maintainable, and production-ready.
Resources
- Helm Documentation: https://helm.sh/docs/
- Chart Best Practices: https://helm.sh/docs/chart_best_practices/
- Artifact Hub: https://artifacthub.io/
- Helm Hub: https://hub.helm.sh/
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!