Skip to main content
Kubernetes·5 min read

Helm Chart Spaghetti — Standardizing 50+ Microservice Configurations

Eliminate duplicated Helm charts across microservices using library charts, common templates, umbrella charts, values inheritance, and automated chart testing with helm-unittest.

DT

DevOps Engineer & Technical Writer

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.

Helm Spaghetti vs Library Pattern

Before: 50 Duplicated Charts

service-a/templates/deployment.yaml (150 lines)

service-b/templates/deployment.yaml (148 lines)

service-c/templates/deployment.yaml (155 lines)

... 47 more copies with variations

Bug fix = 50 PRs. Patch = 50 updates.

After: Library Chart

common-lib/templates/_deployment.tpl

common-lib/templates/_service.tpl

common-lib/templates/_hpa.tpl

service-a/values.yaml (20 lines)

Bug fix = 1 PR. All services updated.

Impact

YAML maintained: 7,500 lines to 500

Add security headers: 2 weeks to 1 hour

Config drift incidents: 12/month to 0

New service onboarding: 2 days to 30 min

---

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.

---