Helm charts are the secret sauce that makes packaging and deploying microservices on Kubernetes feel less like a chaotic free-for-all and more like a well-oiled machine.

Imagine you’ve got a microservice, say, a user authentication service. It needs a deployment, a service to expose it, maybe a config map for its settings, and perhaps a persistent volume claim if it stores anything. Manually creating all these Kubernetes YAML files for each service, and then trying to keep them in sync as your application evolves, is a recipe for disaster. Helm charts bundle all these Kubernetes resources into a single, versionable package.

Here’s a simple Helm chart for our hypothetical auth-service:

# Create a new chart named auth-service
helm create auth-service

This command generates a directory structure like this:

auth-service/
├── charts/
├── charts.lock
├── Chart.yaml
├── templates/
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   └── serviceaccount.yaml
├── .helmignore
└── values.yaml

The templates/ directory is where the magic happens. It contains Go templating language files that are rendered into actual Kubernetes YAML using data from values.yaml and any passed-in configuration.

Let’s look at templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:

  name: {{ include "auth-service.fullname" . }}

  labels:

    {{- include "auth-service.labels" . | nindent 4 }}

spec:

  replicas: {{ .Values.replicaCount }}

  selector:
    matchLabels:

      {{- include "auth-service.selectorLabels" . | nindent 6 }}

  template:
    metadata:
      labels:

        {{- include "auth-service.selectorLabels" . | nindent 8 }}

    spec:
      containers:

        - name: {{ .Chart.Name }}


          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"


          imagePullPolicy: {{ .Values.image.pullPolicy }}

          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
          readinessProbe:
            httpGet:
              path: /readyz
              port: http
          resources:

            {{- toYaml .Values.resources | nindent 10 }}

And here’s the corresponding values.yaml:

replicaCount: 1

image:
  repository: nginx
  pullPolicy: IfNotPresent
  tag: "1.21.6" # Example version

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  create: true
  annotations: {}
  name: ""

podAnnotations: {}
podLabels: {}

podSecurityContext: {}
securityContext: {}

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  className: ""
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

resources: {}
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80
  targetMemoryUtilizationPercentage: 80

nodeSelector: {}
tolerations: []
affinity: {}

When you deploy this chart, Helm takes values.yaml and the templates, renders them, and applies the resulting YAML to your Kubernetes cluster. For instance, if you want to deploy a specific version of your auth-service image, you’d override image.tag:

helm install my-auth-release ./auth-service --set image.tag=v1.2.0

This single command creates a Deployment with 1 replica, using the my-docker-registry/auth-service:v1.2.0 image, and a ClusterIP service exposing port 80.

The power comes from templating. You can define complex logic, conditional statements, and loops within your templates, making charts highly dynamic and reusable. For example, you can conditionally enable an Ingress based on a value:


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

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:

  name: {{ include "auth-service.fullname" . }}

  labels:

    {{- include "auth-service.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.hosts }}

  rules:

    {{- range .Values.ingress.hosts }}


    - host: {{ .host | quote }}

      http:
        paths:

          {{- range .paths }}


          - path: {{ .path }}


            pathType: {{ .pathType }}

            backend:
              service:

                name: {{ include "auth-service.fullname" . }}

                port:
                  number: 80

          {{- end }}


    {{- end }}


  {{- end }}


  {{- if .Values.ingress.tls }}

  tls:

    {{- range .Values.ingress.tls }}

    - hosts:

        {{- range .hosts }}


        - {{ . | quote }}


        {{- end }}


      secretName: {{ .secretName }}


    {{- end }}


  {{- end }}


{{- end }}

This allows users of your chart to simply set ingress.enabled: true in their values.yaml to spin up an ingress resource, without needing to modify the chart’s templates.

One of the most underrated aspects of Helm is its ability to manage releases as discrete, versioned entities. When you helm install or helm upgrade, Helm records the exact chart version and values used. This means you can roll back to a previous state with helm rollback <release-name> <revision-number> if something goes wrong, and you have a clear audit trail of what was deployed.

Beyond packaging, Helm provides a robust ecosystem for sharing charts through chart repositories. Companies often maintain internal repositories for their proprietary services, and public repositories like Artifact Hub host thousands of community-maintained charts for popular applications (databases, caches, monitoring tools, etc.). This dramatically speeds up the deployment of complex, multi-service applications.

The next step after mastering basic chart creation is understanding Helm hooks for advanced lifecycle management.

Want structured learning?

Take the full Microservices course →