Helm Charts, Values Layering, and Dependency Management
Helm Charts, Values Layering, and Dependency Management
The Failure
The team created a shared Helm chart for all microservices. The values.yaml grew to 800 lines. Every team added their service-specific values to the shared defaults. When the platform team changed a default value, 4 out of 12 services broke because they depended on the old default. The shared chart became a coordination bottleneck.
The fix: keep the shared chart minimal (deployment, service, HPA, PDB). Service-specific configuration lives in per-service values files, not in the chart defaults.
The Mechanism
Values Layering
Helm merges values files in order. Later files override earlier ones:
helm template my-service ./charts/microservice \
-f charts/microservice/values.yaml \ # Chart defaults
-f values/common.yaml \ # Organization defaults
-f values/checkout-service.yaml \ # Service-specific
-f values/checkout-service-production.yaml # Environment-specific
Chart Dependencies
A service chart can depend on other charts (Redis, PostgreSQL for local development):
# Chart.yaml
dependencies:
- name: redis
version: "18.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
The Implementation
Shared Microservice Chart
# charts/microservice/Chart.yaml
apiVersion: v2
name: microservice
description: Shared chart for e-commerce microservices
version: 1.0.0
# charts/microservice/values.yaml
# HARDENED: Minimal defaults — services override what they need
replicaCount: 1
image:
repository: "" # Required: must be set per service
tag: "" # Required: must be set per deployment
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
healthCheck:
readiness:
path: /health/ready
initialDelaySeconds: 5
periodSeconds: 10
liveness:
path: /health/live
initialDelaySeconds: 15
periodSeconds: 20
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilization: 80
podDisruptionBudget:
enabled: false
minAvailable: 1
# charts/microservice/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
labels:
{{- include "microservice.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "microservice.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "microservice.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Release.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
readinessProbe:
httpGet:
path: {{ .Values.healthCheck.readiness.path }}
port: {{ .Values.service.port }}
initialDelaySeconds: {{ .Values.healthCheck.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.healthCheck.readiness.periodSeconds }}
livenessProbe:
httpGet:
path: {{ .Values.healthCheck.liveness.path }}
port: {{ .Values.service.port }}
initialDelaySeconds: {{ .Values.healthCheck.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.healthCheck.liveness.periodSeconds }}
{{- if .Values.env }}
env:
{{- toYaml .Values.env | nindent 12 }}
{{- end }}
Per-Service Values
# values/checkout-service.yaml
image:
repository: ghcr.io/acme/checkout-service
service:
port: 8080
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilization: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
env:
- name: CATALOG_SERVICE_URL
value: http://catalog-service.production.svc.cluster.local
- name: INVENTORY_SERVICE_URL
value: http://inventory-service.production.svc.cluster.local
Chart Testing in CI
# HARDENED: Helm chart validation
- name: Lint chart
run: helm lint charts/microservice
- name: Template with each service's values
run: |
for values_file in values/*.yaml; do
SERVICE=$(basename "$values_file" .yaml)
echo "Templating $SERVICE..."
helm template "$SERVICE" charts/microservice \
-f "$values_file" \
--set image.tag=test \
| kubectl apply --dry-run=server -f -
done
- name: Run chart tests
run: |
helm install test-release charts/microservice \
-f values/checkout-service.yaml \
--set image.tag=test \
--dry-run
The Gate
Chart linting and template rendering run in CI on every PR. If a chart change breaks any service’s values file, the PR is blocked. The kubectl apply --dry-run=server validates against the cluster’s API schema.
Chart version bumps require explicit version changes in Chart.yaml. SemVer is enforced: breaking changes require a major version bump.
The Recovery
Chart upgrade breaks a service: Pin the chart version per service. Each service’s ArgoCD Application specifies the chart version. Upgrade one service at a time.
Values file merge produces unexpected results: Use helm template with --debug to see the full rendered output. Helm’s merge behavior can be surprising with nested objects — use explicit keys, not deeply nested structures.