What Are Helm Charts?
Helm Charts are the packaging format used by Helm, the Kubernetes package manager. Think of a Chart as the Kubernetes equivalent of a .deb package or a Docker image — it bundles together all the Kubernetes resource definitions (Deployments, Services, ConfigMaps, Secrets, and more) needed to deploy an application into a single, versioned, shareable unit.
A Chart is essentially a collection of YAML templates, default configuration values, and metadata that describes a Kubernetes application. When you install a Chart, Helm renders these templates by substituting in your specific configuration values, producing valid Kubernetes manifests that can be applied to a cluster.
At its core, a Helm Chart contains:
- Template files — parameterized Kubernetes YAML manifests written using Go templating syntax
- Values — default configuration that gets injected into templates
- Metadata — information about the Chart itself (name, version, description, dependencies)
- Hooks — optional scripts that run at specific points during the install/upgrade/delete lifecycle
Why Helm Charts Matter
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →In a world where organizations deploy hundreds of microservices across multiple Kubernetes clusters, managing raw Kubernetes YAML files becomes unsustainable. Helm Charts solve several critical problems:
- Reusability and Parameterization — Instead of copying and pasting Kubernetes manifests for each environment (dev, staging, production), a single Chart can be configured differently via values files. This eliminates duplication and reduces errors.
- Versioning and Release Management — Charts are versioned artifacts. You can track exactly which version of your application's infrastructure is deployed, roll back to previous configurations, and manage the full lifecycle (install, upgrade, rollback, uninstall) with a single command.
- Dependency Management — Charts can declare dependencies on other Charts (such as databases, message queues, or monitoring stacks), ensuring that complex multi-service deployments boot up in the correct order with proper configuration sharing.
- Distribution and Sharing — Charts can be published to Helm repositories (like Artifact Hub or private OCI registries), making it easy for teams to discover, share, and consume standardized application packages across an organization.
- Separation of Concerns — Chart authors define the infrastructure blueprint; operators provide environment-specific values. Developers don't need deep Kubernetes expertise to deploy — they simply override the values relevant to their environment.
Helm Chart Development: A Complete Step-by-Step Guide
Prerequisites
Before developing Helm Charts, ensure you have the following installed:
- Helm CLI (version 3.x or later) — the package manager itself
- kubectl — configured with access to a Kubernetes cluster (or a local cluster like kind/minikube for testing)
- A text editor or IDE — with YAML and Go template syntax highlighting support
- Basic understanding of Kubernetes resources — Deployments, Services, ConfigMaps, Secrets, Ingress, etc.
Verify your Helm installation:
helm version
# Should output something like: version.BuildInfo{Version:"v3.13.0", ...}
Creating Your First Chart
Helm provides a scaffold command that generates the complete directory structure for a new Chart. This is always the recommended starting point rather than building Charts from scratch:
helm create my-app-chart
This command produces the following directory tree:
my-app-chart/
├── Chart.yaml # Chart metadata and version information
├── values.yaml # Default configuration values
├── charts/ # Directory for dependency charts (subcharts)
├── templates/ # Kubernetes manifest templates
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── serviceaccount.yaml
│ ├── _helpers.tpl # Named template helpers (reusable snippets)
│ ├── NOTES.txt # Text displayed after a successful install
│ └── tests/ # Test pod definitions for helm test
│ └── test-connection.yaml
└── .helmignore # Files to exclude from packaging (like .gitignore)
Let's examine each component in detail and then build a production-grade Chart from scratch.
Chart Structure Deep Dive
Chart.yaml — The Metadata File
The Chart.yaml file is the identity document of your Chart. It must contain specific fields and follows a strict schema. Here is a complete, annotated example:
apiVersion: v2 # REQUIRED: Must be v2 for Helm 3 charts
name: my-app-chart # REQUIRED: The name of the chart
version: 1.0.0 # REQUIRED: SemVer version of the chart itself
appVersion: "2.4.1" # OPTIONAL: Version of the application being deployed
description: A Helm chart for deploying the MyApp microservice
type: application # application or library (library charts provide helpers)
keywords:
- web-application
- rest-api
- golang
home: https://github.com/myorg/my-app
sources:
- https://github.com/myorg/my-app
maintainers:
- name: Jane Smith
email: jane@example.com
url: https://github.com/janesmith
icon: https://raw.githubusercontent.com/myorg/my-app/main/logo.png
dependencies:
- name: postgresql
version: "12.1.6"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
tags:
- database
- name: redis
version: "17.3.14"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
tags:
- cache
annotations:
category: Application
licenses: Apache-2.0
Key points about Chart.yaml:
- apiVersion — Always
v2for Helm 3 charts. The olderv1is deprecated. - version — The chart version. This should be incremented every time you make changes to the chart (templates, defaults, etc.). Use semantic versioning.
- appVersion — The version of the application container image. This is informational and can be referenced in templates using
{{ .Chart.AppVersion }}. - type — Use
applicationfor deployable charts. Uselibraryfor charts that only provide reusable helpers and don't produce Kubernetes resources. - dependencies — Declares subcharts that must be fetched and installed alongside your chart. Each dependency specifies a name, version constraint, and repository URL.
values.yaml — The Default Configuration
The values.yaml file defines the default configuration that gets merged with user-provided values during installation. A well-structured values file is crucial for chart usability. Here's a comprehensive example:
# Default values for my-app-chart
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# -- Number of pod replicas to run
replicaCount: 3
# -- Container image configuration
image:
repository: myregistry.io/myorg/my-app
tag: "" # Defaults to Chart.appVersion if empty
pullPolicy: IfNotPresent
# -- Image pull secrets for private registries
pullSecrets: []
# - name: regcred
# -- Service configuration
service:
type: ClusterIP
port: 8080
targetPort: 8080
# -- Additional ports to expose on the service
additionalPorts: []
# - name: metrics
# port: 9090
# targetPort: 9090
# protocol: TCP
# -- Ingress configuration
ingress:
enabled: false
className: "nginx"
annotations: {}
# kubernetes.io/ingress.class: nginx
# cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: my-app.example.com
paths:
- path: /
pathType: Prefix
tls: []
# - secretName: my-app-tls
# hosts:
# - my-app.example.com
# -- Resource limits and requests for the main container
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
# -- Horizontal Pod Autoscaler configuration
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
# -- Pod-level security context
podSecurityContext:
fsGroup: 2000
runAsNonRoot: true
# -- Container-level security context
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
# -- Environment variables injected into the container
env:
- name: LOG_LEVEL
value: "info"
- name: ENVIRONMENT
value: "production"
# -- Environment variables sourced from secrets (use with caution)
envFromSecrets: []
# - name: DB_PASSWORD
# secretName: my-app-secrets
# secretKey: db-password
# -- Volume mounts for the main container
volumeMounts: []
# - name: config-volume
# mountPath: /etc/config
# -- Volumes attached to the pod
volumes: []
# - name: config-volume
# configMap:
# name: my-app-config
# -- Node selector for pod scheduling
nodeSelector: {}
# kubernetes.io/arch: amd64
# -- Tolerations for pod scheduling
tolerations: []
# - key: "dedicated"
# operator: "Equal"
# value: "compute"
# effect: "NoSchedule"
# -- Affinity rules for pod scheduling
affinity: {}
# podAntiAffinity:
# preferredDuringSchedulingIgnoredDuringExecution:
# - weight: 100
# podAffinityTerm:
# labelSelector:
# matchExpressions:
# - key: app.kubernetes.io/name
# operator: In
# values:
# - my-app
# topologyKey: kubernetes.io/hostname
# -- Service account configuration
serviceAccount:
create: true
annotations: {}
name: "" # Defaults to the full chart name if empty
# -- Pod annotations
podAnnotations: {}
# prometheus.io/scrape: "true"
# prometheus.io/port: "9090"
# -- Pod labels (in addition to standard labels)
podLabels: {}
# -- Configuration for ConfigMap creation
configMap:
enabled: true
data:
app-config.yaml: |
server:
port: 8080
read_timeout: 30s
database:
max_connections: 100
connection_timeout: 10s
# -- Liveness probe configuration
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# -- Readiness probe configuration
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# -- Subchart dependency configuration overrides
postgresql:
enabled: true
auth:
database: myappdb
username: myappuser
password: "" # Set this in a sealed secret or external values file
redis:
enabled: false
The values file serves as the single source of truth for configurable parameters. Every template variable should have a sensible default here, and the file should be heavily commented so users understand what each value controls.
Writing Templates — The Heart of Chart Development
Templates live in the templates/ directory and produce Kubernetes manifests when rendered. They use Go's text/template syntax extended with Helm-specific functions from the Sprig library. Let's build each core template file.
_helpers.tpl — Reusable Named Templates
The _helpers.tpl file (note the leading underscore — files beginning with underscore are not rendered as standalone manifests) defines named templates that can be called from other templates. This promotes DRY principles and consistency across your chart's manifests:
{{/*
Create chart name and version labels as recommended by Kubernetes Helm best practices.
*/}}
{{- define "my-app.chart.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
{{- end -}}
{{/*
Common labels applied to all resources.
Do not modify this block — it is injected into every manifest via the selector.
*/}}
{{- define "my-app.labels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/component: server
{{- end -}}
{{/*
Selector labels used for Pod selection and Service targeting.
These must match the labels on the Pod template.
*/}}
{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
Expand the name of the chart, truncating at 63 characters to comply with Kubernetes naming rules.
*/}}
{{- define "my-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a fully qualified app name.
Truncate at 63 characters, and use the release name if nameOverride is set.
*/}}
{{- define "my-app.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 the name of the service account to use.
*/}}
{{- define "my-app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "my-app.fullname" .) .Values.serviceAccount.name -}}
{{- else -}}
{{- default "default" .Values.serviceAccount.name -}}
{{- end -}}
{{- end -}}
Named templates defined with define can be invoked using include or template. The difference is that include returns the rendered text as a string (allowing further piping), while template writes directly to the output stream and cannot be piped. Always prefer include for maximum flexibility.
deployment.yaml — The Core Workload Template
The Deployment template ties together all the configuration values into a complete Kubernetes Deployment resource:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit | default 10 }}
strategy:
{{- if .Values.strategy }}
{{- toYaml .Values.strategy | nindent 4 }}
{{- else }}
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
{{- end }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-app.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "my-app.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 }}
{{- if .Values.env }}
env:
{{- toYaml .Values.env | nindent 12 }}
{{- end }}
{{- if .Values.envFromSecrets }}
envFrom:
{{- range .Values.envFromSecrets }}
- secretRef:
name: {{ .secretName }}
{{- end }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
{{- range .Values.service.additionalPorts }}
- name: {{ .name }}
containerPort: {{ .targetPort }}
protocol: {{ .protocol | default "TCP" }}
{{- end }}
{{- if .Values.livenessProbe }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
{{- end }}
{{- if .Values.readinessProbe }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- 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 — Network Exposure
apiVersion: v1
kind: Service
metadata:
name: {{ include "my-app.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}
{{- if and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{- toYaml .Values.service.loadBalancerSourceRanges | nindent 4 }}
{{- end }}
selector:
{{- include "my-app.selectorLabels" . | nindent 4 }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
{{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
{{- range .Values.service.additionalPorts }}
- name: {{ .name }}
port: {{ .port }}
targetPort: {{ .targetPort }}
protocol: {{ .protocol | default "TCP" }}
{{- if and (eq $.Values.service.type "NodePort") .nodePort }}
nodePort: {{ .nodePort }}
{{- end }}
{{- end }}
ingress.yaml — External HTTP Access
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "my-app.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "my-app.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:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "my-app.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
configmap.yaml — Application Configuration
{{- if .Values.configMap.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "my-app.fullname" . }}-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
data:
{{- range $key, $value := .Values.configMap.data }}
{{ $key }}: |
{{- $value | nindent 4 }}
{{- end }}
{{- end }}
hpa.yaml — Horizontal Pod Autoscaler
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "my-app.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "my-app.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
serviceaccount.yaml — RBAC Identity
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "my-app.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
NOTES.txt — Post-Installation Instructions
The NOTES.txt file is rendered and displayed to the user after a successful helm install, helm upgrade, or helm rollback. Use it to provide helpful information:
Thank you for installing {{ .Chart.Name }} (version {{ .Chart.Version }}).
Your release is named {{ .Release.Name }} and is deployed in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }}
To access your application via the ingress, use the following URL(s):
{{- range .Values.ingress.hosts }}
https://{{ .host }}
{{- end }}
{{- else if eq .Values.service.type "LoadBalancer" }}
To get the external IP address of the LoadBalancer service, run:
export SERVICE_IP=$(kubectl get svc {{ include "my-app.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "http://$SERVICE_IP:{{ .Values.service.port }}"
{{- else if eq .Values.service.type "ClusterIP" }}
The service is exposed as ClusterIP on port {{ .Values.service.port }}.
To access it locally, run:
kubectl port-forward svc/{{ include "my-app.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} -n {{ .Release.Namespace }}
Then visit: http://localhost:{{ .Values.service.port }}
{{- end }}
{{- if .Values.configMap.enabled }}
Application configuration is stored in ConfigMap:
kubectl get configmap {{ include "my-app.fullname" . }}-config -n {{ .Release.Namespace }} -o yaml
{{- end }}
For more information, see the project documentation at {{ .Chart.Home }}.
Helper Functions and Named Templates — Advanced Patterns
Beyond basic _helpers.tpl entries, you can create sophisticated named templates that encapsulate complex logic. Here are several advanced patterns:
{{/*
Generate a comma-separated list of DNS names for the certificate SAN field.
Usage: {{ include "my-app.sanList" (list "example.com" "*.example.com") }}
*/}}
{{- define "my-app.sanList" -}}
{{- $hosts := . -}}
{{- range $i, $host := $hosts -}}
{{- if $i }},{{ end -}}
{{- $host -}}
{{- end -}}
{{- end -}}
{{/*
Render a complete PodDisruptionBudget if enabled.
*/}}
{{- define "my-app.pdb" -}}
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
{{- if .Values.podDisruptionBudget.minAvailable }}
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
{{- else if .Values.podDisruptionBudget.maxUnavailable }}
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
{{- end }}
{{- end }}
{{/*
Compute the appropriate resource tier labels based on requested memory.
Usage: {{ include "my-app.resourceTier" .Values.resources.requests.memory }}
*/}}
{{- define "my-app.resourceTier" -}}
{{- $memory := . -}}
{{- if kindIs "string" $memory -}}
{{- $memory = regexFind "^[0-9]+" $memory | int -}}
{{- end -}}
{{- if lt $memory 256 -}}tier: small
{{- else if lt $memory 1024 -}}tier: medium
{{- else if lt $memory 4096 -}}tier: large
{{- else -}}tier: xlarge
{{- end -}}
{{- end -}}
Conditional Logic and Control Flow in Templates
Helm templates support a rich set of control structures. Understanding these is essential for building flexible charts:
{{/* Example: Conditional block with if/else */}}
{{- if .Values.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
{{- else }}
volumes:
- name: data
emptyDir: {}
{{- end }}
{{/* Example: Looping with range over a list */}}
{{- range .Values.additionalVolumes }}
- name: {{ .name }}
{{- if .persistentVolumeClaim }}
persistentVolumeClaim:
claimName: {{ .persistentVolumeClaim.claimName }}
{{- else if .configMap }}
configMap:
name: {{ .configMap.name }}
defaultMode: {{ .configMap.defaultMode | default 420 }}
{{- else if .secret }}
secret:
secretName: {{ .secret.secretName }}
{{- end }}
{{- end }}
{{/* Example: Using with to scope into a nested object */}}
{{- with .Values.global.ingress }}
{{- if .enabled }}
ingress:
enabled: true
annotations:
{{- range $key, $value := .annotations }}
{{ $key }}: {{ $value }}
{{- end }}
{{- end }}
{{- end }}
{{/* Example: Default values with the default function */}}
imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }}
replicas: {{ .Values.replicaCount | default 1 }}
{{/* Example: Required values validation */}}
{{- if not .Values.database.password }}
{{- fail "ERROR: database.password must be set in values.yaml or passed via --set" }}
{{- end }}
The fail function is particularly useful for enforcing that critical values are provided. It causes the template rendering to stop with an error message, preventing deployments with missing configuration.
Using Subcharts and Dependencies
When your application depends on external services (databases, caches, message queues), you can declare them as dependencies in Chart.yaml. After declaring dependencies, run helm dependency update to fetch them into the charts/ directory:
# Update dependencies declared in Chart.yaml
helm dependency update ./my-app-chart
# Verify the dependency chart versions
helm dependency list ./my-app-chart
# Build the chart including all dependencies
helm dependency build ./my-app-chart
You can override subchart values by nesting them under the dependency name in your values.yaml:
# In your parent chart's values.yaml:
postgresql:
enabled: true
auth:
database: myappdb
username: myappuser
existingSecret: postgres-secret # Reference an existing secret
primary:
persistence:
size: 50Gi
resources:
requests:
cpu: 500m
memory: 1Gi
metrics:
enabled: true
redis:
enabled: true