Skip to main content

Creating and Managing Helm Charts

A Helm Chart is the standard way to package, configure, and deploy Kubernetes applications. This document explains the essential components and practices for creating custom Helm Charts.

What is a Helm Chart?​

A Helm Chart is a collection of template files that define Kubernetes resources. It provides several benefits:

  • Reusability: Easily deploy the same application across different environments
  • Version Control: Track application versions
  • Configuration Management: Apply different configurations per environment
  • Dependency Management: Define dependencies on other Charts

Basic Helm Chart Structure​

mychart/
├── Chart.yaml # Chart metadata
├── values.yaml # Default configuration values
├── charts/ # Dependent charts
├── templates/ # Kubernetes manifest templates
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── _helpers.tpl # Template helper functions
│ └── NOTES.txt # Post-installation notes
└── .helmignore # Files to exclude from packaging

Chart.yaml - Chart Metadata​

Chart.yaml defines the basic information about the Chart.

apiVersion: v2
name: myapp
description: A Helm chart for custom application
type: application

# Chart version (version of the Chart itself)
version: 1.0.0

# Application version
appVersion: "2.1.0"

# Keywords (for search)
keywords:
- web
- api
- microservice

# Maintainer information
maintainers:
- name: Developer Name
email: developer@example.com
url: https://example.com

# Homepage URL
home: https://github.com/example/myapp

# Source code repositories
sources:
- https://github.com/example/myapp

# Chart dependencies
dependencies:
- name: postgresql
version: "12.1.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
- name: redis
version: "17.3.0"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled

# Icon URL
icon: https://example.com/icon.png

# If deprecated
# deprecated: true

# KubeVersion constraints
kubeVersion: ">=1.24.0-0"

Important Chart.yaml Fields​

FieldRequiredDescription
apiVersion✓Chart API version (v2 recommended)
name✓Chart name (must match directory name)
version✓Chart version (SemVer)
appVersionVersion of the deployed application
typeEither application or library
dependenciesList of dependent charts

values.yaml - Default Configuration Values​

values.yaml defines the default values used in templates.

# Number of replicas
replicaCount: 3

# Docker image configuration
image:
repository: myregistry.azurecr.io/myapp
pullPolicy: IfNotPresent
tag: "2.1.0"

# Image pull secrets
imagePullSecrets:
- name: acr-secret

# Service account
serviceAccount:
create: true
annotations: {}
name: ""

# Pod annotations
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"

# Pod security context
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000

# Container security context
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true

# Service configuration
service:
type: ClusterIP
port: 80
targetPort: 8080
annotations: {}

# Ingress configuration
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: myapp-tls
hosts:
- myapp.example.com

# Resource limits
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi

# Autoscaling
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80

# Health checks
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10

readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5

# Environment variables
env:
- name: APP_ENV
value: "production"
- name: LOG_LEVEL
value: "info"

# Environment variables from ConfigMap
envFrom:
- configMapRef:
name: myapp-config

# Secrets
secrets:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url

# Volumes
volumes:
- name: config
configMap:
name: myapp-config
- name: cache
emptyDir: {}

volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
- name: cache
mountPath: /tmp/cache

# Node selector
nodeSelector: {}

# Tolerations
tolerations: []

# Affinity
affinity: {}

Template Files​

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
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.env }}
env:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- 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: {{ .Values.service.targetPort }}
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 }}

_helpers.tpl - Helper Templates​

_helpers.tpl defines reusable template functions. This file is not rendered as a Kubernetes resource.

{{/*
Expand the name of the chart
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a fully qualified app name
Truncated at 63 chars because some Kubernetes name fields are limited
*/}}
{{- 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
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

{{/*
Environment variables template
*/}}
{{- define "myapp.env" -}}
- name: APP_NAME
value: {{ include "myapp.fullname" . }}
- name: APP_VERSION
value: {{ .Chart.AppVersion | quote }}
- name: RELEASE_NAME
value: {{ .Release.Name }}
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
{{- end }}

{{/*
Conditional labels
*/}}
{{- define "myapp.podLabels" -}}
{{- if .Values.podLabels }}
{{- toYaml .Values.podLabels }}
{{- end }}
{{- end }}

Key Uses of Helper Functions​

  • Naming Conventions: Generate resource names consistently
  • Label Standardization: Follow Kubernetes best practices for labels
  • Conditional Logic: Customize templates based on configuration
  • Code Deduplication: Reuse common template parts

Usage Examples of Helper Functions​

Defined helper functions are called from other template files using the include function.

Definition (_helpers.tpl):

{{- define "myapp.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

Usage (deployment.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
# Call the defined function to expand the name
name: {{ include "myapp.fullname" . }}

Since the include function returns output as a string, it can be passed to other functions (like indent or nindent) via a pipeline. This is crucial for maintaining YAML indentation structure.

labels:
# Indent the output by 4 spaces
{{- include "myapp.labels" . | nindent 4 }}

Template Functions and Pipelines​

Helm uses Go Templates and the Sprig function library.

Basic Template Syntax​

# Value reference
{{ .Values.replicaCount }}

# Default value
{{ .Values.image.tag | default .Chart.AppVersion }}

# Conditional
{{- if .Values.ingress.enabled }}
# Create Ingress resource
{{- end }}

# Range loop
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value }}
{{- end }}

# Pipeline (chaining functions)
{{ .Values.name | upper | quote }}

# Indentation
{{- toYaml .Values.resources | nindent 12 }}

# Calling helper functions
{{ include "myapp.fullname" . }}

Commonly Used Functions​

FunctionDescriptionExample
defaultProvide default value{{ .Values.tag | default "latest" }}
quoteQuote a string{{ .Values.name | quote }}
upper/lowerConvert case{{ .Values.env | upper }}
truncTruncate string{{ .Values.name | trunc 63 }}
trimSuffixRemove suffix{{ .Values.name | trimSuffix "-" }}
nindentAdd indentation{{ toYaml .Values | nindent 4 }}
toYamlConvert to YAML{{ toYaml .Values.resources }}
sha256sumSHA256 hash{{ .Values.config | sha256sum }}

Chart Packaging - .tgz Files​

Helm Charts are packaged as .tgz (tar.gz) archives. This file is used for publishing to Chart repositories or transferring to air-gapped environments (environments without internet access).

Filename Convention​

The package filename is automatically generated based on the name and version defined in Chart.yaml.

Format: <chart-name>-<chart-version>.tgz

Example:

  • Chart Name: myapp
  • Version: 1.0.0
  • Generated Filename: myapp-1.0.0.tgz

Creating a Package​

# Package the chart
helm package mychart/

# Output: myapp-1.0.0.tgz

Package Contents​

The .tgz file contains the source files of the Chart itself, not the rendered Kubernetes manifests. During installation, Helm extracts this package and renders the templates on the fly.

# View package contents
tar -tzf myapp-1.0.0.tgz

# Output:
# myapp/Chart.yaml
# myapp/values.yaml
# myapp/templates/deployment.yaml
# myapp/templates/service.yaml
# ...

.helmignore​

.helmignore specifies files to exclude from packaging.

# Development files
*.swp
*.bak
*.tmp
*.orig
*~

# Git-related
.git/
.gitignore

# CI/CD
.github/
.gitlab-ci.yml

# Tests
tests/
*.test

# Documentation
README.md
CONTRIBUTING.md

# Environment-specific files
values-dev.yaml
values-staging.yaml

Chart Development Workflow​

1. Creating a Chart​

# Create a new chart
helm create myapp

# Directory structure is generated

2. Developing and Testing Templates​

# View rendered templates
helm template myapp ./myapp

# Use specific values file
helm template myapp ./myapp -f values-prod.yaml

# Debug mode with detailed output
helm template myapp ./myapp --debug

3. Validating the Chart​

# Check chart syntax
helm lint myapp/

# Update dependencies
helm dependency update myapp/

# Dry run (doesn't actually install)
helm install myapp ./myapp --dry-run --debug

4. Packaging the Chart​

# Package the chart
helm package myapp/

# Signed package (optional)
helm package myapp/ --sign --key 'keyname' --keyring path/to/keyring

5. Installing the Chart​

# Install from local chart
helm install my-release ./myapp

# Install from package
helm install my-release myapp-1.0.0.tgz

# Specify custom values
helm install my-release ./myapp -f custom-values.yaml

# Override values via command line
helm install my-release ./myapp --set replicaCount=5

# Install to specific namespace
helm install my-release ./myapp -n production --create-namespace

Environment-Specific Configuration​

Use different configurations for different environments (dev, staging, production).

values-dev.yaml​

replicaCount: 1

image:
tag: "dev"

ingress:
enabled: false

resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi

autoscaling:
enabled: false

env:
- name: APP_ENV
value: "development"
- name: LOG_LEVEL
value: "debug"

values-prod.yaml​

replicaCount: 5

image:
tag: "2.1.0"

ingress:
enabled: true
hosts:
- host: myapp.production.com
paths:
- path: /
pathType: Prefix

resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi

autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 20

env:
- name: APP_ENV
value: "production"
- name: LOG_LEVEL
value: "warn"

Usage​

# Development environment
helm install myapp-dev ./myapp -f values-dev.yaml

# Production environment
helm install myapp-prod ./myapp -f values-prod.yaml -n production

# Merge multiple values files (later files take precedence)
helm install myapp ./myapp -f values.yaml -f values-prod.yaml -f values-override.yaml

Umbrella Chart Pattern​

The Umbrella Chart is a Helm design pattern for managing multiple microservices or applications together.

Overview​

An Umbrella Chart itself has few templates (templates/) and specializes in defining other Charts (sub-charts) in the dependencies section of Chart.yaml. This allows you to deploy and manage a complex system as a single release.

Key Use Cases​

  • Microservices Composition: Manage multiple services (Frontend, Backend, DB, etc.) collectively.
  • Dependency Integration: Provide applications and necessary middleware (Redis, PostgreSQL, etc.) as a set.

Structure Example​

umbrella-app/
├── Chart.yaml # Define dependencies
├── values.yaml # Global configuration (override sub-chart settings)
├── charts/ # Where dependent Charts are downloaded
└── templates/ # Usually empty, or common ConfigMap/Secret only

Chart.yaml Configuration​

apiVersion: v2
name: my-complex-app
version: 1.0.0
type: application

dependencies:
- name: frontend
version: 1.2.0
repository: http://my-charts.com
- name: backend
version: 2.3.0
repository: http://my-charts.com
- name: postgresql
version: 12.1.0
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled

Overriding Settings in values.yaml​

You can override sub-chart settings from the parent Chart's (Umbrella Chart) values.yaml. Use the sub-chart name as the key.

# Global settings (shared across all charts)
global:
imageRegistry: myregistry.azurecr.io

# frontend sub-chart settings
frontend:
replicaCount: 3
service:
type: LoadBalancer

# backend sub-chart settings
backend:
env:
- name: DB_HOST
value: "my-complex-app-postgresql"

# postgresql sub-chart settings
postgresql:
enabled: true
auth:
database: mydb
username: myuser

Commands​

To resolve dependencies and prepare the Chart, use the following commands:

# Download dependent Charts and place them in the charts/ directory
helm dependency build ./umbrella-app

# Or
helm dependency update ./umbrella-app

Publishing to Chart Repositories​

Using ChartMuseum​

# Upload to ChartMuseum
curl --data-binary "@myapp-1.0.0.tgz" http://chartmuseum.example.com/api/charts

# Add repository
helm repo add myrepo http://chartmuseum.example.com
helm repo update

# Install from repository
helm install my-release myrepo/myapp

Using Azure Container Registry (ACR)​

# Login to ACR
az acr login --name myregistry

# Push chart as OCI artifact
helm push myapp-1.0.0.tgz oci://myregistry.azurecr.io/helm

# Install from ACR
helm install my-release oci://myregistry.azurecr.io/helm/myapp --version 1.0.0

Best Practices​

1. Naming Conventions​

  • Chart name: lowercase, hyphen-separated (e.g., my-app)
  • Resource names: Use {{ include "myapp.fullname" . }}
  • Labels: Use Kubernetes recommended labels

2. Security​

# Always define security context
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL

# Set resource limits
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi

3. Config and Secret Management​

# Add ConfigMap hash to annotations (restart pods on config change)
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

4. Conditional Resources​

# Control feature toggle via values.yaml
{{- if .Values.ingress.enabled }}
# Ingress resource
{{- end }}

5. Documentation​

  • README.md: Overview and usage instructions
  • NOTES.txt: Information displayed after installation
  • values.yaml: Document all configuration options with comments

Example NOTES.txt​

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 }}

To access your application:

{{- if .Values.ingress.enabled }}
Visit: https://{{ (index .Values.ingress.hosts 0).host }}
{{- else }}
Run: kubectl port-forward svc/{{ include "myapp.fullname" . }} 8080:{{ .Values.service.port }}
Then visit: http://localhost:8080
{{- end }}

Troubleshooting​

Debug Commands​

# View rendered templates
helm template myapp ./myapp --debug

# Installation dry run
helm install myapp ./myapp --dry-run --debug

# Check values of installed chart
helm get values my-release

# Check installed manifests
helm get manifest my-release

# View release history
helm history my-release

Common Issues​

  1. Template indentation errors

    • Use nindent function to control indentation precisely
  2. Values not applied

    • Check actual values with helm get values
    • Verify precedence of --set and -f
  3. Dependency issues

    • Run helm dependency update
    • Check charts/ directory

Summary​

Key points for creating custom Helm Charts:

  1. Chart.yaml: Metadata and version control
  2. values.yaml: All configurable parameters
  3. templates/: Kubernetes manifest templates
  4. _helpers.tpl: Reusable template functions
  5. .tgz files: Packaged charts
  6. Environment-specific config: Flexible deployment across environments

Helm Charts are powerful tools that standardize and make Kubernetes application deployment reusable. Properly structured charts significantly improve team productivity.