Helm Charts: Templating, Values, and Package Management

Helm Charts guide covering templates, values management, chart repositories, and production deployment workflows.

published: reading time: 40 min read author: GeekWorkBench updated: June 17, 2026
Quick Summary

Helm packages Kubernetes manifests into charts that you can version, share, and deploy with a single command. Charts use Go templates with a cascading values system: defaults in values.yaml get overridden by environment-specific files, then by CLI flags. This guide covers chart structure, template functions, named templates for reusable components, hooks for migrations, and the Helm 3 security improvements over Tiller-based Helm 2. For teams managing multiple environments, Helm reduces deployment toil significantly.

Helm Charts: Templating, Values, and Kubernetes Package Management

Helm is the package manager for Kubernetes. Managing multiple YAML files across different environments gets tedious fast. Helm solves this by letting you package everything into a chart that you can version, share, and deploy with a single command.

This guide covers creating basic charts through advanced templating patterns. If you are new to Kubernetes containers, start with our Docker Fundamentals and Advanced Kubernetes guides first.

Introduction

Helm has a client-server architecture. The Helm client interacts with the server-side component called Tiller in Helm 2, or uses the cluster’s service account in Helm 3.

graph LR
    A[Helm Client] -->|Chart| B[Chart Repository]
    A -->|Kubeconfig| C[Kubernetes Cluster]
    C -->|Release| D[Release History]
    C -->|Resources| E[Deployed Resources]

Helm 3 removed Tiller and introduced cluster-scoped service accounts with release-level scope. This was a significant improvement for production use.

Chart Structure

A chart is a collection of files organized in a specific directory structure:

my-chart/
  Chart.yaml          # Chart metadata
  values.yaml         # Default configuration values
  values.schema.json   # Optional JSON schema validation
  charts/              # Dependency charts (Helm 3 style)
  templates/           # Kubernetes manifest templates
  templates/NOTES.txt  # Post-install notes
  .helmignore          # Files to ignore during packaging

Chart.yaml

The Chart.yaml contains the chart’s metadata:

apiVersion: v2
name: my-application
description: A Helm chart for my production application
type: application
version: 1.2.3
appVersion: "2.0.0"
kubeVersion: ">=1.24.0"
keywords:
  - web application
  - api
home: https://github.com/example/my-app
sources:
  - https://github.com/example/my-app
maintainers:
  - name: DevOps Team
    email: devops@example.com
dependencies:
  - name: postgresql
    version: "12.x.x"
    repository: "https://charts.bitnami.com"
    condition: postgresql.enabled
  - name: redis
    version: "17.x.x"
    repository: "https://charts.bitnami.com"
    condition: redis.enabled

The apiVersion: v2 format is for Helm 3. Helm 2 used apiVersion: v1.

Values Files

Values files provide configuration that gets merged into templates. Helm uses a cascading values system: default values in values.yaml, overridden by environment-specific files, overridden by command-line flags.

values.yaml

# Default configuration
replicaCount: 3

image:
  repository: myregistry/myapp
  pullPolicy: IfNotPresent
  tag: "1.0.0"

service:
  type: ClusterIP
  port: 8080

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: myapp-tls
      hosts:
        - api.example.com

resources:
  limits:
    cpu: 1000m
    memory: 1Gi
  requests:
    cpu: 100m
    memory: 256Mi

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

postgresql:
  enabled: true
  auth:
    database: myapp
    username: myapp
  primary:
    persistence:
      size: 10Gi

redis:
  enabled: true
  auth:
    password: ""

Environment-Specific Values

Create environment-specific value files:

# values-staging.yaml
replicaCount: 2
image:
  tag: "1.0.0-staging"
resources:
  limits:
    cpu: 500m
    memory: 512Mi
autoscaling:
  enabled: false
ingress:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-staging
  hosts:
    - host: staging-api.example.com
# values-prod.yaml
replicaCount: 5
image:
  tag: "1.0.0-production"
resources:
  limits:
    cpu: 2000m
    memory: 2Gi
autoscaling:
  minReplicas: 5
  maxReplicas: 20

Deploy with specific values:

helm upgrade --install myapp ./charts/myapp \
  --values values-prod.yaml \
  --namespace production \
  --create-namespace

Template Functions and Pipelines

Helm templates use Go template syntax with Sprig functions for manipulation.

Common Functions

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      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
          livenessProbe:
            httpGet:
              path: /health
              port: http
          readinessProbe:
            httpGet:
              path: /ready
              port: http
          {{- with .Values.resources }}
          resources:
            {{- toYaml . | nindent 12 }}
          {{- end }}

String Functions

# Uppercase
appName: { { .Values.name | upper } }

# Lowercase
envVar: { { .Values.env | lower | squote } }

# Truncate
shortName: { { .Values.name | trunc 63 } }

# Replace
fixedName: { { .Values.name | replace "_" "-" } }

# Quote
quoted: { { .Values.name | quote } }

# Default value
tag: { { .Values.image.tag | default .Chart.AppVersion } }

Conditional Logic

# Ternary operator
replicas: {{ .Values.replicaCount | default 1 }}

# If-else blocks
{{- if .Values.ingress.enabled }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "myapp.fullname" . }}
spec:
  {{- with .Values.ingress }}
  ingressClassName: {{ .className }}
  {{- end }}
{{- end }}

Range Loops

# Loop over list
env:
  {{- range .Values.env }}
  - name: {{ .name }}
    value: {{ .value | quote }}
  {{- end }}

# Loop over key-value map
labels:
  {{- range $key, $value := .Values.labels }}
  {{ $key }}: {{ $value | quote }}
  {{- end }}

# Loop with index
{{- range $i, $v := .Values.replicas }}
- name: replica-{{ $i }}
{{- end }}

Named Templates

Named templates (partials) live in templates/_helpers.tpl and you can include them throughout your chart.

Defining Helpers

# templates/_helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Full name including release and chart.
*/}}
{{- define "myapp.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels.
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{ include "myapp.name" . }}: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ include "myapp.name" . }}
{{- end }}

{{/*
Selector labels.
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Using Helpers in Templates

apiVersion: apps/v1
kind: Deployment
metadata:
  name: { { include "myapp.fullname" . } }
  labels: { { include "myapp.labels" . | nindent 4 } }
spec:
  replicas: { { .Values.replicaCount } }
  selector:
    matchLabels: { { include "myapp.selectorLabels" . | nindent 6 } }

Chart Dependencies

Charts can depend on other charts. In Helm 3, you define dependencies in Chart.yaml and download them to the charts/ directory.

Dependency Management

# Chart.yaml
dependencies:
  - name: postgresql
    version: "12.x.x"
    repository: "https://charts.bitnami.com"
    condition: postgresql.enabled
  - name: redis
    version: "17.x.x"
    repository: "https://charts.bitnami.com"
    condition: redis.enabled

Update dependencies:

helm dependency update ./charts/myapp

This creates a Chart.lock file that locks dependency versions.

Sub-chart Values

Parent charts can override sub-chart values:

# values.yaml
postgresql:
  primary:
    persistence:
      size: 50Gi
  auth:
    database: myapp_prod
    username: myapp

redis:
  auth:
    password: secretpassword
  master:
    persistence:
      enabled: false

Hooks

Hooks run at specific points in the release lifecycle. Use them for database migrations, backup jobs, or waiting for dependencies.

Common Hook Annotations

# hooks/post-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "myapp.fullname" . }}-migrations
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": post-install,post-upgrade
    "helm.sh/hook-weight": "-1"
    "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrations
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command:
            - /app/migrate.sh

Hook Weights

Each hook gets a numeric weight via the helm.sh/hook-weight annotation. Helm sorts hooks by weight ascending and runs them in that order. The default weight is 0 when you omit the annotation entirely.

Negative weights run before zero, and positive weights run after. This lets you sequence dependent hooks:

annotations:
  "helm.sh/hook": pre-upgrade
  "helm.sh/hook-weight": "-5"
annotations:
  "helm.sh/hook": pre-upgrade
  "helm.sh/hook-weight": "5"

Database migrations are the classic case for negative weights. Set the migration hook at weight -5 and the app startup hook at weight 0. The migration finishes before anything tries to connect.

You can also orchestrate resource creation order this way. A ConfigMap hook at weight -3, a migration job at weight -1, and a deployment at weight 0 guarantees the ConfigMap exists before the migration runs, and the migration completes before the app deploys.

Testing Charts

Helm includes a test framework for validating chart installations.

Test Pod

# templates/tests/pod-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: {{ include "myapp.fullname" . }}-test-connection
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-delete-policy": test-success
spec:
  restartPolicy: Never
  containers:
    - name: wget
      image: busybox:1.36
      command:
        - wget
      args:
        - '-O-'
        - 'http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health'

Run tests:

helm test myapp --namespace production

Chart Repositories

Chart repositories serve packaged charts via an HTTP server. You can create your own repository for internal charts.

Creating a Repository

# Package your chart
helm package ./charts/myapp

# Create index.yaml
helm repo index ./charts --url https://charts.example.com/

# Serve repository via HTTP
python3 -m http.server 8080 --directory ./charts

Adding and Using Repositories

# Add repository
helm repo add bitnami https://charts.bitnami.com

# Update repositories
helm repo update

# Search for charts
helm search repo bitnami/postgresql

# Install from repository
helm install mydb bitnami/postgresql --values values.yaml

GitHub Pages Repository

Host your chart repository on GitHub Pages:

# In your charts repository
git checkout gh-pages || git checkout -b gh-pages

# Copy your chart package
cp ../my-chart-1.2.3.tgz ./

# Update index
helm repo index . --url https://example.github.io/charts

git add .
git commit -m "Add my-chart v1.2.3"
git push origin gh-pages

Library Charts

Library charts provide reusable templates that other charts can include. They are useful for standardizing common patterns across your organization.

Library Chart Structure

library-chart/
  Chart.yaml
  templates/
    _deployment.yaml
    _service.yaml
    _configmap.yaml
# Chart.yaml
apiVersion: v2
name: myapp-library
type: library
version: 1.0.0

Using Library Chart

# In your application chart
dependencies:
  - name: myapp-library
    version: "1.x.x"
    repository: "https://charts.example.com"
    import-values:
      - data

JSON Schema Validation

Values schema files validate values provided to your chart:

// values.schema.json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1,
      "maximum": 10
    },
    "image": {
      "type": "object",
      "properties": {
        "repository": { "type": "string" },
        "tag": { "type": "string" },
        "pullPolicy": {
          "type": "string",
          "enum": ["IfNotPresent", "Always", "Never"]
        }
      },
      "required": ["repository"]
    },
    "service": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": ["ClusterIP", "LoadBalancer", "NodePort"]
        },
        "port": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535
        }
      },
      "required": ["type", "port"]
    }
  },
  "required": ["image", "service"]
}

If a user provides invalid configuration, Helm rejects it during template rendering:

helm template myapp ./charts/myapp --values invalid-values.yaml
# Error: values don't meet the schema

Best Practices

Production Deployment Checklist

Before pushing a chart to production, run through this checklist. Skipping any of these steps has bitten teams at least once, so treat them as mandatory rather than optional.

Every item here addresses something that has actually gone wrong in a real deployment. The worst ones always look optional until they bite you.

CategoryActionWhy It Matters
DependenciesPin exact versions in Chart.yaml and commit Chart.lockLoose ranges like 12.x.x can pull breaking changes silently
ValidationAdd values.schema.json and run helm lint --strict in CICatches type errors and missing fields before they hit the cluster
DocumentationComment every value in values.yaml with purpose and constraintsOther teams will thank you when debugging at 2am
Stateful opsUse hooks for migrations, warmups, and seeding; make them idempotentNon-idempotent hooks accumulate duplicate jobs on every upgrade
TestingRun helm template --debug and helm test in staging before every prod deployCatches rendering errors before they block a deployment
SecretsExternalize all credentials via External Secrets Operator or VaultCommitted secrets in git become incidents

Externalizing secrets is the step that gets skipped most often. It feels like overhead when you are starting out, but retrofitting it into a chart where users have already committed secrets to their values files is painful. Do it from the beginning.

For GitOps teams, add a pre-commit hook that runs helm lint --strict so bad charts never enter git. The pipeline catches the hard errors, but a pre-commit hook stops the trivial ones from ever being committed.

Security Considerations

Security mistakes in Helm charts can expose your whole cluster. These are the ones that show up most often in postmortems:

Most damaging Helm security incidents fall into three buckets: secrets that leaked through version control, overprivileged service accounts, and un-pinned dependencies with known CVEs.

Secrets in values files

Never put credentials, tokens, or certificates into any file that gets committed to git. Once a secret is in git, assume it is compromised. Pull secrets from External Secrets Operator, HashiCorp Vault, or AWS Secrets Manager at runtime. For local development, pass them via --set flags through a gitignored values-local.yaml or environment variables.

Container privileges

Run as non-root whenever possible. Add this to your deployment values:

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  allowPrivilegeEscalation: false
  capabilities:
    drop: ["ALL"]

podSecurityContext:
  runAsNonRoot: true
  fsGroup: 10001

Some containers need root early in their startup (directory permissions, certificate generation) but can drop privileges after. If the image does not support this, look for alternatives or use a startup script that calls su-exec or setpriv before the main process starts.

Scanning dependencies

Every chart dependency is an attack surface. A chart that pulls Bitnami PostgreSQL inherits every CVE in that PostgreSQL version. Run Trivy or Snyk in CI on both the chart and the container images it references:

# Scan chart dependencies
trivy chart ./charts/myapp

# Scan container images
trivy image myregistry/myapp:1.2.3
trivy image myregistry/postgresql:15.2.0

Make the CI job fail on HIGH or CRITICAL vulnerabilities so bad images never reach a cluster.

RBAC for Helm 3

Helm 3 uses the permissions of whichever identity runs helm install or helm upgrade. That identity creates all the Kubernetes resources on your behalf. If you give that identity cluster-admin, a chart bug or a malicious hook can do anything in the cluster. Create a dedicated service account per namespace with only the permissions the chart actually needs:

kubectl create serviceaccount helm-deployer -n production
kubectl create role helm-deployer \
  --verb=get,list,watch,create,update,patch \
  --resource=deployments,services,configmaps,secrets,pvc -n production
kubectl create rolebinding helm-deployer-binding \
  --role=helm-deployer --serviceaccount=production:helm-deployer -n production

Then run Helm with that service account token. If you are running an external secrets operator, grant it read access to the secret store and nothing else.

Release Management

Treat chart releases the same way you treat application releases: versioned, tagged, and automated. Charts that get deployed manually from a laptop work fine until they are the production cluster and the laptop is closed.

The release pipeline

Every chart release should flow through a CI/CD pipeline that runs the same validation steps in the same order every time. A manual helm install from a developer’s laptop is fine for dev, but production deserves a pipeline.

A production-ready pipeline runs these steps in sequence:

  1. Lint and schema validation: helm lint --strict ./charts/myapp
  2. Template rendering and diff: helm template --debug ./charts/myapp
  3. Dependency update and lock: helm dependency update ./charts/myapp
  4. Dry-run install in a staging namespace: helm upgrade --install --dry-run --debug
  5. Package: helm package ./charts/myapp
  6. Push to chart repository or OCI registry

All six steps must pass before the pipeline tags the commit with the chart version and triggers a deployment. This sounds like a lot of overhead, but once it lives in a GitHub Actions workflow or a Jenkins stage, it runs in under two minutes and catches problems that would otherwise require a late-night rollback.

Versioning and tagging

Chart versions follow SemVer (major.minor.patch). The version field in Chart.yaml is the chart version, not the application version. The appVersion field is the application version being packaged. Keep them separate so a patch-level chart bump does not confuse people about which application version is deployed.

Tag your git commits to match chart versions. When myapp-1.2.3 is deployed in production, you want to be able to run git log --oneline --grep="1.2.3" and find the exact commit that produced it. This comes in handy during incidents when you need to understand what changed between the working version and the broken one.

CHANGELOG entries worth writing

A CHANGELOG that says “updated dependencies” is not useful. Write entries that help someone debugging at 11pm:

## 1.3.0

### Added

- `ingress.allowedCidrs` value for restricting access by IP range

### Changed

- `postgresql` sub-chart upgraded from 12.2.0 to 12.8.0 (security patch)
- Default `replicaCount` increased from 2 to 3 for production

### Breaking

- `resources.limits.memory` is now required (previously had no default)

The breaking changes section matters most. It tells anyone upgrading from 1.2.x to 1.3.x exactly what to check before running helm upgrade. Without it, they find out about the memory limit requirement the hard way.

Signing charts

Use helm gpg sign to sign your packaged charts if they are distributed via a shared repository or air-gapped transfer. Recipients verify with helm verify before installing. For internal charts behind a VPN, it is overkill. For public distribution, it is mandatory.

When to Use / When Not to Use

Helm is powerful but not always the right choice. Here is when to use it and when to consider alternatives.

When to Use Helm

Use Helm when:

  • You deploy applications to Kubernetes across multiple environments (dev, staging, production)
  • You need to manage complex applications with many Kubernetes resources
  • You want to version and roll back application configurations
  • You are sharing infrastructure or platform components across teams
  • You need to package applications for distribution via chart repositories
  • Your application has environment-specific configuration that changes between deployments

Use Helm for stateful applications when:

  • The application has clear upgrade paths and rollback procedures
  • You have tested hooks for database migrations or similar operations
  • Storage and secrets are properly externalized from the chart

When Not to Use Helm

Helm solves real problems, but it adds complexity that is not always justified. Here is when Helm is the wrong call.

Simple one-off resources

If you are deploying a single Deployment and Service that will never change, raw YAML with kubectl apply -f is faster and easier to understand. The mental overhead of a chart only pays off when you have enough configuration to templatize.

GitOps workflows with ArgoCD or Flux

GitOps tools want a single declarative state. Helm adds a template rendering step that does not always fit cleanly into the GitOps model. ArgoCD and Flux both have Helm integration, but they work most naturally with rendered YAML or Kustomize overlays. If your team is already all-in on GitOps and Helm is causing friction, Kustomize or raw YAML with ArgoCD App-of-Apps may be a better fit.

Highly dynamic workloads

Helm’s release model assumes you are deploying a known configuration that changes infrequently. If your workload reconfigures itself constantly (dynamic autoscaling based on real-time queue depth, on-the-fly replica count changes from an HPA that responds to external load), Helm’s upgrade cycle is too slow. Kubernetes operators handle workload-managed configurations better.

Strict immutability requirements

Helm upgrades modify the release in place. The release history is a sequence of modifications to the same release object. If you need true immutability where nothing is ever overwritten and only new resources are created, Helm is not designed for that. Some compliance frameworks require immutability, so check whether your requirements conflict with Helm’s model before committing to it.

Air-gapped or security-restricted environments

Helm 3 solved most of Helm 2’s security issues by removing Tiller. That said, some environments restrict what the deployment tool can create in the cluster. Helm 3’s service account model is more granular than Tiller, but it still needs create permissions on the resources your chart deploys. Check whether Helm’s RBAC requirements conflict with your environment’s constraints before adopting it at scale.

Helm vs Alternatives

ToolBest ForLimitations
HelmApplication packaging, multi-environment deploymentsTemplate complexity for very dynamic workloads
KustomizeOverlay-based configuration, Git-native workflowsLess structured than Helm charts
ArgoCDGitOps, declarative continuous deliveryRequires additional setup for templating
raw kubectlOne-off deployments, simple resourcesNo parameterization, no rollback support

Production Failure Scenarios

Helm failures in production can block deployments or cause unexpected behavior.

FailureImpactMitigation
Hook failureRelease marked as failed, subsequent hooks do not runDesign hooks to be idempotent, use hook-delete-policy
Template rendering errorDeployment fails silently with wrong configurationUse --dry-run in CI, validate with JSON schema
Dependency unavailableChart fails to install or upgradePin exact versions in Chart.lock, maintain mirror
Release name collisionCannot install or upgrade without forceUse namespaces for isolation, unique naming conventions
Stuck release (pending-upgrade)Cannot upgrade or rollbackUse helm rollback or manually remove finalizers
Upgrade causes data lossStateful workloads may lose dataAlways test with dry-run first, backup before upgrade
Image pull failuresPods hang in Pending stateUse private registries with image pull secrets
Insufficient cluster resourcesPods cannot be scheduledSet appropriate resource requests and limits

Stuck Release Recovery

# Identify stuck releases
helm list --all --failed

# Get release history
helm history myapp --namespace production

# Rollback to last working revision
helm rollback myapp --namespace production

# If rollback fails, manually remove the release
kubectl delete secret -n production -l "owner=helm,name=myapp"

Common Pitfalls / Anti-Patterns

Template Pitfalls

Overusing toYaml

# Anti-pattern: Unbounded toYaml
spec:
  {{- toYaml .Values.podSpec | nindent 8 }}

# Better: Explicitly define expected fields
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    {{- include "myapp.selectorLabels" . | nindent 6 }}

Not handling nil values

# Anti-pattern: Will fail if .Values.ingress is nil
annotations:
  {{- .Values.ingress.annotations | toYaml | nindent 8 }}

# Better: Use conditional checks
{{- with .Values.ingress }}
annotations:
  {{- .annotations | toYaml | nindent 8 }}
{{- end }}

Forgetting the minus sign for whitespace control

# Anti-pattern: Extra blank line before content
spec:
  containers:
    - name: app
      image: {{ .Values.image }}

# Better: Hyphen trims preceding whitespace
spec:
  containers:
    - name: app
      image: {{ .Values.image }}

Values Structure Pitfalls

Flat values that should be nested

# Anti-pattern: Flat structure makes templating complex
replicaCount: 3
imageRepository: myapp
imageTag: "1.0.0"
servicePort: 8080

# Better: Group related values
replicaCount: 3
image:
  repository: myapp
  tag: "1.0.0"
service:
  type: ClusterIP
  port: 8080

Not using JSON schema validation Without schema validation, users can pass any values causing cryptic template errors at render time.

Hook Pitfalls

Non-idempotent hooks

# Anti-pattern: Hook creates duplicate resources each run
metadata:
  annotations:
    "helm.sh/hook": post-upgrade
    "helm.sh/hook-weight": "1"
spec:
  containers:
    - name: migrate
      command: ["/app/migrate.sh"]
      # This runs every upgrade, creating duplicates if not cleaned up

Not handling hook failures gracefully

# Anti-pattern: Hook failure blocks entire release
annotations:
  "helm.sh/hook": post-upgrade
  "helm.sh/hook-failure-policy": fail

# Better: Allow failure without blocking
annotations:
  "helm.sh/hook": post-upgrade
  "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation

Dependency Pitfalls

Not pinning versions

# Anti-pattern: Version range can cause unexpected behavior
dependencies:
  - name: postgresql
    version: ">=12.0.0"

# Better: Pin exact version for reproducibility
dependencies:
  - name: postgresql
    version: "12.8.0"

Circular dependencies Avoid charts that depend on each other. This causes installation and upgrade ordering issues.

Key Takeaways & Checklist

Key Takeaways

Helm is worth the learning curve if you are deploying to Kubernetes across multiple environments. The payoff comes when you need to update a database password across 12 services without editing 12 YAML files manually.

What Helm gives you

The core value is templated configuration that moves with your application from dev to staging to production without duplication. The cascading values system (default values.yaml overridden by environment files, overridden by CLI flags) handles most environment-specific differences without requiring separate chart copies.

Named templates in _helpers.tpl are the mechanism for consistency. Define labels, annotations, and naming conventions once and reuse them everywhere. The first time you need to rename a label across 30 resources, you will be glad you did this.

Hooks solve the stateful problem. Database migrations, cache warmups, and configuration seeding are not part of the stateless app deployment, but they have to run at specific points in the release lifecycle. Hooks give you that control. They must be idempotent and re-runnable, which requires upfront thought.

Where people get into trouble

The most common mistakes: not pinning dependency versions (and getting surprise breaking changes), not adding JSON schema validation (and getting cryptic template errors in production), and not externalizing secrets (and leaking credentials in git). All three are preventable.

Library charts are underused. Once you have three or more charts that share the same deployment pattern, extract the common parts into a library chart. Updating one file beats updating fifteen.

The mental model shift

Helm is a packaging system that makes your Kubernetes manifests portable, not just a deployment tool. Think of it as a versioned, distributable unit of deployment rather than a script that applies YAML. When you frame it that way, the chart structure, the versioning, and the release model all make more sense.

Production Readiness Checklist

# Template validation
helm template myapp ./charts/myapp --debug

# Dry-run installation
helm upgrade --install myapp ./charts/myapp \
  --dry-run --debug \
  --values values-prod.yaml \
  --namespace production

# Schema validation
helm lint ./charts/myapp --strict

# Test installation
helm test myapp --namespace production

# Dependency update and lock
helm dependency update ./charts/myapp
helm dependency build ./charts/myapp

# Verify rendered templates
helm get manifest myapp --namespace production

# Check release status
helm status myapp --namespace production
helm history myapp --namespace production

Pre-Production Validation Commands

# Scan for vulnerabilities
trivy image $(helm get values myapp -n production -o jsonpath='{.data.image}' | base64 -d)

# Verify RBAC permissions
kubectl auth can-i get pods --as=system:serviceaccount:production:helm-deployer -n production

# Check for deprecated APIs
helm template myapp ./charts/myapp | kubeval --strict

# Review rendered YAML differences
helm diff upgrade myapp ./charts/myapp --values values-prod.yaml -n production

Observability Checklist

Helm charts need observability at both the release and application level.

Release Monitoring

graph LR
    A[Helm Release] --> B[Release Metadata]
    A --> C[Revision History]
    A --> D[Resource Status]
    B --> E[Name Namespace]
    B --> F[Chart Version]
    C --> G[Upgrade Rollback]
    D --> H[Pod Health]
    D --> I[PVC Bound]

Track these release metrics:

  • Release revision count: too many revisions indicate instability
  • Time since last successful deployment
  • Number of failed releases across environments
  • Hook execution success rates

Application Observability

graph TD
    A[Application Metrics] --> B[Pod Resource Usage]
    A --> C[Service Endpoints]
    A --> D[Ingress Status]
    E[Kubernetes Events] --> F[Pod Scheduling]
    E --> G[Volume Mounts]
    E --> H[Image Pulls]

Metrics to monitor:

  • Pod CPU and memory actual usage vs requested
  • Service endpoint availability
  • Persistent volume claim status and usage
  • Ingress controller backend health
  • Container image versions deployed

Alert Configuration

Critical alerts:

  • Helm release failed during production deployment
  • Pods in CrashLoopBackOff after Helm upgrade
  • Hook jobs failing repeatedly
  • Release revision rapidly increasing (indicates instability)

Warning alerts:

  • PVC pending for more than 5 minutes
  • Deployment replica count below desired
  • Image pull backoff occurring

Security Checklist

Helm charts require security review before production deployment.

Chart Security

  • Scan for vulnerabilities: Use Trivy or Snyk to scan chart dependencies

    trivy chart ./charts/myapp
  • Verify chart signatures: Use GPG signing for chart verification

    helm verify myapp-1.0.0.tgz
  • Review values files: Ensure no hardcoded secrets or insecure defaults

  • Validate values schema: Use JSON schema to enforce secure configurations

Image Security

# values.yaml security settings
image:
  repository: myregistry.myapp.com/api
  pullPolicy: IfNotPresent
  securityContext:
    runAsNonRoot: true
    runAsUser: 10000

podSecurityContext:
  runAsNonRoot: true
  fsGroup: 10000

securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop:
      - ALL

Image hardening checklist:

  • Use specific image tags, never latest
  • Scan images for CVEs before deployment
  • Use private registry with authentication
  • Set imagePullPolicy appropriately (IfNotPresent or Always)

RBAC for Helm

Helm 3 uses the permissions of the user or service account running it.

# Create dedicated SA for Helm
kubectl create serviceaccount helm-deployer -n production

# Grant only needed permissions
kubectl create role helm-deployer --verb=get,list,watch,create,update,patch,delete \
  --resource=deployments,services,configmaps,secrets,pvc -n production

kubectl create rolebinding helm-deployer-binding \
  --role=helm-deployer --serviceaccount=production:helm-deployer -n production

Secret Management

Never:

  • Commit secrets to values files
  • Store secrets in Chart.yaml or templates
  • Use ConfigMap for sensitive data

Always:

  • Use External Secrets Operator or Vault
  • Reference secrets from external secret stores
  • Use --set-file for certificate contents

Trade-off Analysis

AspectHelmKustomizeCarvel (kapp-controller)
ModelTemplate + valuesOverlay + patchesPackage + config
Learning curveModerate (Go templates)Low (YAML only)Moderate
Reusable packagesYes (chart repos)Limited (git-based)Yes (imgpkg bundles)
DebuggingRender and inspectBuild and diffBuild and inspect
EcosystemMassive (Bitnami)GrowingSmaller but maturing
GitOps friendlyYes (with Flux/ArgoCD)NativeNative
Secret managementValues encryptionSealed secretsBank-Vaults

Helmfile vs Helm

Helmfile is a declarative deployment tool that sits above Helm, letting you manage multiple charts and environments in a single configuration file.

When Helmfile Helps

# helmfile.yaml
releases:
  - name: myapp
    chart: ./charts/myapp
    values:
      - values-prod.yaml
    set:
      - name: replicaCount
        value: 5

  - name: postgresql
    chart: bitnami/postgresql
    values:
      - db-prod.yaml

Key Differences

AspectHelm AloneHelmfile
Multi-chart manageManual per-chart commandsSingle file declares all charts
Environment configManual —values flagsEnvironments block with overrides
State trackinghelm list per namespaceDeclarative state file
TemplatingGo templates in chartsHelmfile has own templating
Sync behaviorApply individuallyAuto detects drift and syncs

CI/CD Integration

Helmfile excels in GitOps pipelines:

# Preview what would change
helmfile diff

# Apply changes
helmfile apply

# Target specific environment
helmfile --env production apply

Helmfile keeps your Helm deployments declarative and auditable. Use it when managing more than 2-3 charts or multiple environments.

OCI Registry Support

Helm 3 added native OCI support for distributing charts via container registries. This unifies chart distribution with your existing container workflow.

Login to Registry

Authenticating to an OCI registry uses helm registry login, the same command you use for container images. Credentials are stored in ~/.config/helm/auth.yml, separate from the chart.

For interactive logins, provide username and password at the prompt:

helm registry login registry.example.com

For automation pipelines, pass credentials via stdin so passwords never end up in shell history:

echo $OCI_TOKEN | helm registry login -u myuser --password-stdin registry.example.com

Some registries support token-based authentication or keytabs instead of password login. Check your registry documentation for the appropriate authentication method. For CI/CD systems, prefer using service account tokens or workload identity rather than storing static credentials.

Push and Pull Charts

# Package chart first
helm package ./charts/myapp

# Push to OCI registry
helm push myapp-1.2.3.tgz oci://registry.example.com/charts

# Pull from registry
helm pull oci://registry.example.com/charts/myapp --version 1.2.3

Install from OCI

Once your chart is pushed to an OCI registry, installing it works like any Helm install but with an oci:// URL instead of a chart path. You can pin to a specific version or let Helm pull the latest:

# Install specific version
helm upgrade --install myapp oci://registry.example.com/charts/myapp \
  --version 1.2.3 \
  --namespace production

# Install latest (use with caution in production)
helm upgrade --install myapp oci://registry.example.com/charts/myapp \
  --namespace production

The --version flag accepts SemVer constraints like >=1.0.0 if you want to pin a minimum version. Without a version constraint, Helm always resolves to the newest available tag, which can cause surprises if you forget to pin.

Helm pulls the chart layer from the registry, verifies the digest against the stored manifest, and renders it just like a local chart. You can combine OCI installs with --values and --set flags the same way you would with any Helm release.

OCI Workflow Benefits

  • Unified auth: Use same credentials as container images
  • Existing infra: No separate chart repository server needed
  • Mirroring: Charts travel with container images in air-gapped setups
  • Versioning: OCI registries handle deduplication and layering

Limitations: OCI registries do not serve index.yaml, so helm search does not work directly. Use helm fetch with exact versions.

Rollback Strategies

Helm tracks every release revision. When deployments go sideways, you have multiple rollback strategies.

Basic Rollback

Helm keeps a full revision history for every release, so you can always undo an upgrade. The simplest case is rolling back to the previous revision, which requires no additional arguments:

# Rollback to previous revision
helm rollback myapp --namespace production

# Rollback to specific revision
helm rollback myapp 3 --namespace production

When you specify a revision number, Helm rolls back to that exact revision regardless of how far back it is. Revision numbers start at 1 for the initial install and increment with each upgrade. Running helm history myapp --namespace production shows you the full revision timeline with timestamps and status, so you can pick the right revision to roll back to.

Rollback is treated as an upgrade in Helm’s model, so it creates a new revision (revision 5 after rolling back from revision 4 to revision 3). This keeps the full audit trail intact so you never lose history.

Rollback with Dry-Run

Always verify before rolling back in production:

# See what the rollback would do
helm rollback myapp --dry-run --debug --namespace production

# Check release history first
helm history myapp --namespace production

Output:

REVISION  UPDATED                  STATUS     CHART           DESCRIPTION
1         2026-03-15 10:30:00     superseded myapp-1.0.0    Install complete
2         2026-03-15 11:45:00     superseded myapp-1.1.0    Upgrade complete
3         2026-03-15 14:20:00     failed     myapp-1.2.0     Upgrade failed
4         2026-03-15 14:25:00     deployed   myapp-1.1.0     Rollback complete

Selective Rollback to Known Good State

For critical applications where you know a specific revision is stable, you can target it directly rather than relying on the immediately previous revision:

# Identify stable revision
helm history myapp --namespace production | grep superseded

# Rollback two revisions (from bad upgrade to last good)
helm rollback myapp 2 --namespace production

Looking at the revision history, entries marked superseded are safe rollback targets because they represent a state that previously deployed successfully. Entries marked failed indicate an upgrade that did not complete, and deployed is the current active state. For stateful applications, document your known-good revisions for each major version in a runbook so on-call engineers are not guessing during an incident.

Before rolling back in production, always run helm rollback --dry-run --debug to see exactly what will change. This is especially important for stateful workloads where the rollback might affect persistent storage or database schemas. If the dry-run output looks correct, proceed with the actual rollback but keep the output handy for post-incident review.

Automated Rollback in CI/CD

# In your deployment pipeline
helm upgrade --install myapp ./charts/myapp \
  --values values-prod.yaml \
  --namespace production \
  --atomic \
  --timeout 5m

# --atomic automatically rolls back on failure

Preventing Bad Rollbacks

# Lock release so it cannot be accidentally rolled back
helm upgrade --install myapp ./charts/myapp \
  --set locked=true

# Use fail-fast hooks to catch issues early
helm upgrade myapp ./charts/myapp --timeout 2m

Interview Questions

1. What is Helm and why would you use it over plain kubectl for Kubernetes deployments?

Expected answer points:

  • Helm is the package manager for Kubernetes, bundling related manifests into a reusable chart
  • It provides templating so one chart works across dev, staging, and production with different values
  • Release management includes versioning, rollback, and upgrade tracking that kubectl lacks
  • Charts can be shared via repositories, enabling reuse of community charts like Bitnami's
2. Explain the difference between Helm 2 (Tiller) and Helm 3 architecture.

Expected answer points:

  • Helm 2 used Tiller, a server-side component that ran inside the cluster and managed releases
  • Helm 3 removed Tiller entirely, using cluster service accounts instead for security
  • Release metadata moved from cluster storage to secrets, reducing RBAC complexity
  • Three-way (or four-way) merge diffs in Helm 3 prevent accidental rollbacks on drift
3. How does the Helm values cascading hierarchy work?

Expected answer points:

  • Default values live in values.yaml inside the chart
  • Environment-specific files (values-staging.yaml, values-prod.yaml) override defaults
  • Command-line --set and --set-file flags override everything
  • Order matters: defaults < environment file < CLI flags
4. What are named templates in Helm and when would you use them?

Expected answer points:

  • Named templates (partials) live in templates/_helpers.tpl and are reusable across templates
  • Define once, include everywhere with {{ include "myapp.labels" . | nindent 4 }}
  • Common uses: chart name, fullname, labels, selectorLabels, common annotations
  • Reduce duplication and enforce consistency across Kubernetes resources
5. What are Helm hooks and how do you design them for production?

Expected answer points:

  • Hooks run Jobs or Pods at specific lifecycle points: pre-install, post-install, pre-upgrade, post-upgrade, pre-delete, post-delete
  • Hook weight controls execution order (negative runs first)
  • Design hooks to be idempotent: use hook-delete-policy to re-run cleanly
  • Database migrations are the classic use case; they must tolerate being re-run
  • hook-delete-policy: hook-succeeded,before-hook-creation prevents duplicates
6. How do you manage secrets in Helm charts without hardcoding them?

Expected answer points:

  • Never commit secrets to values.yaml or Chart.yaml
  • Use External Secrets Operator to sync secrets from Vault, AWS Secrets Manager, or GCP Secret Manager
  • Use --set-file to load certificate or key file contents at deploy time
  • HashiCorp Vault CSI provider can inject secrets as mounted files
  • For testing, use test values that are clearly marked as non-production
7. What is the difference between Helm library charts and application charts?

Expected answer points:

  • Application charts produce actual Kubernetes resources when installed
  • Library charts set type: library and define reusable template partials (_deployment.yaml, _service.yaml)
  • Other charts depend on library charts and import their templates
  • Library charts are useful for standardizing organization-wide patterns
8. How does JSON schema validation work in Helm charts?

Expected answer points:

  • values.schema.json enforces type, constraints, and required fields on values provided to the chart
  • Helm validates during helm template and helm install/upgrade with --strict flag
  • Rejects invalid configuration before any resources are rendered
  • Example: replicaCount must be integer 1-10, image.repository is required, pullPolicy enum
9. When would you choose Helmfile over plain Helm?

Expected answer points:

  • Helmfile excels when you manage multiple charts across multiple environments in one place
  • Useful when your deployment involves 3+ charts that need to be deployed together
  • Environment blocks with overrides keep environment-specific config clean and auditable
  • helmfile diff shows exactly what would change before applying, useful in CI/CD
  • Not needed for simple single-chart deployments where Helm alone suffices
10. How do you handle Helm chart rollbacks in a CI/CD pipeline safely?

Expected answer points:

  • Always check helm history before rolling back to see all revisions
  • Use helm rollback with --dry-run --debug to preview the rollback first
  • Use the --atomic flag during upgrade to auto-rollback on failure
  • Set a timeout to prevent indefinite hangs during broken deployments
  • Store rollback playbooks in runbooks so on-call engineers do not have to improvise
11. Explain how Helm template debugging works and what tools you use.

Expected answer points:

  • Use helm template to render charts locally without cluster access
  • Use --debug with helm install/upgrade to see rendered manifests
  • Use helm get manifest to inspect what was actually deployed
  • Use --dry-run --debug to validate without making changes
  • Use printf debugging in templates with {{ .Values | toJson }}
  • Run helm lint to catch syntax errors and schema violations
12. What is the difference between Helm library charts and application charts?

Expected answer points:

  • Application charts set type: application and produce actual Kubernetes resources when installed
  • Library charts set type: library and define reusable template partials (_deployment.yaml, _service.yaml)
  • Other charts depend on library charts and import their templates via import-values
  • Library charts are useful for standardizing organization-wide patterns across teams
  • Library charts cannot install resources directly, only provide templates
13. How do chart dependencies work in Helm 3 and what is the Chart.lock file?

Expected answer points:

  • Dependencies are defined in Chart.yaml under dependencies[] with name, version, and repository
  • Run helm dependency update to download dependencies to the charts/ directory
  • Chart.lock is auto-generated and locks exact versions from the dependency resolution
  • Commit Chart.lock to ensure reproducible installs across machines
  • Use helm dependency build to install from Chart.lock without re-resolving
  • Conditions and tags in Chart.yaml control when sub-charts are enabled
14. Describe the Helm release lifecycle and how hooks interact with it.

Expected answer points:

  • Lifecycle: install → upgrade → uninstall, each with pre/post phases
  • Hook types: pre-install, post-install, pre-upgrade, post-upgrade, pre-delete, post-delete, test
  • Hook weight determines execution order (negative weights run first)
  • Use hook-delete-policy to control when resources are cleaned up (hook-succeeded, before-hook-creation, hook-failed)
  • Hooks are Jobs that run to completion; release waits for hook to finish
  • Failed hooks mark the release as failed and block subsequent hooks
15. What are the security considerations when using Helm in a production cluster?

Expected answer points:

  • Helm 3 uses RBAC service accounts, so grant only necessary permissions
  • Never embed secrets in values files; use External Secrets Operator or Vault
  • Sign charts with GPG and verify with helm verify before installation
  • Scan charts and container images for vulnerabilities with Trivy or Snyk
  • Use --set-file for certificates instead of hardcoding values
  • Lock chart versions in Chart.lock to prevent unexpected updates
16. How does JSON schema validation prevent misconfiguration in Helm charts?

Expected answer points:

  • values.schema.json defines type constraints, required fields, and allowed values
  • Helm validates during helm template and helm install/upgrade --strict
  • Rejects invalid configuration before any resources are rendered to the cluster
  • Prevents runtime errors from missing required fields or wrong types
  • Example: replicaCount must be integer 1-10, image.repository is required, pullPolicy enum
17. When would you choose OCI registry support over traditional chart repositories?

Expected answer points:

  • Use OCI when you want unified authentication with container images (same registry credentials)
  • Useful for air-gapped environments where charts travel with container images
  • OCI registries handle deduplication and layering efficiently
  • Choose traditional repos when you need helm search repo functionality
  • OCI works well with GitOps workflows where charts are versioned alongside images
  • Limitation: no index.yaml means helm search does not work with OCI registries
18. What is Helmfile and when does it become preferable to plain Helm commands?

Expected answer points:

  • Helmfile is a declarative tool that sits above Helm, managing multiple charts in one file
  • Prefer Helmfile when managing 3+ charts or multiple environments (dev, staging, production)
  • Environment blocks with overrides keep environment-specific config auditable
  • helmfile diff shows exactly what would change before applying, useful in CI/CD
  • Single source of truth for all chart deployments in a project
  • Not needed for simple single-chart deployments where Helm alone suffices
19. How do you structure Helm charts for monorepos with multiple microservices?

Expected answer points:

  • Each microservice gets its own chart in a charts/ directory at repo root
  • Use Helmfile at the repo root to declare all chart releases and their dependencies
  • Extract shared templates into library charts that all microservices import
  • Use values-{env}.yaml files at the repo level, not inside individual charts
  • Group related services (e.g., backend-api, frontend-web) under a single release if they deploy together
  • Use hook-weight for migration jobs that must run before application pods start
20. Explain the difference between Helm's three-way (or four-way) merge in Helm 3.

Expected answer points:

  • Helm 3 introduced three-way merge to prevent accidental rollback on configuration drift
  • Three-way merge considers: last release manifest, current cluster state, new values
  • Prevents overwriting changes made directly in the cluster that are not in the previous release
  • Four-way diff (upgrade) compares: last release, current cluster, new template, new values
  • Result: only changes from new values are applied, cluster changes outside Helm are preserved
  • This makes Helm 3 safer for production use where cluster state may diverge from git

Further Reading

Conclusion

Helm makes Kubernetes deployments actually manageable. Without it, you are juggling YAML files across environments, hoping nothing drifts out of sync. The chart structure, Go templating, and dependency system feel clunky at first, but once they click, you will wonder how you lived without it.

Start by converting your existing Kubernetes manifests to a chart. Add templating for environment-specific values. Then extract reusable components into library charts as patterns emerge. You will iterate on your chart structure as you go and that is fine.

For continued learning, explore the Advanced Kubernetes guide for operators and controllers that work alongside Helm. The Prometheus & Grafana guides cover observability for your Helm-deployed applications.

Category

Related Posts

Developing Helm Charts: Templates, Values, and Testing

Create production-ready Helm charts with Go templates, custom value schemas, and testing using Helm unittest and ct.

#helm #kubernetes #devops

Helm Versioning and Rollback: Managing Application Releases

Master Helm release management—revision history, automated rollbacks, rollback strategies, and handling failed releases gracefully.

#helm #kubernetes #devops

Advanced Kubernetes: Controllers, Operators, RBAC, Production Patterns

Explore Kubernetes custom controllers, operators, RBAC, network policies, storage classes, and advanced patterns for production cluster management.

#kubernetes #containers #devops