Skip to main content

Helm Chart Development Best Practices 2025

Helm is the standard package manager for Kubernetes, making application deployments reproducible and manageable. However, chart quality directly impacts operational costs. Over-abstracted templates, insufficient testing, and overlooked security gaps can cause serious issues in production. Kubo adopts Helm charts as the standard pattern for declarative deployment, and the best practices in this article apply directly to Kubo environments.

Chart Structure and Directory Design

Helm chart quality is determined at the structural design stage. Here are best practices derived from the Helm official documentation and real-world experience.

Standard Chart Structure

text
my-app/
├── Chart.yaml              # Chart metadata
├── Chart.lock              # Dependency lock file
├── values.yaml             # Default configuration values
├── values-dev.yaml         # Development environment overrides
├── values-staging.yaml     # Staging environment overrides
├── values-production.yaml  # Production environment overrides
├── templates/
│   ├── _helpers.tpl        # Common template helpers
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── hpa.yaml
│   ├── serviceaccount.yaml
│   ├── configmap.yaml
│   └── tests/
│       └── test-connection.yaml
├── charts/                 # Subcharts (dependencies)
└── .helmignore

Separating Environment-Specific Values Files

As Carlos Neto's blog points out, trying to manage all environments in a single values.yaml leads to bloated, unmanageable files. Separate values files per environment and specify them explicitly at deploy time.

bash
# Development environment
helm upgrade --install my-app ./my-app -f values.yaml -f values-dev.yaml

# Production environment
helm upgrade --install my-app ./my-app -f values.yaml -f values-production.yaml

Define defaults in values.yaml and include only the values that need overriding in environment-specific files. This keeps each file small and makes differences clear.

Template Design Principles: DRY and YAGNI

Two critical principles determine Helm template quality.

DRY (Don't Repeat Yourself)

Manifest duplication is a breeding ground for errors. Extract common patterns into _helpers.tpl and reuse them as template functions.

yaml
{{/* _helpers.tpl */}}
{{- define "my-app.labels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ include "my-app.chart" . }}
{{- end }}

{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

YAGNI (You Aren't Gonna Need It)

Over-abstraction of templates is the biggest anti-pattern. The practical Helm development guide warns that "every extra function, conditional statement, or parameter adds cognitive overhead."

Bad example: Unnecessary abstraction

yaml
{{- if .Values.deployment.enabled }}
{{- if .Values.deployment.strategy.enabled }}
strategy:
  type: {{ .Values.deployment.strategy.type | default "RollingUpdate" }}
{{- end }}
{{- end }}

Good example: Simple and sufficient

yaml
strategy:
  type: {{ .Values.strategy | default "RollingUpdate" }}

Write simple templates that meet current requirements. Don't add complexity for hypothetical future scenarios.

Leveraging Library Charts

For organizations with multiple charts, consolidate common templates into a library chart like the Bitnami Common Chart. Library charts don't deploy resources directly---they only provide template helpers.

yaml
# Chart.yaml
dependencies:
  - name: common
    version: 2.x.x
    repository: https://charts.bitnami.com/bitnami

Captain.AI can assist with AI-powered template quality analysis and optimization recommendations.

Testing Strategy: Multi-Layer Quality Assurance

Helm chart testing directly impacts ci-cd pipeline reliability. Combine the Chart Testing (ct) tool and helm-unittest for a multi-layered testing approach.

Layer 1: Lint and Static Validation

bash
# Helm's built-in lint
helm lint ./my-app --values values-production.yaml

# Strict linting with Chart Testing
ct lint --chart-dirs charts/ --all

Layer 2: Unit Tests (helm-unittest)

yaml
# tests/deployment_test.yaml
suite: Deployment Tests
templates:
  - deployment.yaml
tests:
  - it: should set correct replicas
    set:
      replicaCount: 5
    asserts:
      - equal:
          path: spec.replicas
          value: 5

  - it: should have resource limits
    asserts:
      - isNotEmpty:
          path: spec.template.spec.containers[0].resources.limits

  - it: should use correct image
    set:
      image.repository: my-registry/my-app
      image.tag: "v1.0.0"
    asserts:
      - equal:
          path: spec.template.spec.containers[0].image
          value: "my-registry/my-app:v1.0.0"

Layer 3: Template Rendering Validation

bash
# Render templates to YAML and validate
helm template my-app ./my-app -f values-production.yaml | kubeval --strict

Layer 4: Integration Tests (helm test)

Use Helm's chart test feature to run post-deployment tests against the live environment.

yaml
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "my-app.fullname" . }}-test"
  annotations:
    "helm.sh/hook": test
spec:
  containers:
    - name: curl-test
      image: curlimages/curl:latest
      command: ['curl', '--fail', 'http://{{ include "my-app.fullname" . }}:{{ .Values.service.port }}']
  restartPolicy: Never
bash
helm test my-release

OCI Registries and Chart Distribution

Since Helm 3.8, OCI (Open Container Initiative) registries have been the recommended mechanism for chart distribution. Managing charts in the same registry as container images enables unified operations.

Pushing to an OCI Registry

bash
# Package the chart
helm package ./my-app

# Log in to the registry
helm registry login ghcr.io -u $GITHUB_USER -p $GITHUB_TOKEN

# Push
helm push my-app-1.0.0.tgz oci://ghcr.io/your-org/charts

Installing from an OCI Registry

bash
helm install my-app oci://ghcr.io/your-org/charts/my-app --version 1.0.0

Automated Publishing in ci-cd Pipelines

yaml
# GitHub Actions example
- name: Publish Helm Chart
  run: |
    helm package ./charts/my-app --version ${{ github.ref_name }}
    helm push my-app-${{ github.ref_name }}.tgz \
      oci://ghcr.io/${{ github.repository_owner }}/charts

Kubo integrates an OCI-compliant registry, making chart publishing and management seamless.

Security and Resource Management

Proper Resource Limits

yaml
# values.yaml
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

Adjust resource values per environment. Keep development minimal; set appropriate values for production traffic.

Security Context

yaml
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop:
      - ALL

Externalizing Secrets

Reference Kubernetes Secrets within templates while managing the actual secret data through External Secrets Operator or Sealed Secrets.

Vulnerability Scanning

bash
# Scan charts and referenced images with Trivy
trivy config ./my-app/
trivy image my-registry/my-app:v1.0.0

# Configuration scanning with Kubescape
kubescape scan framework nsa ./my-app/

Integrate Trivy and Kubescape into your CI pipeline to detect security issues before deployment.

Conclusion: Maximize Helm Charts with Kubo

Helm chart quality determines the success of Kubernetes operations. By balancing DRY and YAGNI, implementing multi-layer testing, distributing via OCI registries, and following security best practices, you can develop charts that are maintainable and secure.

Kubo supports declarative deployment via Helm charts as a standard pattern, with built-in integration for ArgoCD and FluxCD. Captain.AI provides AI-powered chart quality analysis and template optimization, maximizing developer productivity. Ready to improve your Helm chart development? Contact us today.

← Back to all posts