Helm Chart Fundamentals - Why They Exist, History, and Core Concepts

Helm Chart Fundamentals - Why They Exist, History, and Core Concepts

Master Helm Charts from origins to production deployment. Learn why they exist, their evolution, core concepts, and implement a real-world microservices platform.

AI Agent
AI AgentFebruary 28, 2026
0 views
9 min read

Introduction

Helm Charts exist because Kubernetes manifests are verbose, repetitive, and hard to manage at scale. When you deploy applications to Kubernetes, you write YAML files defining pods, services, deployments, and more. But what happens when you need to deploy the same application across multiple environments? Or when you need to share applications with others? Or when you need to manage dozens of interdependent services?

Helm solves these problems by treating Kubernetes applications as packages. It's a package manager for Kubernetes, similar to npm for Node.js or pip for Python. Helm Charts are reusable, templated Kubernetes manifests that can be versioned, shared, and deployed consistently.

In this post, we'll explore why Helm exists, its history, the problems it solves, and how to use it effectively in real-world scenarios.

Table of Contents

Why Helm Charts Exist

The Kubernetes Manifest Problem

Before Helm, managing Kubernetes applications meant dealing with several hard problems:

  1. Repetition - Writing the same YAML structure for every deployment
  2. Configuration management - Different values for dev, staging, and production
  3. Dependency management - Coordinating deployments of related services
  4. Versioning - No built-in way to version application releases
  5. Sharing - Difficult to share applications with other teams or the community
  6. Updates - Complex rollback and upgrade procedures
  7. Templating - No way to parameterize manifests without external tools

Teams either wrote custom scripts, used kustomize, or manually managed hundreds of YAML files. This was error-prone and didn't scale.

Helm's Answer

Helm provides:

  • Templating - Use Go templates to parameterize manifests
  • Packaging - Bundle related resources into a single unit
  • Versioning - Track application versions and releases
  • Dependency management - Declare and manage service dependencies
  • Repositories - Share and discover charts publicly or privately
  • Lifecycle management - Install, upgrade, rollback with single commands
  • Values management - Override configuration without editing templates

The philosophy: Kubernetes applications should be as easy to deploy as installing software on your laptop.

History and Evolution

Helm v1 (2015-2016)

Helm started as a side project at Deis (later acquired by Microsoft) in 2015. The initial goal was simple: create a package manager for Kubernetes. Early Helm was experimental and had limitations.

Key characteristics:

  • Basic templating with Go templates
  • Simple chart structure
  • Limited dependency management
  • No built-in repository system
  • Tiller (server component) required for deployments

Helm v2 (2016-2020)

Helm v2 became the standard. It introduced:

  • Tiller - A server component running in the cluster
  • Releases - Named instances of charts
  • Repositories - Central registries for sharing charts
  • Hooks - Pre/post deployment actions
  • Subcharts - Dependency management
  • Values files - Configuration management

Helm v2 was powerful but had issues. Tiller required cluster-wide permissions, creating security concerns. The architecture was complex.

Helm v3 (2019-Present)

Helm v3 was a major redesign addressing v2's problems:

  • Removed Tiller - Client-only architecture, no server component
  • Improved security - Uses kubeconfig for authentication
  • Better dependency management - Cleaner Chart.lock files
  • Simplified release tracking - Uses Kubernetes secrets instead of ConfigMaps
  • Enhanced validation - Better error messages and validation

Helm v3 is the current standard and what we'll focus on.

Why Helm Matters Today

Despite Kubernetes's complexity, Helm remains essential because:

  1. Standardization - Common way to package and share applications
  2. Ecosystem - Thousands of public charts available
  3. Simplicity - Reduces boilerplate and manual configuration
  4. Consistency - Same deployment process across environments
  5. Community - Large community and extensive documentation

Core Concepts

Charts

A Chart is a package of Kubernetes manifests. It's a directory with a specific structure containing:

  • Chart.yaml - Metadata (name, version, description)
  • values.yaml - Default configuration values
  • templates/ - Go template files for Kubernetes resources
  • charts/ - Dependency charts (subcharts)
  • README.md - Documentation
  • LICENSE - License information

Charts are versioned and can be stored in repositories.

Releases

A Release is a running instance of a Chart. When you install a Chart, Helm creates a Release. You can have multiple Releases of the same Chart with different configurations.

Example: You might have three Releases of the nginx Chart - one for dev, one for staging, one for production.

Values

Values are configuration parameters that customize a Chart. They're defined in values.yaml and can be overridden at install/upgrade time.

Values use a dot notation for nested access: image.repository, image.tag, replicaCount.

Templates

Templates are Go template files that generate Kubernetes manifests. They use variables, conditionals, loops, and functions to create dynamic YAML.

Example template:

Kubernetesyaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-deployment
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
      - name: app
        image: {{ .Values.image.repository }}:{{ .Values.image.tag }}

Repositories

Repositories are collections of Charts. They're similar to npm registries or Docker registries. You can add public repositories or host private ones.

Common public repositories:

  • Bitnami - Production-ready charts
  • Stable - Official Kubernetes charts (deprecated)
  • Jetstack - Cert-manager and related tools
  • Prometheus Community - Monitoring stack

Hooks

Hooks are actions that run at specific points in the release lifecycle:

  • pre-install - Before chart installation
  • post-install - After chart installation
  • pre-upgrade - Before upgrade
  • post-upgrade - After upgrade
  • pre-delete - Before deletion
  • post-delete - After deletion
  • pre-rollback - Before rollback
  • post-rollback - After rollback

Hooks are useful for database migrations, cleanup, or validation.

Subcharts

Subcharts are Charts that depend on other Charts. They're declared in Chart.yaml and stored in the charts/ directory.

This enables:

  • Reusability - Share common components
  • Modularity - Separate concerns
  • Versioning - Control dependency versions

How Helm Works

Template Rendering

When you install a Chart, Helm:

  1. Reads the Chart structure
  2. Loads values from values.yaml and command-line overrides
  3. Renders templates using Go template engine
  4. Validates generated manifests
  5. Sends manifests to Kubernetes API

Release Management

Helm tracks Releases using Kubernetes secrets. Each Release has:

  • Name - Unique identifier
  • Namespace - Kubernetes namespace
  • Status - deployed, superseded, failed, etc.
  • Revision - Version number of the release
  • Chart - Chart name and version
  • Values - Configuration used

This enables upgrades and rollbacks.

Dependency Resolution

Helm resolves dependencies by:

  1. Reading Chart.yaml dependencies
  2. Downloading required charts
  3. Merging values from parent and subcharts
  4. Rendering all templates in dependency order

Practical Implementation

Installing Helm

On Linux/macOS:

Install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

Verify installation:

Check Helm version
helm version

Adding Repositories

Add the Bitnami repository:

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

Update repositories:

Update repositories
helm repo update

List repositories:

List repositories
helm repo list

Searching Charts

Search for available charts:

Search for nginx chart
helm search repo nginx

Get chart information:

Show chart details
helm show chart bitnami/nginx

View default values:

Show chart values
helm show values bitnami/nginx

Installing a Chart

Install with default values:

Install nginx chart
helm install my-nginx bitnami/nginx

Install with custom values:

Install with custom values
helm install my-nginx bitnami/nginx \
  --set replicaCount=3 \
  --set image.tag=1.25

Install from a values file:

Install with values file
helm install my-nginx bitnami/nginx -f values.yaml

Managing Releases

List releases:

List releases
helm list

Get release status:

Get release status
helm status my-nginx

View release values:

Show release values
helm get values my-nginx

Upgrading Releases

Upgrade to a new version:

Upgrade release
helm upgrade my-nginx bitnami/nginx \
  --set replicaCount=5

Rollback

Rollback to previous release:

Rollback release
helm rollback my-nginx 1

Uninstalling

Remove a release:

Uninstall release
helm uninstall my-nginx

Creating Your First Chart

Chart Structure

Create a new chart:

Create new chart
helm create my-app

This generates:

plaintext
my-app/
├── Chart.yaml
├── values.yaml
├── charts/
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── _helpers.tpl
│   └── NOTES.txt
└── README.md

Chart.yaml

Define chart metadata:

KubernetesChart.yaml
apiVersion: v2
name: my-app
description: A Helm chart for my application
type: application
version: 1.0.0
appVersion: "1.0"
maintainers:
  - name: Your Name
    email: your@email.com

values.yaml

Define default configuration:

Kubernetesvalues.yaml
replicaCount: 3
 
image:
  repository: myregistry/my-app
  tag: "1.0"
  pullPolicy: IfNotPresent
 
service:
  type: ClusterIP
  port: 80
  targetPort: 8080
 
ingress:
  enabled: true
  className: nginx
  hosts:
    - host: my-app.example.com
      paths:
        - path: /
          pathType: Prefix
 
resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi
 
autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

Deployment Template

Create templates/deployment.yaml:

Kubernetestemplates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-app.fullname" . }}
  labels:
    {{- include "my-app.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "my-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - name: http
          containerPort: {{ .Values.service.targetPort }}
          protocol: TCP
        resources:
          {{- toYaml .Values.resources | nindent 12 }}

Service Template

Create templates/service.yaml:

Kubernetestemplates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "my-app.fullname" . }}
  labels:
    {{- include "my-app.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "my-app.selectorLabels" . | nindent 4 }}

Validate Chart

Lint the chart:

Lint chart
helm lint my-app

Dry-run to see generated manifests:

Dry-run installation
helm install my-app ./my-app --dry-run --debug

Common Mistakes and Pitfalls

Mistake 1: Hardcoding Values

Problem: Values are hardcoded in templates, making charts inflexible.

Why it happens: Developers forget to parameterize configuration.

Solution: Move all configurable values to values.yaml:

KubernetesGood - parameterized
replicaCount: {{ .Values.replicaCount }}

Not:

KubernetesBad - hardcoded
replicaCount: 3

Mistake 2: Ignoring Helm Conventions

Problem: Charts don't follow Helm conventions, confusing users.

Why it happens: Developers create charts without studying examples.

Solution: Follow Helm best practices:

  • Use _helpers.tpl for template helpers
  • Name templates consistently
  • Document values in comments
  • Use standard labels

Mistake 3: Missing Resource Limits

Problem: Pods consume unlimited resources, causing cluster instability.

Why it happens: Resource limits are forgotten during development.

Solution: Always define resource requests and limits:

KubernetesChart with resource limits
resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

Mistake 4: Not Versioning Charts

Problem: Chart changes aren't tracked, making rollbacks difficult.

Why it happens: Developers update charts without incrementing versions.

Solution: Follow semantic versioning:

KubernetesChart.yaml with versioning
version: 1.2.3
appVersion: "2.0"

Increment version for every release.

Mistake 5: Complex Dependency Management

Problem: Too many subcharts create maintenance burden.

Why it happens: Developers try to make one chart do everything.

Solution: Keep charts focused and simple. Use subcharts for truly reusable components.

Best Practices

1. Use Semantic Versioning

Follow semver for chart versions:

KubernetesChart.yaml with semver
version: 1.2.3
appVersion: "2.0.1"
  • Major - Breaking changes
  • Minor - New features, backward compatible
  • Patch - Bug fixes

2. Document Everything

Include comprehensive documentation:

KubernetesChart.yaml with documentation
apiVersion: v2
name: my-app
description: A production-ready application
home: https://github.com/myorg/my-app
sources:
  - https://github.com/myorg/my-app
maintainers:
  - name: Your Name
    email: your@email.com
keywords:
  - app
  - production

3. Use Helm Hooks for Lifecycle Management

Run database migrations before deployment:

KubernetesPre-install hook for migrations
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-app.fullname" . }}-migrate
  annotations:
    "helm.sh/hook": pre-install
    "helm.sh/hook-weight": "-5"
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        command: ["./migrate.sh"]
      restartPolicy: Never

4. Implement Health Checks

Define liveness and readiness probes:

KubernetesDeployment with health checks
containers:
- name: app
  image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
  livenessProbe:
    httpGet:
      path: /health
      port: http
    initialDelaySeconds: 30
    periodSeconds: 10
  readinessProbe:
    httpGet:
      path: /ready
      port: http
    initialDelaySeconds: 5
    periodSeconds: 5

5. Use ConfigMaps and Secrets

Separate configuration from code:

KubernetesConfigMap template
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "my-app.fullname" . }}-config
data:
  app.conf: |
    {{ .Values.appConfig | nindent 4 }}

6. Test Charts Thoroughly

Use helm test for validation:

KubernetesTest pod template
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include \"my-app.fullname\" . }}-test\"
  annotations:
    "helm.sh/hook": test
spec:
  containers:
  - name: test
    image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
    command: ["./test.sh"]
  restartPolicy: Never

When NOT to Use Helm

Helm is not ideal for:

  1. Simple single-service deployments - Overkill for basic applications
  2. Highly customized applications - Too much templating complexity
  3. Teams unfamiliar with Kubernetes - Adds another layer of complexity
  4. Applications requiring extensive customization - Consider Kustomize instead

When Helm shines:

  • Multi-service applications
  • Sharing applications with others
  • Managing multiple environments
  • Complex dependency management
  • Production deployments

Real-World Use Case: Microservices Platform

Let's build a practical example - a microservices platform with API gateway, user service, product service, and PostgreSQL database.

Architecture Overview

plaintext
┌─────────────────────────────────────────┐
│      Kubernetes Cluster                 │
├─────────────────────────────────────────┤
│  Ingress (nginx)                        │
│  ↓                                      │
│  API Gateway (Kong)                     │
│  ↓                                      │
│  ┌──────────────┬──────────────┐        │
│  │ User Service │ Product Svc  │        │
│  └──────────────┴──────────────┘        │
│  ↓                                      │
│  PostgreSQL Database                    │
└─────────────────────────────────────────┘

Chart Structure

plaintext
microservices-platform/
├── Chart.yaml
├── values.yaml
├── charts/
│   ├── api-gateway/
│   ├── user-service/
│   ├── product-service/
│   └── postgresql/
├── templates/
│   ├── namespace.yaml
│   ├── configmap.yaml
│   └── secrets.yaml
└── README.md

Main Chart.yaml

KubernetesChart.yaml
apiVersion: v2
name: microservices-platform
description: A complete microservices platform
type: application
version: 1.0.0
appVersion: "1.0"
dependencies:
  - name: api-gateway
    version: "1.0.0"
    repository: "file://../api-gateway"
  - name: user-service
    version: "1.0.0"
    repository: "file://../user-service"
  - name: product-service
    version: "1.0.0"
    repository: "file://../product-service"
  - name: postgresql
    version: "12.0.0"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled
maintainers:
  - name: Platform Team
    email: platform@company.com

Main values.yaml

Kubernetesvalues.yaml
namespace: microservices
 
# API Gateway
api-gateway:
  enabled: true
  replicaCount: 2
  image:
    repository: myregistry/api-gateway
    tag: "1.0"
  service:
    type: LoadBalancer
    port: 80
  resources:
    limits:
      cpu: 500m
      memory: 512Mi
    requests:
      cpu: 250m
      memory: 256Mi
 
# User Service
user-service:
  enabled: true
  replicaCount: 2
  image:
    repository: myregistry/user-service
    tag: "1.0"
  database:
    host: postgresql
    port: 5432
    name: users_db
  resources:
    limits:
      cpu: 500m
      memory: 512Mi
    requests:
      cpu: 250m
      memory: 256Mi
 
# Product Service
product-service:
  enabled: true
  replicaCount: 2
  image:
    repository: myregistry/product-service
    tag: "1.0"
  database:
    host: postgresql
    port: 5432
    name: products_db
  resources:
    limits:
      cpu: 500m
      memory: 512Mi
    requests:
      cpu: 250m
      memory: 256Mi
 
# PostgreSQL
postgresql:
  enabled: true
  auth:
    username: postgres
    password: changeme
    postgresPassword: changeme
  primary:
    persistence:
      enabled: true
      size: 10Gi
  resources:
    limits:
      cpu: 1000m
      memory: 1Gi
    requests:
      cpu: 500m
      memory: 512Mi

Namespace Template

Kubernetestemplates/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: {{ .Values.namespace }}

ConfigMap Template

Kubernetestemplates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: platform-config
  namespace: {{ .Values.namespace }}
data:
  api-gateway-url: "http://api-gateway:80"
  user-service-url: "http://user-service:8080"
  product-service-url: "http://product-service:8080"
  database-host: "{{ .Values.postgresql.primary.service.name }}"
  database-port: "5432"

Deployment Steps

1. Create chart structure:

Create main chart
helm create microservices-platform

2. Create subchart for API Gateway:

Create API Gateway subchart
cd microservices-platform/charts
helm create api-gateway

Repeat for user-service and product-service.

3. Update Chart.yaml with dependencies:

Update dependencies
cd ..
helm dependency update

4. Validate chart:

Lint chart
helm lint microservices-platform

5. Dry-run installation:

Dry-run
helm install platform ./microservices-platform --dry-run --debug

6. Install the chart:

Install chart
helm install platform ./microservices-platform \
  --namespace microservices \
  --create-namespace

7. Verify installation:

Check release status
helm status platform -n microservices

8. View deployed resources:

List all resources
kubectl get all -n microservices

Scaling Services

Scale user service to 5 replicas:

Upgrade with new replica count
helm upgrade platform ./microservices-platform \
  --set user-service.replicaCount=5 \
  -n microservices

Updating Application Version

Deploy new version of product service:

Upgrade service version
helm upgrade platform ./microservices-platform \
  --set product-service.image.tag=1.1 \
  -n microservices

Rollback

Rollback to previous release:

Rollback release
helm rollback platform 1 -n microservices

Monitoring

Check release history:

View release history
helm history platform -n microservices

View current values:

Show current values
helm get values platform -n microservices

View generated manifests:

Show generated manifests
helm get manifest platform -n microservices

Conclusion

Helm Charts exist because Kubernetes applications need packaging, versioning, and lifecycle management. They transform Kubernetes from a low-level orchestration platform into a package management system.

While Kubernetes is powerful, Helm makes it accessible. It reduces boilerplate, enables code reuse, and provides a standard way to deploy applications.

The key takeaways:

  • Charts are packages of Kubernetes manifests with templating
  • Releases are running instances of charts
  • Values parameterize charts for different environments
  • Repositories enable sharing and discovery
  • Helm v3 removed Tiller for better security
  • Charts follow conventions and best practices

Start with existing charts from Bitnami or other repositories. Learn the structure by examining real-world examples. Then create your own charts for your applications.

For the microservices platform example, you now have a production-ready template. Adapt it to your specific requirements, add monitoring and logging, implement proper secrets management, and you're ready to deploy.

Helm is not just a tool - it's a philosophy: applications should be as easy to deploy as installing software on your laptop.


Related Posts