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
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 |
See Also
- 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
Working With 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.
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 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
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.