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
apiVersionChart API version (v2 recommended)
nameChart name (must match directory name)
versionChart 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.