Mid-level 9 min · March 06, 2026

Helm Upgrade Deleted Production ConfigMap — Ownership

Helm's resource ownership model caused outage when a ConfigMap was removed from chart v1.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Helm is the package manager for Kubernetes — it bundles YAML manifests into a reusable Chart.
  • Charts contain templates, default values, and dependency metadata in a standard directory layout.
  • The templating engine uses Go templates with Sprig functions; values.yaml supplies default overrides.
  • Helm tracks releases in Secret objects (v3), enabling atomic upgrades and rollbacks without external state.
  • Production pitfall: templating logic that works in dev often breaks in prod due to missing values or environment-specific structure.
  • Key rule: keep templates declarative and push logic to values files to avoid debugging Go template syntax at 2 AM.
Plain-English First

Imagine you're moving into a new apartment. Instead of buying furniture piece by piece and figuring out where everything goes each time, you hire an interior design service that hands you a single catalog. You tick a few boxes — 'I want 2 bedrooms, modern style, budget mid-range' — and they configure, order, and arrange everything perfectly. Helm is that catalog service for Kubernetes. Your application has dozens of moving parts (Deployments, Services, ConfigMaps, Secrets, Ingresses), and Helm bundles them into one tidy package called a Chart so you can install, upgrade, or roll back the whole apartment with one command.

Kubernetes solves the hard problem of running containers at scale, but it hands you a new problem in return: you're now managing dozens of YAML files per application, each tightly coupled to an environment, a team convention, or a one-off config decision made eighteen months ago by someone who has since left. At five services this is annoying. At fifty, it's a liability. Helm exists precisely because the Kubernetes community hit this wall and needed a package manager — the same reason Linux got apt and Node got npm.

Helm solves three distinct problems in one tool. First, it packages all Kubernetes manifests for an application into a versioned, distributable artifact. Second, it gives you a templating layer so a single chart can target dev, staging, and production with different values rather than copy-pasted YAML. Third, it tracks release state in the cluster itself, enabling atomic upgrades and rollbacks without external tooling. These three capabilities together are why Helm became the de-facto standard for application delivery on Kubernetes.

By the end of this article you'll understand how Helm's rendering pipeline actually works under the hood, how to write charts that are safe for production (not just tutorials), how hooks let you orchestrate pre- and post-install logic, and where Helm's design choices create real operational risk if you're not paying attention. We'll build a realistic multi-environment chart from scratch, examine the Release object internals, and walk through the patterns that separate hobby charts from charts you'd trust in a regulated, high-availability environment.

Chart Structure and Anatomy

A Helm chart is a directory with a standardised layout. At the root you'll find Chart.yaml (metadata), values.yaml (default configuration), and a templates/ folder containing Go template files that expand to Kubernetes manifests. Optional directories include charts/ (for dependencies) and crds/ (for CRDs that must be installed before the templates).

Understanding this structure is the first step to writing production-grade charts. Every file has a purpose: Chart.yaml holds version, appVersion, and dependencies; values.yaml defines the config surface your team will override; and the templates/ folder contains the logic that turns those values into Deployments, Services, and everything else. The secret to maintainable charts is keeping templates thin and making values.yaml expressive. Don't hide logic in templates — expose it as knobs in values.yaml and document each one.

The chart version is separate from the application version. Use semantic versioning for the chart and appVersion for the underlying software. Helm's release system uses the chart version to determine upgrade paths, so bumping the chart version even for trivial changes is critical for traceability.

mychart/Chart.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# TheCodeForge - Sample Chart.yaml
apiVersion: v2
name: myapp
description: A production-grade web application
version: 1.2.0
appVersion: "3.0.1"
type: application
dependencies:
  - name: redis
    version: "~17.0.0"
    repository: "https://charts.bitnami.com/bitnami"
    condition: redis.enabled
  - name: postgresql
    version: "^12.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled
Key Detail
The 'type' field can be 'application' (deployable) or 'library' (reusable helpers). Library charts never produce their own release — they are pulled in as dependencies and export templates.
Production Insight
If you change the chart version without bumping it, Helm cannot distinguish between the old and new chart.
This silently prevents rollbacks because all revisions share the same chart version.
Rule: always bump version (patch, minor, major) for every change that affects the rendered output.
Key Takeaway
Chart.yaml is the contract.
Version your chart independently of the app.
Library charts reduce duplication across teams.
Choosing between application and library chart type
IfThe chart will be installed directly into a namespace
UseUse type: application. It generates a Helm release and can be managed with install/upgrade/delete.
IfThe chart only provides reusable template helpers or sub-charts
UseUse type: library. Library charts are never installed directly; they are pulled as dependencies.
IfYou need to maintain a common set of helpers across multiple application charts
UseCreate a library chart, push it to a chart repository, and list it in the dependencies block of each application chart.

Helm Chart Directory Structure Visual

A clear mental model of the chart directory tree is essential for debugging and team collaboration. Below is the canonical directory layout for a Helm chart named mychart. Every production chart should follow this structure strictly — deviations confuse operators and break dependency management.

The root directory contains three mandatory items: Chart.yaml (chart metadata), values.yaml (default configuration values), and the templates/ directory (Kubernetes manifest templates). Optionally, a charts/ directory holds packaged subchart dependencies (downloaded via helm dependency update), a crds/ directory for CustomResourceDefinitions (installed before templates), and a README.md for documentation. The templates/ directory may contain a _helpers.tpl file for reusable template functions and tests/ for test manifests (run with helm test).

Understanding this layout at a glance helps you quickly locate the source of a misconfigured resource: if a ConfigMap is missing, check whether the template file exists under templates/ and whether it's gated by an if condition in the template logic. If a dependency isn't installed, check charts/ or Chart.yaml dependencies block.

Naming Convention
Keep template filenames consistent with the resource kind: deployment.yaml, service.yaml, configmap.yaml, ingress.yaml. This makes it trivial to find a specific resource during an incident.
Production Insight
When your chart grows beyond 20 templates, split them into subdirectories under templates/ (e.g., templates/app/, templates/db/). Helm will recursively render all .yaml and .tpl files. But be careful: subdirectories are not scoped — a file in templates/app/ can access the same .Values as templates/db/. Use naming prefixes to avoid collisions.
Key Takeaway
Standard directory layout ensures consistency. Use subdirectories for large charts. Always include a .helmignore file to exclude sensitive files from the chart package.

Template Syntax (Go Templates) Basics

Helm uses Go's text/template engine to turn template files into manifest YAML. If you've never written a Go template, the syntax can look cryptic. This section gives you the foundational constructs you need to read and write chart templates confidently.

The core syntax is {{ ... }} — anything inside double curly braces is evaluated as a Go template expression. Plain text outside the braces is output as-is. The dot (.) represents the current context, which is the root of the merged values object at the top level. You access values with .Values.<key>. For example, {{ .Values.replicaCount }} outputs the replica count from values.yaml.

Control structures
  • if/else: {{ if .Values.ingress.enabled }} ... {{ end }}
  • range: loops over a list or map. {{ range .Values.containers }} ... {{ end }} — inside the range, dot changes to the current item. Use $. to access the root context.
  • with: scopes dot to a specific value. {{ with .Values.database }} ... {{ end }} — inside, . refers to the database object.

Variables: assign values with $var :=. Useful to cache complex expressions: {{ $port := .Values.service.port | default 8080 }}.

Functions: Helm includes all Sprig functions (e.g., upper, default, quote, toYaml, b64enc). The pipe syntax chains functions: {{ .Values.name | upper | quote }}.

The include function is critical for reusing template blocks. It takes a template name and a context: {{ include "mychart.labels" . }} renders the _helpers.tpl block called "mychart.labels" with the current dot.

Escaping and whitespace: {{- trims leading whitespace; -}} trims trailing whitespace. Always use {{- and -}} inside control structures to avoid blank lines in rendered YAML.

Built-in objects: Besides .Values, Helm provides .Release (Name, Namespace, Service, Revision), .Chart (metadata from Chart.yaml), .Files (access to chart files outside templates), and .Capabilities (cluster API versions).

templates/example-syntax.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# TheCodeForge - Go Template syntax examples in Helm
apiVersion: v1
kind: ConfigMap
metadata:
  # include a helper template, trim leading whitespace
  name: {{- include "mychart.fullname" . }}-config
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
data:
  # default function provides fallback
  app_port: "{{ .Values.service.port | default 8080 | quote }}"
  # range loop over a list
  allowed_origins: |
    {{- range .Values.allowedOrigins }}
    - {{ . | quote }}
    {{- end }}
  # with scoping — . refers to .Values.database inside
  {{- with .Values.database }}
  db_host: {{ .host | required "db.host is required" | quote }}
  db_port: {{ .port | default 5432 | quote }}
  {{- end }}
  # inline if condition
  feature_debug: {{ .Values.debug | default false | quote }}
Whitespace Trimming
Forgetting {{- in range/if blocks leaves huge blank lines in your YAML, causing indentation errors or invalid manifests. Always use {{- at the start and -}} at the end of control structures that span multiple lines.
Production Insight
Never call required inside a non-root scope (e.g., inside a range) without also providing a fallback. If the required value is missing, Helm stops rendering immediately — and the error message does not tell you which iteration failed. Instead, validate critical values outside the loop using an initial if block.
Key Takeaway
Go templates use {{ }} delimiters. Dot is the context. Use include, default, required, and whitespace trimming. Practice with helm template --debug before deploying.

Templating Engine Internals: How Values and Templates Merge

Helm uses Go's text/template engine with an extended function set from Sprig and a few Helm-specific functions (include, required, toYaml, etc.). The rendering pipeline works in three phases: parse, evaluate, and produce YAML.

  1. Parse: Helm reads all template files (.yaml, .tpl) from the templates/ directory and the values from values.yaml + any --set or --values flags. Values are merged in order: built-in defaults, values.yaml, parent chart values, user-supplied values. Later overrides win.
  2. Evaluate: Go templates are executed. The context (dot) starts as the root of the merged values object. You access values with .Values.someKey. Nested keys use dot notation. Templates can use control structures (if/else, range, with), variables ($val := .Values.someKey), and pipelines. The 'required' function is a common safety net: required "Port is mandatory" .Values.service.port.
  3. Produce: The evaluated templates are concatenated into a single YAML document. If an evaluated template produces multiple YAML documents (separated by ---), each is treated as a separate resource. Validation then runs against the cluster's API before anything is applied.

A common trap: scope. The 'range' instruction inside a template changes the dot to the current iteration item. If you need to access the top-level Values inside a range, save it beforehand with $root := ..

templates/configmap.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
# TheCodeForge - A well-structured ConfigMap template
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "myapp.fullname" . }}-config
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
data:
  # Tolerates missing value by falling back to a sensible default
  app_port: "{{ .Values.service.port | default 8080 | quote }}"
  log_level: "{{ .Values.logging.level | default "info" }}"
  feature_flags: |
    {{- .Values.featureFlags | toYaml | nindent 4 }}
Common Misconception
The '--reuse-values' flag does NOT merge new values with old values. It reuses the exact set of values from the previous release. If you add a new key to values.yaml, --reuse-values will ignore it. This is a frequent source of surprise during upgrades.
Production Insight
Using 'required' in templates prevents silent empty values.
Without it, a missing .Values.db.host renders an empty string and the pod crashes on startup.
Rule: use required for every config value that has no sensible default.
Key Takeaway
Values merge order: defaults < values.yaml < parent values < --set/--values.
Watch out: --reuse-values skips new keys.
Use required() for mandatory fields.

Release Lifecycle: Install, Upgrade, Rollback, Delete

Helm tracks releases via Secrets in the target namespace (v3). Each release revision gets a Secret named sh.helm.release.v1.<RELEASE-NAME>.v<REVISION>. The Secret contains the rendered manifests, values, and metadata. This in-cluster storage is the source of truth for all Helm operations.

  • Install: Creates the first revision. Helm renders templates and applies them to the cluster. If any resource fails, the install is rolled back (the release is marked FAILED, not deleted). You can then inspect the failure and retry.
  • Upgrade: Creates a new revision. By default, Helm does a three-way strategic merge patch: it compares the current live state, the previous release state, and the desired state. This allows it to detect and preserve manual changes (but also creates risk if resources are removed from templates — see production incident).
  • Rollback: Creates a new revision that is a copy of a previous revision's manifests. Rollback is itself an upgrade — it increments the revision number. This means you can rollback a rollback.
  • Delete: Removes the release record and deletes all resources that were created during the install. By default, the history is preserved so you can still see release names. Pass '--keep-history' to retain release secrets for audit purposes.

Understanding the upgrade strategy is vital. The three-way merge is powerful but can produce surprising results when coupled with hooks, custom resource definitions, or manually patched resources.

deploy.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# TheCodeForge - Safe upgrade with dry-run and diff
helm diff upgrade my-release ./mychart --values values-prod.yaml
if [ $? -eq 0 ]; then
  echo "No changes detected, skipping."
elif [ $? -eq 2 ]; then
  echo "Changes detected. Proceeding with dry-run..."
  helm upgrade my-release ./mychart --values values-prod.yaml --dry-run
  read -p "Apply? (y/N) " -n 1 -r
  if [[ $REPLY =~ ^[Yy]$ ]]; then
    helm upgrade my-release ./mychart --values values-prod.yaml --atomic --timeout 10m
  fi
else
  echo "Diff failed, check Helm version and cluster connectivity."
  exit 1
fi
Helm Release as Git
  • Install = initial commit (revision 1)
  • Upgrade = new commit (revision 2, 3, ...)
  • Rollback = revert to a previous commit (still creates a new revision)
  • History = git log -- you can always see what changed
  • Force push = helm upgrade --force (overrides conflicts, but can lose data)
Production Insight
Running helm upgrade without --atomic can leave a release in 'pending-upgrade' state if the operation times out.
A pending upgrade blocks all future operations on that release.
Rule: always use --atomic and --timeout in automated pipelines to guarantee rollback on failure.
Key Takeaway
Helm uses Secrets to store release state.
Upgrades are three-way merges.
Use --atomic for CI/CD.
Rollback is just another upgrade.

Hooks: Pre- and Post-Operations

Helm hooks allow you to run actions at specific points in the release lifecycle — before install, after install, before upgrade, after upgrade, before delete, after delete. Hooks are defined by annotating a template with "helm.sh/hook": <hook-name> (e.g., pre-install, post-upgrade).

Hooks run as Kubernetes Jobs, and their pods must complete successfully for the lifecycle phase to proceed. If a pre-install hook fails, the install fails. Hooks can also have a weight to control execution order. Lower weight runs first; same weight runs in alphabetical order.

Common use cases
  • pre-install/pre-upgrade: Database migration jobs, secret generation (e.g., via a tool that writes to a Vault), or validation scripts.
  • post-install/post-upgrade: Notifications to Slack, integration tests, or warm-up requests.
  • pre-delete/pre-rollback: Cleanup of external resources (DNS records, load balancers) or backup creation.

A hidden danger: hook resources are NOT part of the release's managed resource set. If you delete a release, hook resources (if they completed) are not automatically deleted. You must clean them up manually or use the hook-delete-policy annotation.

templates/migration-job.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# TheCodeForge - Database migration hook
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "myapp.fullname" . }}-migration
  annotations:
    "helm.sh/hook": pre-upgrade
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: migration
        image: {{ .Values.migration.image | quote }}
        env:
        - name: DB_HOST
          value: {{ required "A valid .Values.db.host is required" .Values.db.host | quote }}
Hook Cleanup Trap
If you do not set 'hook-delete-policy', completed hook jobs remain in the namespace indefinitely. This can exhaust disk space on the cluster's etcd or confuse operators who see many completed jobs. Always set a delete policy.
Production Insight
Hooks that talk to external services (databases, APIs) must handle transient failures.
A five-second network blip can cause a post-upgrade hook to fail and mark the upgrade as failed.
Rule: wrap hook logic with retries and exponential backoff inside the container.
Key Takeaway
Hooks run as Jobs before/after lifecycle events.
Hook resources are not tracked in the release.
Always set hook-delete-policy.
Add retries inside hook containers.

Helm Hooks Lifecycle Reference Table

Hooks are triggered at specific points during the Helm release lifecycle. The table below summarises all available hook annotations, when they fire, and whether they support weights. Use this as a quick reference when designing hook-based workflows.

Hook annotationFires duringSupports weightCommon use case
pre-installAfter templates are rendered, before any resources are createdYesDatabase schema migration, secret generation
post-installAfter all resources are successfully createdYesIntegration test, notification (e.g., Slack)
pre-upgradeAfter templates are rendered for upgrade, before applying changesYesMigration, pre-flight config validation
post-upgradeAfter upgrade resources are applied successfullyYesWarm-up requests, smoke tests
pre-rollbackBefore rollback manifests are appliedYesBackup creation, external state snapshot
post-rollbackAfter rollback is completeYesRestore validation, notification
pre-deleteBefore release deletionYesClean up external resources (DNS, load balancers)
post-deleteAfter release deletionYesAudit logging, resource reconciliation
testWhen helm test is runNoConnectivity checks, health validation

Weights range from negative to positive integers; lower numbers run first. Hooks with the same weight run in alphabetical order by resource name. The hook-delete-policy annotation controls cleanup: before-hook-creation (remove previous hook before new one), hook-succeeded (delete after success), hook-failed (keep failed hook for debugging).

A common pattern: use a single pre-install hook with hook-delete-policy: before-hook-creation,hook-succeeded to ensure only one completed job remains in the namespace at any time. For critical migrations, set hook-succeeded only (keep for audit), but monitor namespace resource usage.

Hook Deletion Policy Best Practice
Always combine before-hook-creation with either hook-succeeded or hook-failed. When using hook-failed, you can inspect the job logs before Helm creates a new one on retry. In production pipelines, set hook-succeeded and rely on external monitoring to capture failures.
Production Insight
Hooks with high weight (e.g., 10) run last. Use high-weight pre-upgrade hooks to run final validation (e.g., linting credentials) just before the upgrade applies. This reduces the time between validation and resource creation, minimising the risk of env drift between pre-check and apply.
Key Takeaway
Hooks fire at fixed lifecycle points. Use weights to order execution. Always set hook-delete-policy. Annotate the use case in comments.

Production Patterns: Multi-Environment, Dependencies, and Library Charts

Production Helm usage demands patterns that separate environments, manage interdependencies, and share common code across teams.

Multi-environment values: Maintain separate values files: values-dev.yaml, values-staging.yaml, values-prod.yaml. Use a base values.yaml for defaults and override only the differences. Avoid copying the entire values structure; use granular overrides. Helm's value merging picks user-supplied files in order, and later files override earlier ones. A common CI pattern: helm upgrade --values values.yaml --values values-$(ENV).yaml.

Dependencies: Charts can depend on other charts. Declare them in Chart.yaml's dependencies block, then run 'helm dependency update' to download them into charts/. Bitnami charts are commonly used for databases and middleware. The condition field allows enabling/disabling subcharts based on a values flag: condition: redis.enabled.

Library charts: When multiple application charts share helper templates (e.g., labels, ingress helpers, secret generation), extract those into a library chart. Library charts have type: library and are pulled as dependencies. They never create releases; they only export templates. This reduces duplication and ensures consistency.

Subchart scope: Subcharts have their own scope. If your main chart depends on a Redis chart, you configure it under .Values.redis in the parent values. The subchart receives only its own subtree. To share values across subcharts, use global values under the global key in the parent values.

values-prod.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# TheCodeForge - Production values override
environment: production
replicaCount: 5
service:
  port: 443
  type: ClusterIP
resources:
  requests:
    memory: "512Mi"
    cpu: "500m"
  limits:
    memory: "1Gi"
    cpu: "1"
ingress:
  enabled: true
  host: app.example.com
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
redis:
  enabled: true
  architecture: replication
  auth:
    enabled: true
    password: ""  # Should be provided via external secrets or --set
postgresql:
  enabled: false

global:
  imageRegistry: "my-registry.internal:5000"
Audit Tip
Store values files in the same Git repository as your chart, per environment. Tag each release's values commit so you can replay an exact upgrade. Use 'helm get values RELEASE' to compare what's running vs what's in Git.
Production Insight
Subchart values can override parent values accidentally if keys overlap.
For example, if the Redis subchart uses .Values.image.tag, and your parent values file also has an image.tag, the subchart's default applies — not the parent's.
Rule: namespace subchart values under the subchart's name in the parent's values.yaml to avoid collisions.
Key Takeaway
Separate values per environment.
Use dependencies for databases and middleware.
Library charts eliminate helper duplication.
Be careful with key collisions between parent and subchart values.
When to use library charts vs copying helpers
IfYou have three or more application charts that share label templates, ingress definitions, or secret helpers
UseCreate a library chart. Version it and pull it as a dependency.
IfOnly one chart needs the helpers, or the helpers are tightly coupled to that chart's domain
UseKeep helpers in a '_helpers.tpl' inside the chart's templates directory.
IfYou need to enforce company-wide standards for labels, annotations, or naming conventions
UseLibrary chart is mandatory. It becomes the single source of truth for those conventions.
● Production incidentPOST-MORTEMseverity: high

The Case of the Vanishing ConfigMap: How a Helm Upgrade Deleted Production Credentials

Symptom
After a routine helm upgrade, the application started failing with 403 errors from the database. The ConfigMap containing database credentials was missing from the namespace.
Assumption
The team assumed helm upgrade only applies changes to resources defined in the chart. They thought resources not mentioned in the new chart version would remain untouched.
Root cause
Helm v3 treats the release as the sole owner of all resources created during install or upgrade. If a resource is removed from the chart, a helm upgrade with --reuse-values will delete it because Helm reconciles the release state to match the chart’s templates. The ConfigMap was present in v1.0 but absent in v1.1 — upgrade deleted it.
Fix
Restored the ConfigMap from a backup and pinned the release to v1.0. Changed the CI/CD pipeline to always run 'helm diff upgrade' before applying changes. Added a pre-upgrade hook that checks for missing resources and warns the operator.
Key lesson
  • Helm's resource ownership model assumes you define every resource you want to exist — omitting one is equivalent to deleting it.
  • Never use --reuse-values without also running a diff first. Use 'helm diff upgrade --detailed-exitcode' to catch removals.
  • Use Helm hooks or admission controllers to enforce protection labels on critical resources (e.g. database credentials).
  • Store Helm values and chart versions in Git and review the diff between releases before any upgrade.
Production debug guideSymptom → Action steps for the most common Helm issues at scale4 entries
Symptom · 01
helm upgrade fails with 'rendered manifests contain a resource that already exists'
Fix
Run 'helm get manifest RELEASE' and compare with the existing resources. The conflict usually means the chart changed the name or kind of a resource. Use '--force' only after verifying the change is intentional.
Symptom · 02
New template variables from values.yaml are not being substituted in the pod
Fix
Render the templates locally with 'helm template RELEASE CHART --values values-prod.yaml'. Check if the variable was misspelled or if the template uses .Values. in the correct scope (e.g., .Values.nested.key, not .Values.nested_key).
Symptom · 03
Helm install hangs with no error output
Fix
Add '--debug' flag. Check cluster events: 'kubectl get events --namespace NAMESPACE'. Common cause: a hook job never completes because it's stuck on a service dependency (e.g., waiting for a database that doesn't exist).
Symptom · 04
Rollback fails with 'release: not found' even though the release exists
Fix
Release secrets may have been deleted by a cluster admin or a backup restore. Use 'helm list --all-namespaces' to confirm. If the secret is missing, you must reinstall the chart or recreate the release secret from backups.
★ Helm Quick Debug Cheat SheetFive common Helm problems and the exact commands to fix them — no theory, just actionable steps.
Templates not rendering with expected values
Immediate action
Render locally to see the raw YAML
Commands
helm template my-release ./mychart --values values-prod.yaml --debug
helm get manifest my-release --namespace prod
Fix now
Check for typos in .Values paths and ensure the correct values file is loaded
Upgrade accidentally deletes resources+
Immediate action
Prevent the upgrade and inspect the diff
Commands
helm diff upgrade my-release ./mychart --values values-prod.yaml
helm get manifest my-release --revision PREVIOUS_REVISION
Fix now
Add resource protection annotations or use a pre-upgrade hook to validate deletions
Release status stuck in 'pending-install' or 'pending-upgrade'+
Immediate action
Force rollback to a stable revision
Commands
helm rollback my-release PREVIOUS_REVISION --force
helm history my-release
Fix now
If rollback fails, delete the pending release with 'helm delete --purge my-release' and reinstall
Dependencies not downloaded+
Immediate action
Update dependencies and rebuild chart
Commands
helm dependency update ./mychart
helm dependency build ./mychart
Fix now
Ensure Chart.yaml has a valid 'dependencies' block and the repositories are reachable
Helm hook job fails to complete+
Immediate action
Check the hook pod logs and events
Commands
kubectl logs -n PROD job/post-upgrade-job --tail=100
kubectl describe -n PROD job/post-upgrade-job
Fix now
Set a backoff limit on the hook job and ensure it handles errors gracefully
Helm Hooks vs Subcharts vs Library Charts
ConcernHookSubchartLibrary Chart
Run a job before/after installYes — pre/post hooksNo — subcharts are resources, not logicNo — only provides templates, not execution
Share common labels across chartsNo — hooks are for side effectsNo — subcharts are independent deploymentsYes — export template definitions like 'mycompany.labels'
Deploy Redis alongside your appNo — hooks are one-off jobsYes — define redis as a dependencyNo — library charts don't create resources
Validate configuration before installYes — pre-install hook with validation scriptNo — subcharts don't validate parent valuesNo — but you can use 'required' in templates

Key takeaways

1
Helm packages Kubernetes manifests into versioned Charts with a templating layer.
2
Release state is stored in-cluster via Secrets, enabling atomic install/upgrade/rollback.
3
Templates use Go templating with Sprig; values files follow a strict override order.
4
Hooks allow pre/post lifecycle actions but require careful cleanup policies.
5
Production patterns
separate environment values, use library charts for shared helpers, and always diff before upgrade.
6
Never use --reuse-values in automated pipelines; always supply explicit values files.

Common mistakes to avoid

3 patterns
×

Using --reuse-values without understanding its limitations

Symptom
After upgrading to a new chart version, newly added values.yaml keys are ignored. The application crashes because it expects a configuration that was never passed.
Fix
Avoid --reuse-values in automated deployments. Instead, always supply the complete set of values via --values files or a single --values from a Git repository. Use 'helm get values RELEASE' to inspect current values before upgrade.
×

Not setting hook-delete-policy on hook templates

Symptom
Post-upgrade hook jobs accumulate indefinitely. Namespace becomes cluttered with completed jobs, and 'kubectl get jobs' returns dozens of entries that confuse both operators and monitoring tools.
Fix
Always add the annotation '"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded' to your hook Jobs. This ensures old hook resources are cleaned before each release.
×

Putting logic-heavy templates instead of pushing logic to values.yaml

Symptom
Templates become unreadable with complex nested if/else blocks, making it hard to debug or modify. A simple config change requires understanding Go template syntax.
Fix
Refactor: expose decision points as values.yaml keys. For example, instead of checking .Values.ingress.enabled inside a template, use the built-in 'enabled' condition on the Ingress template itself (if not .Values.ingress.enabled, it's not rendered). Keep templates declarative.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Helm v3 store release state, and why does this matter for rollb...
Q02SENIOR
Explain the three-way strategic merge patch used by 'helm upgrade'. How ...
Q03SENIOR
What is a library chart, and when would you use one instead of copying t...
Q04SENIOR
How would you handle cross-cutting configuration like image registry or ...
Q01 of 04SENIOR

How does Helm v3 store release state, and why does this matter for rollback safety?

ANSWER
Helm v3 stores release information in Secrets within the target namespace. Each release revision is stored as a Secret named 'sh.helm.release.v1.<RELEASE-NAME>.v<REVISION>'. These Secrets contain the full rendered manifests, the values used, and metadata. This means rollback does not rely on external state — it simply retrieves the manifests from a previous revision and re-applies them. It matters because rollbacks are atomic and do not require a separate state store, but it also means accidental deletion of these Secrets makes the release unrecoverable. Always back up release Secrets if you plan to restore namespaces.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between Helm v2 and v3 regarding release storage?
02
How can I prevent Helm upgrade from deleting resources that exist in the cluster but are not in the chart anymore?
03
Can I use Helm to manage CRDs and custom resources?
04
How do I structure values files for multiple environments without duplication?
🔥

That's Kubernetes. Mark it forged?

9 min read · try the examples if you haven't

Previous
Kubernetes HPA — Autoscaling
7 / 12 · Kubernetes
Next
kubectl Commands Cheatsheet