TL;DR — Quick Fix
If you have 50+ services each with duplicated Helm charts:
# Create a library chart for shared templates
helm create charts/common-lib
# Edit Chart.yaml: set type to "library"
# Add as dependency in each service chart
cat >> charts/my-service/Chart.yaml << 'EOF'
dependencies:
- name: common-lib
version: "1.0.0"
repository: "file://../common-lib"
EOF
# Use shared templates
cat > charts/my-service/templates/deployment.yaml << 'EOF'
{{- include "common-lib.deployment" . }}
EOF
helm dependency update charts/my-service
helm template charts/my-service
---
The Problem with Duplicated Charts
When each microservice has its own chart, you get drift, inconsistency, and maintenance nightmares.
---
Library Chart Pattern
# charts/common-lib/Chart.yaml
apiVersion: v2
name: common-lib
description: Shared templates for all microservices
type: library
version: 1.0.0
# charts/common-lib/templates/_deployment.tpl
{{- define "common-lib.deployment" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "common-lib.fullname" . }}
labels:
{{- include "common-lib.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount | default 2 }}
selector:
matchLabels:
{{- include "common-lib.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "common-lib.selectorLabels" . | nindent 8 }}
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: {{ .Values.metricsPort | default "9090" | quote }}
spec:
serviceAccountName: {{ include "common-lib.serviceAccountName" . }}
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.containerPort | default 8080 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
livenessProbe:
httpGet:
path: {{ .Values.healthCheck.liveness | default "/healthz" }}
port: {{ .Values.containerPort | default 8080 }}
readinessProbe:
httpGet:
path: {{ .Values.healthCheck.readiness | default "/ready" }}
port: {{ .Values.containerPort | default 8080 }}
{{- end }}
---
Service Chart (Consumer)
# charts/user-service/Chart.yaml
apiVersion: v2
name: user-service
version: 0.1.0
dependencies:
- name: common-lib
version: "1.x.x"
repository: "oci://registry.example.com/helm-charts"
# charts/user-service/values.yaml — Only differences
replicaCount: 3
image:
repository: registry.example.com/user-service
tag: "v2.4.1"
containerPort: 8080
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
healthCheck:
liveness: /health
readiness: /ready
# charts/user-service/templates/deployment.yaml — Just one line
{{- include "common-lib.deployment" . }}
---
Umbrella Charts for Environments
# charts/platform/Chart.yaml
apiVersion: v2
name: platform
version: 1.0.0
dependencies:
- name: user-service
version: "0.1.x"
repository: "oci://registry.example.com/helm-charts"
- name: order-service
version: "0.3.x"
repository: "oci://registry.example.com/helm-charts"
- name: payment-service
version: "0.2.x"
repository: "oci://registry.example.com/helm-charts"
condition: payment-service.enabled
# charts/platform/values-production.yaml
user-service:
replicaCount: 5
resources:
requests:
cpu: 500m
memory: 512Mi
order-service:
replicaCount: 3
resources:
requests:
cpu: 1000m
memory: 1Gi
payment-service:
enabled: true
replicaCount: 3
---
Chart Testing with helm-unittest
# charts/common-lib/tests/deployment_test.yaml
suite: deployment template tests
templates:
- templates/deployment.yaml
tests:
- it: should set correct replica count
set:
replicaCount: 5
asserts:
- equal:
path: spec.replicas
value: 5
- it: should enforce security context
asserts:
- equal:
path: spec.template.spec.securityContext.runAsNonRoot
value: true
- it: should include health probes
asserts:
- isNotNull:
path: spec.template.spec.containers[0].livenessProbe
- it: should set resource limits
set:
resources:
limits:
memory: 512Mi
asserts:
- equal:
path: spec.template.spec.containers[0].resources.limits.memory
value: 512Mi
# Run chart tests in CI
helm plugin install https://github.com/helm-unittest/helm-unittest
helm unittest charts/common-lib
helm unittest charts/user-service
# Lint all charts
for chart in charts/*/; do
helm lint "$chart" --strict
done
---
CI/CD Integration
# .github/workflows/helm-ci.yml
name: Helm Chart CI
on:
push:
paths: ['charts/**']
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-helm@v3
- name: Install helm-unittest
run: helm plugin install https://github.com/helm-unittest/helm-unittest
- name: Lint charts
run: |
for chart in charts/*/Chart.yaml; do
helm lint "$(dirname $chart)" --strict
done
- name: Run unit tests
run: |
for chart in charts/*/tests; do
helm unittest "$(dirname $chart)"
done
---
FAQ
Q: How do I version the library chart without breaking services?
A: Use semantic versioning. Breaking changes = major bump. New features = minor. Fixes = patch. Pin services to "1.x.x" for auto non-breaking updates.
Q: What if one service needs a custom template?
A: Allow escape hatches. Services can override by providing their own template file. The library templates can check and yield.
Q: How do I migrate 50 existing charts gradually?
A: Start with the simplest resource (ServiceAccount). Migrate one type at a time. Diff helm template before/after. Validate 5 services for a week, then batch the rest.
Q: Should I use one library chart or multiple?
A: One for standard web services. Create additional libraries for special workloads (CronJobs, StatefulSets). Keep the common one focused.
Q: How do teams customize without forking?
A: Use values.yaml for all configuration. Provide extension points via named templates. Document supported customization paths.
---