Sheet ⁨06⁩ · ⁨DevTips⁩Surveyed ⁨2026⁩

Blog post image for Helm Charts: Templating & Multi-Environment Kubernetes Deployments - How Helm templates Kubernetes manifests for multi-environment deployments: values overrides per environment, conditional logic, chart dependencies, and GitOps rollout with ArgoCD.

Helm Charts: Templating & Multi-Environment Kubernetes Deployments

Published: 04 Mins read04 Mins listen
Markdown for AI(opens in a new tab)

Why Helm matters

The Kubernetes manifest problem

Managing Kubernetes manifests at scale becomes a nightmare.

You have a deployment for dev, staging and production. Each one is 90% identical: same containers, same services, different replicas and resource limits and environment variables. Copy the YAML, edit a few lines, deploy. That works right up until someone edits dev and forgets staging, or a typo lands in production and nobody notices until users report the outage.

Helm packages a Kubernetes application as a chart with templates, values and a version number. The YAML you deploy is generated from one source instead of maintained in three places.

Where hand-written YAML breaks down

Environment Fragmentation:
prod-deployment.yaml ← Different values hardcoded
staging-deployment.yaml ← Needs 3 edits for prod
dev-deployment.yaml ← Inconsistent structure
Result:
• Configurations drift
• Changes in one place aren't replicated
• Rollback = manually revert files
• New environments = copy-paste hell

The fix: Helm charts

What Helm is

Helm is package management for Kubernetes, like npm for Node.js or pip for Python.

  • Charts: Helm packages containing templated Kubernetes manifests
  • Values: Configuration that gets injected into templates (replicas, image tags, resources)
  • Releases: Deployed instances of charts, tracked with versions for easy rollback
  • Repos: Central repositories where teams share charts

What you get

  • Single chart, multiple environments: Use template variables instead of duplicating YAML
  • Templating system: {{ .Values.replicas }} → substituted with values from env-specific file
  • Dependency management: Charts can depend on other charts (PostgreSQL, Redis, etc.)
  • Versioning & rollback: Every deployment tracked, instant rollback to previous version
  • Validation: Helm validates charts before deployment
  • GitOps ready: Store charts in Git, deploy from Git

Chart structure

Directory layout

my-app/
├── Chart.yaml # Chart metadata (name, version, description)
├── values.yaml # Default values
├── values-dev.yaml # Dev environment overrides
├── values-staging.yaml # Staging overrides
├── values-prod.yaml # Production overrides
├── templates/
│ ├── deployment.yaml # Templated Kubernetes Deployment
│ ├── service.yaml # Service manifest
│ ├── configmap.yaml # ConfigMap for config
│ ├── hpa.yaml # Horizontal Pod Autoscaler (prod only)
│ └── _helpers.tpl # Template helpers/functions
└── README.md # Chart documentation

Chart.yaml

Chart.yaml
apiVersion: v2
name: my-app
description: A Helm chart for my microservice
type: application
version: 1.0.0 # Chart version
appVersion: '1.2.3' # Application version
maintainers:
- name: Your Team

Templating

Basic values templating

values.yaml
# Default values for all environments
replicaCount: 1
image:
repository: myregistry.azurecr.io/my-app
tag: '1.2.3'
pullPolicy: IfNotPresent
resources:
requests:
memory: '128Mi'
cpu: '100m'
limits:
memory: '256Mi'
cpu: '500m'
environment: 'dev'
debug: true
values-prod.yaml
# Production overrides
replicaCount: 3 # More replicas for load
resources:
requests:
memory: '512Mi'
cpu: '500m'
limits:
memory: '1Gi'
cpu: '2000m'
environment: 'production'
debug: false # Disable debug logging

A templated deployment

templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{.Release.Name}}
namespace: {{.Release.Namespace}}
spec:
replicas: {{.Values.replicaCount}} # Injected from values
selector:
matchLabels:
app: {{.Chart.Name}}
template:
metadata:
labels:
app: {{.Chart.Name}}
spec:
containers:
- name: {{.Chart.Name}}
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
imagePullPolicy: {{.Values.image.pullPolicy}}
env:
- name: ENVIRONMENT
value: {{.Values.environment}}
- name: DEBUG
value: '{{ .Values.debug }}'
ports:
- containerPort: 8080
resources:
requests:
memory: {{.Values.resources.requests.memory | quote}}
cpu: {{.Values.resources.requests.cpu | quote}}
limits:
memory: {{.Values.resources.limits.memory | quote}}
cpu: {{.Values.resources.limits.cpu | quote}}

Conditional logic in templates

Environment-specific configuration

templates/deployment.yaml
spec:
{{- if eq .Values.environment "production" }}
replicas: 3
{{- else if eq .Values.environment "staging" }}
replicas: 2
{{- else }}
replicas: 1
{{- end }}

Resources that exist only in some environments

templates/hpa.yaml
{{- if eq .Values.environment "production" }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ .Release.Name }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ .Release.Name }}
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
{{- end }}

Conditional security settings

templates/deployment.yaml
spec:
{{- if eq .Values.environment "production" }}
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsReadOnlyRootFilesystem: true
{{- end }}

Managing multiple environments

Environment-specific values files

Deploy to dev:
helm install my-app . -f values.yaml -f values-dev.yaml
Deploy to staging:
helm install my-app . -f values.yaml -f values-staging.yaml
Deploy to prod:
helm install my-app . -f values.yaml -f values-prod.yaml

Values priority (the last one wins)

Terminal window
# Later files override earlier ones
helm install my-app \
-f values.yaml \ # Base defaults
-f values-prod.yaml \ # Override for prod
--set environment=production # Override from CLI (highest priority)

Chart dependencies

Depending on PostgreSQL

Chart.yaml
dependencies:
- name: postgresql
version: '13.0.0'
repository: https://charts.bitnami.com/bitnami

Installing dependencies

Terminal window
# Download dependencies
helm dependency update
# Then deploy (PostgreSQL chart auto-installs)
helm install my-app . -f values-prod.yaml

Values for a sub-chart

values-prod.yaml
# My app values
replicaCount: 3
# PostgreSQL sub-chart values
postgresql:
enabled: true
auth:
password: 'prod-secure-password'
primary:
persistence:
size: 100Gi
metrics:
enabled: true

Release management

The basic commands

Terminal window
# Install a release
helm install my-app ./chart -f values-prod.yaml
# Upgrade to new version
helm upgrade my-app ./chart -f values-prod.yaml
# Check release history
helm history my-app
# Rollback to previous version (instant!)
helm rollback my-app 2
# Delete release
helm uninstall my-app

A deployment run, start to finish

Terminal window
# 1. Make changes to chart
# 2. Test locally
helm install test-release . -f values-dev.yaml
# 3. Test succeeded, upgrade
helm uninstall test-release
# 4. Deploy to production
helm upgrade my-app . -f values-prod.yaml
# 5. Verify
kubectl get pods -l app=my-app
# 6. If something's wrong, instant rollback
helm rollback my-app

GitOps integration

Storing charts in Git

git repository structure:
├── charts/
│ ├── my-app/
│ │ ├── Chart.yaml
│ │ ├── values.yaml
│ │ ├── values-dev.yaml
│ │ ├── values-prod.yaml
│ │ └── templates/
│ └── other-app/
├── .gitignore
└── README.md

GitOps with ArgoCD

ArgoCD watches your Git repo and automatically keeps your Kubernetes cluster in sync:

argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-prod
spec:
project: default
source:
repoURL: https://github.com/myteam/infrastructure
targetRevision: main
path: charts/my-app
helm:
values: |
environment: production
replicaCount: 3
destination:
server: https://kubernetes.default.svc
namespace: prod
syncPolicy:
automated:
prune: true
selfHeal: true

Deploy workflow:

1. Developer updates Chart in Git
2. Commits and pushes
3. ArgoCD detects change
4. Automatically syncs to Kubernetes
5. Helm applies new deployment
6. Services updated with zero downtime

Going further with templates

Helper functions

templates/_helpers.tpl
{{- define "my-app.labels" -}}
helm.sh/chart: {{ include "my-app.chart" . }}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{- define "my-app.name" -}}
{{ .Chart.Name }}
{{- end }}
templates/deployment.yaml
# Reuse helper
metadata:
labels: {{- include "my-app.labels" . | nindent 4}}

Loops

templates/deployment.yaml
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}

Best practices

1. Version charts semantically

version: 2.1.0 # MAJOR.MINOR.PATCH
# MAJOR: Breaking changes
# MINOR: New features
# PATCH: Bug fixes

2. Use namespaces

Terminal window
# Deploy each environment to different namespace
helm install app . -n production -f values-prod.yaml
helm install app . -n staging -f values-staging.yaml
helm install app . -n dev -f values-dev.yaml

3. Lint before you deploy

Terminal window
# Validate chart syntax
helm lint ./chart
# Dry-run to see what will be deployed
helm install --dry-run my-app ./chart -f values-prod.yaml

4. Test charts

Terminal window
helm test my-app # Run chart tests

5. Validate values against a schema

values.schema.json
{
'$schema': 'https://json-schema.org/draft-07/schema#',
'type': 'object',
'properties':
{
'replicaCount': {'type': 'integer', 'minimum': 1},
'image': {'type': 'object', 'properties': {'tag': {'type': 'string'}}},
},
}

A complete minimal example

Chart structure

my-service/
├── Chart.yaml
├── values.yaml
├── values-prod.yaml
└── templates/
├── deployment.yaml
└── service.yaml

Deploying it

Terminal window
# Dev
helm install my-service . -f values.yaml -f values-dev.yaml
# Production
helm upgrade my-service . -f values.yaml -f values-prod.yaml --install
# Rollback if needed
helm rollback my-service

Wrapping up

Helm replaces four copies of a deployment with one chart and four values files.

Templated manifests, values per environment and numbered releases mean a rollback is one command instead of an archaeology session in Git history. Pair it with ArgoCD and the cluster follows the repo. The templating language is the price of admission, and it is a real cost, but it buys you environments that actually match.

Resources

Was this useful?

You might also enjoy

More posts on similar topics

ArgoCD GitOps: Sync Kubernetes Deployments Automatically from Git

ArgoCD GitOps: Sync Kubernetes Deployments Automatically from Git

Why GitOps for Kubernetes? From kubectl apply to Git as the source of truth Hey, want to stop deploying to Kubernetes by hand? If your releases still come from someone running `kubectl ap

Policy-as-Code Governance with OPA/Rego

Policy-as-Code Governance with OPA/Rego

Why policy-as-code matters The governance problem Managing infrastructure at scale gets complicated fast. As your infrastructure grows, keeping it consistent and compliant gets harder. M

Structured Logging & Log Aggregation with ELK Stack

Structured Logging & Log Aggregation with ELK Stack

Why centralized logging matters When services fail, where do you look first? In a distributed system, logs scatter across servers, containers and regions. One request might touch five service

Kubernetes Namespaces: Organize, Isolate, and Secure Multi-Team Clusters

Kubernetes Namespaces: Organize, Isolate, and Secure Multi-Team Clusters

Why cluster isolation matters The multi-tenant reality If you're running a separate cluster for every environment and every dev team, you have already seen the bill and the amount of upgrade

Understanding Kubernetes Services: ClusterIP vs NodePort vs LoadBalancer

Understanding Kubernetes Services: ClusterIP vs NodePort vs LoadBalancer

If you're working with Kubernetes, you've probably noticed that Pods come and go, and their IP addresses keep changing. That's where Services come in. They give you a stable way to keep your apps acce

Tracing Microservices with OpenTelemetry

Tracing Microservices with OpenTelemetry

Why monitor your microservices? The complexity of distributed systems If you're juggling multiple services, it's hard to track how they work together. OpenTelemetry lets you follow one reques

6 related posts