---
title: "Helm Charts: Templating & Multi-Environment Kubernetes Deployments"
description: "Master Helm Charts for Kubernetes deployments. Learn templating, values overrides, conditional logic, chart dependencies, and GitOps workflows to manage complex microservices across dev, staging, and production environments."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/devtips/post/helm-charts-kubernetes-multi-environment
---

# Helm Charts: Templating & Multi-Environment Kubernetes Deployments

## 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 but with different replicas, resource limits, environment variables. Copy-paste the YAML, make a few edits, and deploy. Works fine until someone edits dev and forgets to update staging. Or production has a typo and nobody notices until users report the outage.

Helm packages Kubernetes applications, providing templating, versioning, and dependency management transforming "paste YAML and hope" into "deploy with confidence."

### The Manual YAML Approach Breaks At Scale

```
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 Solution: Helm Charts

### What is Helm?

**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

### Core Benefits

- **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

```yaml title="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
    email: team@example.com
```

## Templating: The Power of Helm

### Basic Values Templating

```yaml title="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
```

```yaml title="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
```

### Templated Deployment

```yaml title="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 Helm Templates

### Environment-Specific Configuration

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

### Conditional Resources

```yaml title="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 Policies

```yaml title="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 (Right-to-left wins)

```bash
# 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

### Depends on PostgreSQL

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

### Install Dependencies

```bash
# Download dependencies
helm dependency update

# Then deploy (PostgreSQL chart auto-installs)
helm install my-app . -f values-prod.yaml
```

### Values for Dependencies

```yaml title="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
```

## Helm Release Management

### Basic Deployment

```bash
# 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
```

### Deployment Workflow

```bash
# 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

### Store 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 Workflow with ArgoCD

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

```yaml title="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
```

## Advanced Templating

### Helper Functions

```yaml title="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 }}
```

```yaml title="templates/deployment.yaml"
# Reuse helper
metadata:
  labels: {{- include "my-app.labels" . | nindent 4}}
```

### Loops and Iterations

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

## Best Practices

### 1. Semantic Versioning for Charts

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

### 2. Use Namespaces

```bash
# 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. Helm Lint Before Deploy

```bash
# 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

```bash
helm test my-app  # Run chart tests
```

### 5. Values Schema Validation

```yaml title="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'}}},
    },
}
```

## Real-World Example: Complete Deployment

### Minimal Chart Structure

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

### Deploy

```bash
# 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
```

## Conclusion

**Helm transforms Kubernetes deployments from manual tedium to automated simplicity.**

By templating your manifests, managing values per environment, and versioning releases, you eliminate the fragility of manual YAML management. Combined with GitOps tools like ArgoCD, Helm becomes the backbone of reliable, repeatable Kubernetes deployments.

## Resources

- [Helm Official Documentation](https://helm.sh/docs/)
- [Chart Template Guide](https://helm.sh/docs/chart_template_guide/)
- [Helm Best Practices](https://helm.sh/docs/chart_best_practices/)
- [ArtifactHub: Pre-built Charts](https://artifacthub.io/)
- [ArgoCD + Helm Integration](https://argo-cd.readthedocs.io/en/stable/user-guide/helm/)
