Mid-level 12 min · March 06, 2026
Helm Charts for Kubernetes

Helm Upgrade Deleted Production ConfigMap — Ownership

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

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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.
✦ Definition~90s read
What is Helm Charts for Kubernetes?

Helm is the package manager for Kubernetes, solving the fundamental problem of managing complex, repetitive YAML configurations across multiple environments. Without Helm, you're hand-editing dozens of YAML files per service, per environment, with no versioning, no rollback, and no way to parameterize differences between staging and production.

Imagine you're moving into a new apartment.

Helm Charts are the packaging format — a directory of templated Kubernetes resource files combined with a values.yaml file that injects environment-specific configuration at install or upgrade time. This is not optional for any serious Kubernetes deployment; the alternative is either fragile shell scripts or manual kubectl apply commands that inevitably drift and break.

A Chart's anatomy is strict: Chart.yaml (metadata), values.yaml (default configuration), templates/ (Go-templated YAML), and optionally charts/ (subchart dependencies). The templating engine uses Go templates with Sprig functions, merging your values.yaml with any --set or --values overrides at release time.

When you run helm upgrade, it diffs the current release's manifest against the new one, then applies only the changes — but here's the trap: if you delete a key from values.yaml that was previously present, Helm treats that as an intentional removal and will delete the corresponding Kubernetes resource on upgrade. That's how production ConfigMaps vanish.

The release lifecycle is where most teams get burned. helm install creates a release with a revision number. helm upgrade creates a new revision and applies the diff. helm rollback reverts to a previous revision. helm delete removes the release but by default leaves the history — you can recover with helm rollback if you haven't cleaned it. The critical insight: Helm tracks every resource it created, and any resource not present in the new chart version is deleted during upgrade.

This includes ConfigMaps, Secrets, even Namespaces if you're reckless. The ownership model is all-or-nothing: Helm owns everything in the release, and you cannot selectively protect individual resources without using annotations or moving them outside the chart.

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.

Why Helm Charts Are Not Optional for Kubernetes

Helm is a package manager for Kubernetes that bundles related resources into a single deployable unit called a chart. A chart is a collection of YAML templates and a values file that parameterizes those templates. The core mechanic is that Helm renders templates at install or upgrade time, injecting values from the values file or command-line overrides, then applies the resulting manifests to the cluster. This turns static YAML into a configurable, reusable artifact.

Helm tracks releases — each install or upgrade creates a new revision stored as a Secret in the cluster. This enables rollback to any previous revision. Charts can depend on other charts, forming a dependency tree. In practice, this means you can deploy a full application stack (e.g., a web app, database, and ingress) with one command, and upgrade or rollback each component independently.

Use Helm when you need to manage multiple Kubernetes environments (dev, staging, prod) with different configurations, or when you need to distribute a complex application to other teams. Without Helm, you either copy-paste YAML (error-prone) or write custom scripts (fragile). Helm provides a standard, versioned, and auditable way to manage Kubernetes resources at scale.

Helm Does Not Manage Resource Lifecycle
Helm only creates, updates, or deletes resources based on the chart — it does not monitor or reconcile state. If someone manually deletes a ConfigMap, Helm won't recreate it until the next upgrade.
Production Insight
Teams that manually patch ConfigMaps outside Helm find those changes silently overwritten on the next helm upgrade.
The symptom is a sudden config change in production after a routine upgrade, with no trace in the chart diff.
Rule: never edit Helm-managed resources directly — always change the values file or chart and run helm upgrade.
Key Takeaway
Helm charts are the standard way to package, version, and deploy Kubernetes applications.
Always use helm diff (or a plugin) to preview changes before applying an upgrade.
Never manually edit resources created by Helm — your changes will be lost on the next upgrade.
Helm Upgrade Deleted ConfigMap — Ownership THECODEFORGE.IO Helm Upgrade Deleted ConfigMap — Ownership Flow from chart structure to release lifecycle and hooks Helm Chart Directory Structure Chart.yaml, values.yaml, templates/ folder Go Template Syntax {{ .Values.key }} and pipeline functions Templating Engine Internals Values merge and template rendering Release Lifecycle Install, upgrade, rollback, delete Helm Hooks Pre- and post-operations for resources ConfigMap Ownership Helm manages resources it created ⚠ Upgrade deletes ConfigMap not in chart Always include all managed resources in templates THECODEFORGE.IO
thecodeforge.io
Helm Upgrade Deleted ConfigMap — Ownership
Helm Charts Kubernetes

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.
Helm Chart Directory Tree
mychart/Chart.yamlvalues.yamltemplates/_helpers.tpldeployment.yamlservice.yamlconfigmap.yamltests/test-connection.yamlcharts/redis-17.x.tgzcrds/crd-myresource.yamlREADME.md.helmignore

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.
Helm Hook Lifecycle Flow
YesNoYesNoInstall/Upgrade/Rollback/DeletePre-hooksRun pre-hook jobs in weightorderAll succeeded?Apply manifestsFail releasePost-hooksRun post-hook jobsAll succeeded?Release successfulFail release

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.

Why Helm Charts Are Not Optional: The Real Reason Your Team Needs a Package Manager

Stop treating Helm like a nice-to-have. In production, raw Kubernetes manifests are a liability. You’ll end up with copy-paste drift across environments, inconsistent labels, and a rollback process that requires a PhD in your own YAML. Helm solves this by giving you a single source of truth: a chart. With helm rollback, you revert your entire application state, not just one Deployment. Without it, you’re manually tracking five different YAML files and praying kubectl apply doesn’t break something. The cost of not using Helm is technical debt you pay every deploy. If you’re deploying to Kubernetes without charts, you’re shipping an unversioned artifact. That’s a risk no senior engineer should accept.

rollback.shBASH
1
2
3
4
5
6
7
# Without Helm: manual, error-prone, multi-file restore
kubectl apply -f deployment.yaml --record && kubectl rollout undo deployment/my-app

# With Helm: atomic, versioned, auditable
helm upgrade --install my-app ./my-chart --namespace prod
helm history my-app -n prod
helm rollback my-app 2 -n prod
Output
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Mon Jan 13 10:00:00 2025 superseded my-chart-1.0.0 1.0 Install complete
2 Mon Jan 13 11:00:00 2025 deployed my-chart-1.0.1 1.1 Upgrade complete
Rollback was a success! Happy Helming!
Production Trap:
Don’t use kubectl apply for production rollbacks. It only reverts one resource. Helm rollbacks are atomic—they restore the entire chart state. Always test rollbacks in staging before you need them in production.
Key Takeaway
If you’re managing multiple YAMLs manually, you’re not doing Kubernetes—you’re just writing documentation.

Chart Structure and Anatomy: Tear Down the Boilerplate to Ship Faster

A Helm chart is more than a folder of YAML files. It’s a blueprint. The structure is strict for a reason: it enforces consistency. The Chart.yaml is your chart ID—name, version, dependencies. The values.yaml is your contract with the user. Every variable that could change between environments goes here. Templates live in templates/. They’re not static YAML; they’re Go templates that render with release.Name, .Values.replicaCount, and conditionals. The charts/ and templates/NOTES.txt are optional but powerful—the former for subcharts, the latter for post-install instructions. Master this structure, and you can onboard a new microservice in ten minutes. The secret? Keep values.yaml flat for readability and use _helpers.tpl for shared logic to avoid template bloat.

mychart/templates/deployment.yamlBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "mychart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "mychart.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: {{ .Values.service.port }}
Output
# Rendered output (helm template .):
apiVersion: apps/v1
kind: Deployment
metadata:
name: mychart-prod
labels:
app.kubernetes.io/name: mychart
app.kubernetes.io/instance: prod
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: mychart
app.kubernetes.io/instance: prod
template:
metadata:
labels:
app.kubernetes.io/name: mychart
app.kubernetes.io/instance: prod
spec:
containers:
- name: mychart
image: nginx:1.25
ports:
- containerPort: 80
Production Habit:
Always run helm lint before shipping a chart. It catches YAML syntax errors and missing fields. Combine that with --dry-run to diff your changes against the live cluster before any upgrade.
Key Takeaway
A well-structured chart is a deployable contract—not a pile of YAML.
● 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?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Kubernetes. Mark it forged?

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

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