← Back to DevBytes

Helm Chart Development: Complete Implementation Guide

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:

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:

Helm Chart Development: A Complete Step-by-Step Guide

Prerequisites

Before developing Helm Charts, ensure you have the following installed:

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:

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

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles