Kubernetes Helm Charts: Authoring, Values Schema, Hooks, Tests, and OCI Registry
Author production Kubernetes Helm charts: chart structure, values.yaml with JSON schema validation, pre-install and post-upgrade hooks, chart tests with helm test, and publishing to an OCI-compliant registry.
Helm is the package manager for Kubernetes. Deploying an application without Helm means maintaining dozens of YAML manifests manually — duplicating values across files, no versioning, no rollback, no parameterization. A well-authored Helm chart makes deploying to dev, staging, and production as simple as helm upgrade --install with different values files.
This post covers production Helm chart authoring: directory structure, values schema validation, deployment and service templates, pre-install database migration hooks, chart tests, and publishing to an OCI-compatible registry like AWS ECR.
1. Chart Structure
charts/myapp/
Chart.yaml ← Chart metadata and dependencies
values.yaml ← Default values
values.schema.json ← JSON Schema validation for values
templates/
_helpers.tpl ← Named templates (reusable partials)
deployment.yaml
service.yaml
ingress.yaml
configmap.yaml
secret.yaml
serviceaccount.yaml
hpa.yaml ← Horizontal Pod Autoscaler
pdb.yaml ← Pod Disruption Budget
hooks/
migration-job.yaml ← Pre-upgrade database migration
tests/
connection-test.yaml ← helm test pod
NOTES.txt ← Post-install instructions printed to user
2. Chart Metadata
# charts/myapp/Chart.yaml
apiVersion: v2
name: myapp
description: Viprasol API service Helm chart
type: application
version: 1.4.2 # Chart version (SemVer) — bump on chart changes
appVersion: "2.1.0" # Application version — informational
maintainers:
- name: Viprasol Tech Team
email: ops@viprasol.com
keywords:
- api
- saas
- nodejs
dependencies:
- name: postgresql
version: "15.5.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled # Only install if .Values.postgresql.enabled = true
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
3. Values and JSON Schema Validation
# charts/myapp/values.yaml
replicaCount: 2
image:
repository: viprasol/api
tag: "" # Defaults to Chart.appVersion if empty
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 3000
ingress:
enabled: false
className: nginx
host: ""
tls: []
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
env:
NODE_ENV: production
LOG_LEVEL: info
secrets:
dbSecretArn: ""
stripeSecretArn: ""
migrations:
enabled: true # Run DB migration job before upgrade
image: "" # Defaults to main image if empty
postgresql:
enabled: false # Use external DB in production
// charts/myapp/values.schema.json
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["image", "service"],
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1,
"maximum": 50
},
"image": {
"type": "object",
"required": ["repository"],
"properties": {
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string" },
"pullPolicy": {
"type": "string",
"enum": ["Always", "IfNotPresent", "Never"]
}
}
},
"service": {
"type": "object",
"required": ["port"],
"properties": {
"type": { "type": "string", "enum": ["ClusterIP", "NodePort", "LoadBalancer"] },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 }
}
},
"resources": {
"type": "object",
"properties": {
"requests": {
"type": "object",
"properties": {
"memory": { "type": "string", "pattern": "^[0-9]+(Mi|Gi)$" },
"cpu": { "type": "string", "pattern": "^[0-9]+(m|\\.[0-9]+)?$" }
}
}
}
}
}
}
4. Deployment Template
# charts/myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
# Force pod restart when configmap changes
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # Zero-downtime: always have at minimum current replicas
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
serviceAccountName: {{ include "myapp.serviceAccountName" . }}
terminationGracePeriodSeconds: 30
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "myapp.fullname" . }}-config
env:
# Expose pod metadata for logging
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
resources:
{{- toYaml .Values.resources | nindent 12 }}
# Spread pods across nodes
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 14 }}

⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
Recommended Reading
5. Pre-Upgrade Migration Hook
# charts/myapp/templates/hooks/migration-job.yaml
{{- if .Values.migrations.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "myapp.fullname" . }}-migration-{{ .Release.Revision }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
# Run BEFORE upgrade, delete after 3 minutes regardless of outcome
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
spec:
backoffLimit: 0 # Don't retry — failing migration should block deploy
activeDeadlineSeconds: 300
template:
spec:
serviceAccountName: {{ include "myapp.serviceAccountName" . }}
restartPolicy: Never
containers:
- name: migration
image: "{{ .Values.migrations.image | default (printf "%s:%s" .Values.image.repository (.Values.image.tag | default .Chart.AppVersion)) }}"
command: ["npx", "prisma", "migrate", "deploy"]
envFrom:
- configMapRef:
name: {{ include "myapp.fullname" . }}-config
{{- end }}
6. Chart Tests
# charts/myapp/tests/connection-test.yaml
apiVersion: v1
kind: Pod
metadata:
name: {{ include "myapp.fullname" . }}-test
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test # Run with: helm test <release>
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
restartPolicy: Never
containers:
- name: test
image: curlimages/curl:8.5.0
command:
- sh
- -c
- |
# Test health endpoint
curl -sf http://{{ include "myapp.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.port }}/health/ready \
|| exit 1
echo "Health check passed"
# Test API endpoint (basic smoke test)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
http://{{ include "myapp.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.port }}/api/version)
[ "$STATUS" = "200" ] || exit 1
echo "API version endpoint returned 200"
7. Publishing to OCI Registry (AWS ECR)
#!/usr/bin/env bash
# scripts/publish-chart.sh
set -euo pipefail
CHART_DIR="charts/myapp"
ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
CHART_NAME="myapp"
# Authenticate Helm to ECR
aws ecr get-login-password --region "${AWS_REGION}" \
| helm registry login --username AWS --password-stdin "${ECR_REGISTRY}"
# Lint before publish
helm lint "${CHART_DIR}"
# Package chart
helm package "${CHART_DIR}" --destination /tmp/charts/
# Get packaged filename
CHART_FILE=$(ls /tmp/charts/${CHART_NAME}-*.tgz | head -1)
CHART_VERSION=$(helm show chart "${CHART_DIR}" | grep '^version:' | awk '{print $2}')
# Push to ECR OCI registry
helm push "${CHART_FILE}" "oci://${ECR_REGISTRY}/helm-charts"
echo "✅ Published ${CHART_NAME}:${CHART_VERSION} to ECR"
# Install from OCI registry
helm install myapp \
oci://${ECR_REGISTRY}/helm-charts/myapp \
--version 1.4.2 \
--values values-production.yaml \
--namespace production \
--create-namespace
# Upgrade
helm upgrade myapp \
oci://${ECR_REGISTRY}/helm-charts/myapp \
--version 1.4.3 \
--values values-production.yaml \
--namespace production \
--atomic \ # Auto-rollback on failure
--timeout 5m
# Run tests after deploy
helm test myapp --namespace production
Cost Reference
| Helm approach | Maintenance | Upgrade safety | Notes |
|---|---|---|---|
| Raw YAML files | High | Low | No versioning or rollback |
| Helm with local charts | Medium | Medium | Rollback via helm rollback |
| Helm + OCI registry | Low | High | Versioned, auditable releases |
| ArgoCD + Helm | Low | Very high | GitOps with automatic sync |
Continue Learning
- Kubernetes Ingress NGINX: TLS, Rate Limiting, and Canary Deployments
- Kubernetes Cost Optimization: Right-Sizing, Spot Nodes, and Autoscaling
- Docker Multi-Stage Builds: Layer Caching and Minimal Images
- Terraform State Management: Remote State and Workspaces
- AWS ECS Fargate in Production: Task Definitions and Blue/Green Deploys
Our Approach at Viprasol
Deploying Kubernetes workloads with raw YAML manifests and manual kubectl apply? We author production Helm charts with JSON schema validation, zero-downtime rolling update strategy, pre-upgrade migration hooks, chart tests, and OCI registry publishing — giving you versioned, testable, rollback-capable Kubernetes deployments.
External Resources
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.