Mastering Helm Charts for Microservices
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 chartsHere 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: urlHelm 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 }}/readyCI/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 --allKey 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.