Home/Blog/Mastering Helm Charts for Microservices
KUBERNETES

Mastering Helm Charts for Microservices

calendar_todayFeb 28, 2026schedule5 min readpersonArtem Porubai

When you manage 30+ microservices, each with its own Deployment, Service, Ingress, HPA, and PDB, copy-pasting YAML between repos becomes a maintenance nightmare. Helm library charts let you define rendering logic once and share it across every service chart. This article walks through building a production-grade library chart and consuming it from individual service charts.

Chart Directory Structure

A well-organized Helm monorepo looks like this:

charts/
  lib-microservice/          # Library chart (type: library)
    Chart.yaml
    templates/
      _deployment.tpl
      _service.tpl
      _ingress.tpl
      _hpa.tpl
      _pdb.tpl
      _helpers.tpl
    values.schema.json       # JSON Schema for values validation
 
  user-service/              # Application chart
    Chart.yaml               # depends on lib-microservice
    values.yaml
    values-staging.yaml
    values-production.yaml
    templates/
      deployment.yaml        # calls lib-microservice templates
      service.yaml
      ingress.yaml
    tests/
      connection_test.yaml
 
  order-service/             # Another application chart
    Chart.yaml
    values.yaml
    ...

Building the Library Chart

The library chart's Chart.yaml must declare type: library. It cannot be installed directly -- it only provides named templates.

# charts/lib-microservice/Chart.yaml
apiVersion: v2
name: lib-microservice
version: 1.4.0
type: library
description: Shared templates for all microservice Helm charts

Here is the core deployment template. It encapsulates best practices: resource limits, liveness/readiness probes, graceful shutdown, topology spread constraints, and pod disruption budgets.

{{/* charts/lib-microservice/templates/_deployment.tpl */}}
{{- define "lib-microservice.deployment" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "lib-microservice.fullname" . }}
  labels:
    {{- include "lib-microservice.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "lib-microservice.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }}
      labels:
        {{- include "lib-microservice.selectorLabels" . | nindent 8 }}
    spec:
      terminationGracePeriodSeconds: {{ .Values.terminationGracePeriod | default 30 }}
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              {{- include "lib-microservice.selectorLabels" . | nindent 14 }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: {{ .Values.service.targetPort | default 8080 }}
              protocol: TCP
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          livenessProbe:
            httpGet:
              path: {{ .Values.probes.liveness.path | default "/healthz" }}
              port: {{ .Values.service.targetPort | default 8080 }}
            initialDelaySeconds: {{ .Values.probes.liveness.initialDelay | default 10 }}
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: {{ .Values.probes.readiness.path | default "/ready" }}
              port: {{ .Values.service.targetPort | default 8080 }}
            initialDelaySeconds: {{ .Values.probes.readiness.initialDelay | default 5 }}
            periodSeconds: 5
{{- end -}}

Consuming the Library Chart

Each service chart declares the library as a dependency:

# charts/user-service/Chart.yaml
apiVersion: v2
name: user-service
version: 2.1.0
type: application
dependencies:
  - name: lib-microservice
    version: "~1.4.0"
    repository: "file://../lib-microservice"

The service chart's templates become one-liners that invoke the library:

# charts/user-service/templates/deployment.yaml
{{ include "lib-microservice.deployment" . }}

Values Schema Validation

Helm 3 supports JSON Schema validation of values.yaml via a values.schema.json file. This catches typos and invalid configurations at helm install time rather than at runtime.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["image", "resources"],
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1,
      "maximum": 50
    },
    "image": {
      "type": "object",
      "required": ["repository", "tag"],
      "properties": {
        "repository": { "type": "string", "pattern": "^[a-z0-9./-]+$" },
        "tag": { "type": "string", "minLength": 1 }
      }
    },
    "resources": {
      "type": "object",
      "required": ["requests", "limits"],
      "properties": {
        "requests": {
          "type": "object",
          "properties": {
            "cpu": { "type": "string" },
            "memory": { "type": "string" }
          }
        }
      }
    }
  }
}

Helm Hooks for Database Migrations

Use pre-install and pre-upgrade hooks to run database migrations before the new pods start. The hook runs as a Job that must succeed before Helm proceeds:

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "lib-microservice.fullname" . }}-migrate
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: ["./migrate", "--direction", "up"]
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: {{ .Release.Name }}-db
                  key: url

Helm Test Suites

Helm tests are pods annotated with helm.sh/hook: test. They run when you execute helm test <release> and validate the release is working correctly.

# charts/user-service/tests/connection_test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: {{ include "lib-microservice.fullname" . }}-test
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  restartPolicy: Never
  containers:
    - name: curl-test
      image: curlimages/curl:8.5.0
      command:
        - sh
        - -c
        - |
          echo "Testing health endpoint..."
          curl -sf http://{{ include "lib-microservice.fullname" . }}:{{ .Values.service.port }}/healthz
          echo "Testing readiness endpoint..."
          curl -sf http://{{ include "lib-microservice.fullname" . }}:{{ .Values.service.port }}/ready

CI/CD Integration

Integrate chart linting and testing into your CI pipeline:

# .github/workflows/helm-ci.yaml
jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: azure/setup-helm@v3
      - name: Lint all charts
        run: |
          for chart in charts/*/; do
            helm lint "$chart" --strict --values "$chart/values.yaml"
          done
      - name: Template and validate
        run: |
          for chart in charts/*/; do
            helm template test "$chart" | kubectl apply --dry-run=server -f -
          done
      - name: Run chart-testing
        uses: helm/chart-testing-action@v2
        with:
          command: ct lint-and-install --all

Key Takeaways

  • Use library charts to eliminate YAML duplication across microservices.
  • Enforce configuration correctness with values.schema.json validation.
  • Run database migrations via pre-upgrade hooks to keep schema changes atomic with deployments.
  • Add Helm test pods to verify releases post-deploy.
  • Lint, template, and dry-run every chart change in CI before it reaches a cluster.