Helm Charts: Kubernetes Package Management


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.