Introduction


Helm is the de facto package manager for Kubernetes, enabling developers to define, install, and upgrade complex applications through reusable chart packages. A single Helm chart can encapsulate dozens of Kubernetes resources into a versioned, configurable unit that can be deployed across multiple environments with minimal repetition.


This guide covers advanced Helm concepts including chart structure, templating, dependency management, CI/CD integration, and enterprise best practices.


Chart Structure


A well-organized Helm chart follows a standard directory layout:



my-app/

  Chart.yaml          # Metadata: name, version, dependencies

  values.yaml         # Default configuration values

  values.schema.json  # JSON Schema for values validation

  charts/             # Sub-charts (managed by helm dependency)

  templates/          # Go template YAML files

    _helpers.tpl      # Named template definitions

    deployment.yaml

    service.yaml

    ingress.yaml

    hpa.yaml

    tests/            # Test pods for chart validation

      test-connection.yaml

  crds/               # Custom Resource Definitions

  README.md


The `Chart.yaml` file defines metadata and dependencies:



apiVersion: v2

name: my-app

description: A production-grade web application

version: 1.2.3

appVersion: 2.0.0

kubeVersion: ">=1.25.0-0"

type: application

dependencies:

  - name: postgresql

    version: "12.x"

    repository: https://charts.bitnami.com/bitnami

    condition: postgresql.enabled

  - name: redis

    version: "18.x"

    repository: https://charts.bitnami.com/bitnami

    condition: redis.enabled


Advanced Templating


Helm uses Go templates with the Sprig function library for dynamic resource generation. Beyond simple variable substitution, you can implement complex logic:



{{- /* Conditional resource creation */}}

{{- if .Values.ingress.enabled }}

apiVersion: networking.k8s.io/v1

kind: Ingress

metadata:

  name: {{ include "my-app.fullname" . }}

  annotations:

    {{- toYaml .Values.ingress.annotations | nindent 4 }}

spec:

  ingressClassName: {{ .Values.ingress.className }}

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


Named templates in `_helpers.tpl` promote reuse:



{{- define "my-app.labels" -}}

app.kubernetes.io/name: {{ include "my-app.name" . }}

app.kubernetes.io/instance: {{ .Release.Name }}

app.kubernetes.io/version: {{ .Chart.AppVersion }}

app.kubernetes.io/managed-by: {{ .Release.Service }}

helm.sh/chart: {{ include "my-app.chart" . }}

{{- end -}}



{{- define "my-app.probe" -}}

initialDelaySeconds: {{ .initialDelay | default 5 }}

periodSeconds: {{ .period | default 10 }}

timeoutSeconds: {{ .timeout | default 3 }}

successThreshold: {{ .successThreshold | default 1 }}

failureThreshold: {{ .failureThreshold | default 3 }}

{{- end -}}


Values Management and Validation


JSON Schema validation catches configuration errors early:



{

  "$schema": "https://json-schema.org/draft-07/schema#",

  "type": "object",

  "properties": {

    "replicaCount": {

      "type": "integer",

      "minimum": 1,

      "maximum": 100

    },

    "image": {

      "type": "object",

      "properties": {

        "repository": { "type": "string" },

        "tag": { "type": "string", "pattern": "^v?[0-9]" },

        "pullPolicy": {

          "type": "string",

          "enum": ["Always", "IfNotPresent", "Never"]

        }

      },

      "required": ["repository"]

    },

    "resources": {

      "type": "object",

      "properties": {

        "limits": { "$ref": "#/$defs/ResourceSpec" },

        "requests": { "$ref": "#/$defs/ResourceSpec" }

      }

    }

  },

  "required": ["image", "replicaCount"]

}


Environment-specific overrides keep values DRY:



# values.yaml (defaults)

replicaCount: 1

image:

  tag: latest



# values-staging.yaml

replicaCount: 2

image:

  tag: staging-abc123

ingress:

  enabled: true

  hosts:

    - host: staging.my-app.com



# values-production.yaml

replicaCount: 6

image:

  tag: v2.0.0

resources:

  limits:

    cpu: 2

    memory: 4Gi

  requests:

    cpu: 1

    memory: 2Gi

ingress:

  enabled: true

  hosts:

    - host: my-app.com

    - host: www.my-app.com


Dependency Management


Lock dependency versions with `Chart.lock`:



helm dependency update ./charts/my-app


Use alias and condition fields for environment-optimized dependencies:



dependencies:

  - name: postgresql

    alias: primary-db

    version: "12.x"

    repository: https://charts.bitnami.com/bitnami

    condition: primary-db.enabled

  - name: postgresql

    alias: replica-db

    version: "12.x"

    repository: https://charts.bitnami.com/bitnami

    condition: replica-db.enabled


CI/CD Integration


Integrate Helm linting and testing into pipelines:



# .github/workflows/helm-release.yml

name: Helm Release

on:

  push:

    branches: [main]

jobs:

  lint:

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: azure/setup-helm@v3

        with:

          version: "v3.14.0"

      - run: helm lint ./charts/my-app

      - run: helm template --validate ./charts/my-app



  release:

    needs: lint

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: azure/setup-helm@v3

      - name: Package and push to OCI registry

        run: |

          helm package ./charts/my-app

          helm push my-app-*.tgz oci://ghcr.io/${{ github.repository }}/charts


Chart Versioning and Publishing


Use OCI registries for chart distribution:



# Login and push

helm registry login ghcr.io -u $USER

helm push my-app-1.2.3.tgz oci://ghcr.io/my-org/charts



# Install from OCI

helm install my-app oci://ghcr.io/my-org/charts/my-app --version 1.2.3


Follow semantic versioning strictly. Breaking template changes require a major version bump, as `helm upgrade` must never silently break deployed resources.


Best Practices


  • **Use `helm create` scaffolds** as a starting point but customize helpers aggressively.
  • **Validate in CI**: run `helm lint`, `helm template --validate`, and `kubeconform` on every PR.
  • **Unit test templates**: use the `helm-unittest` plugin to test template output without a cluster.
  • **Library charts**: extract common helpers into a library chart (`type: library`) shared across microservices.
  • **Avoid sprints in templates**: complex logic belongs in application code, not chart templates.
  • **Resource policy annotations**: use `helm.sh/resource-policy: keep` sparingly and document its use.

  • Helm remains the most widely adopted packaging tool in the Kubernetes ecosystem, and mastering its advanced features is essential for operating production-grade workloads at scale.