Back to Blog

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.

Viprasol Tech Team
December 22, 2026
13 min read

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 approachMaintenanceUpgrade safetyNotes
Raw YAML filesHighLowNo versioning or rollback
Helm with local chartsMediumMediumRollback via helm rollback
Helm + OCI registryLowHighVersioned, auditable releases
ArgoCD + HelmLowVery highGitOps with automatic sync

See Also


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.

Talk to our team → | Explore our cloud solutions →

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need DevOps & Cloud Expertise?

Scale your infrastructure with confidence. AWS, GCP, Azure certified team.

Free consultation • No commitment • Response within 24 hours

Viprasol · Big Data & Analytics

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.