StackPractices
intermediate By Mathias Paulenko

Helm Chart Review Checklist

A checklist for reviewing Helm charts covering values, templates, security, resource limits, probes, RBAC, and best practices.

Topics: testing

Overview

A Helm chart review checklist ensures charts are secure, maintainable, and follow Kubernetes best practices. It covers values schema, template correctness, security contexts, resource limits, health probes, RBAC, and chart metadata. Without a checklist, chart reviews are inconsistent and miss common issues.

When to Use

  • For alternatives, see CI/CD Pipeline Design Template.

  • Reviewing a new Helm chart before publishing

  • Auditing existing charts for security or compliance

  • Onboarding third-party charts to a cluster

  • Upgrading Helm charts to new Kubernetes versions

  • Standardizing chart quality across teams

Solution

# Helm Chart Review — `<Chart Name>`

## Review Overview

| Field | Value |
|-------|-------|
| Chart Name | example-app |
| Chart Version | 1.3.0 |
| App Version | 2.1.0 |
| Reviewer | DevOps Team |
| Review Date | 2026-07-05 |
| Kubernetes Version | 1.29+ |
| Helm Version | 3.14+ |

## 1. Chart Structure

| Check | Status | Notes |
|-------|--------|-------|
| Chart.yaml present and valid | ✅ | apiVersion: v2 |
| values.yaml present | ✅ | All defaults documented |
| templates/ directory exists | ✅ | 12 template files |
| _helpers.tpl present | ✅ | Named templates defined |
| .helmignore present | ✅ | Excludes .git, .vscode |
| README.md present | ✅ | Installation and configuration docs |
| values.schema.json present | ✅ | Type validation for all values |
| LICENSE present | ✅ | Apache 2.0 |
| charts/ directory (if dependencies) | ✅ | No subchart dependencies |
| NOTES.txt present | ✅ | Post-install instructions |

### Chart.yaml Validation

| Field | Value | Check |
|-------|-------|-------|
| apiVersion | v2 | ✅ Helm 3 format |
| name | example-app | ✅ Lowercase, no spaces |
| version | 1.3.0 | ✅ Semantic versioning |
| appVersion | 2.1.0 | ✅ Application version |
| description | Example web application | ✅ Descriptive |
| type | application | ✅ Application chart |
| keywords | [web, app, frontend] | ✅ Searchable |
| maintainers | [{name: devops-team, email: devops@example.com}] | ✅ Contact info |
| icon | https://example.com/icon.png | ✅ Chart icon |

## 2. Values Schema

### values.yaml Review

| Check | Status | Notes |
|-------|--------|-------|
| No secrets in values.yaml | ✅ | Secrets via --set or external secret store |
| All values have comments | ✅ | Every key documented |
| Sensible defaults | ✅ | Chart works with no overrides |
| No hardcoded environment values | ✅ | Environment-specific values overridden |
| Image tag uses appVersion | ✅ | `{{ .Chart.AppVersion }}` |
| Replica count defaults to > 1 | ✅ | Default: 3 |
| Resources have defaults | ✅ | requests and limits set |
| Service type is configurable | ✅ | Default: ClusterIP |

### values.schema.json Review

| Check | Status | Notes |
|-------|--------|-------|
| All values have type definitions | ✅ | String, integer, boolean, object |
| Required fields marked | ✅ | image.repository required |
| Enum values for restricted fields | ✅ | serviceType: [ClusterIP, NodePort, LoadBalancer] |
| Minimum/maximum for numeric fields | ✅ | replicas: min 1, max 100 |
| Pattern for string fields | ✅ | image.repository: non-empty string |
| Additional properties disabled | ✅ | additionalProperties: false |

### Values Structure

```yaml
replicaCount: 3

image:
  repository: ghcr.io/example/app
  pullPolicy: IfNotPresent
  tag: ""  # Overrides .Chart.AppVersion

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  create: true
  automount: true
  annotations: {}
  name: ""

podAnnotations: {}
podLabels: {}

podSecurityContext:
  runAsNonRoot: true
  runAsUser: 10001
  runAsGroup: 10001
  fsGroup: 10001
  seccompProfile:
    type: RuntimeDefault

securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop:
      - ALL

service:
  type: ClusterIP
  port: 8080

ingress:
  enabled: false
  className: nginx
  annotations: {}
  hosts:
    - host: chart-example.local
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls: []

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

livenessProbe:
  httpGet:
    path: /health
    port: http
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /ready
    port: http
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 3

autoscaling:
  enabled: false
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80
  targetMemoryUtilizationPercentage: 80

volumes: []
volumeMounts: []

nodeSelector: {}
tolerations: []
affinity: {}

podDisruptionBudget:
  enabled: false
  minAvailable: 1

3. Template Review

Template Best Practices

CheckStatusNotes
No hardcoded namespaces✅Uses .Release.Namespace
Labels follow conventions✅app.kubernetes.io/* labels
Selectors match labels✅Consistent across deployments and services
Named templates for repeated blocks✅_helpers.tpl for labels, names
No trailing whitespace✅{{- }} and {{- -}} used
Include comments for complex logic✅Conditional blocks documented
No template errors on empty values✅Defaults via
Helm hooks used correctly✅post-install, pre-delete hooks

Labels Checklist

LabelPresentValue
app.kubernetes.io/name✅{{ include “chart.name” . }}
app.kubernetes.io/instance✅{{ .Release.Name }}
app.kubernetes.io/version✅{{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by✅{{ .Release.Service }}
app.kubernetes.io/component✅frontend
app.kubernetes.io/part-of✅example-app

Template Files

FileCheckStatus
deployment.yamlCorrect API version (apps/v1)✅
deployment.yamlStrategy configurable✅
deployment.yamlImage pull policy configurable✅
service.yamlCorrect API version (v1)✅
service.yamlPort configurable✅
service.yamlSelector matches deployment labels✅
ingress.yamlCorrect API version (networking.k8s.io/v1)✅
ingress.yamlPath type specified✅
ingress.yamlTLS configurable✅
serviceaccount.yamlCorrect API version (v1)✅
serviceaccount.yamlConditional creation✅
configmap.yamlCorrect API version (v1)✅
configmap.yamlData from values✅
hpa.yamlCorrect API version (autoscaling/v2)✅
hpa.yamlConditional on autoscaling.enabled✅
pdb.yamlCorrect API version (policy/v1)✅
pdb.yamlConditional on podDisruptionBudget.enabled✅
NOTES.txtPost-install instructions✅

4. Security Review

Pod Security Context

CheckStatusNotes
runAsNonRoot: true✅Pod runs as non-root
runAsUser: non-zero UID✅UID 10001
runAsGroup: non-zero GID✅GID 10001
fsGroup: non-zero✅GID 10001
seccompProfile: RuntimeDefault✅Seccomp enabled

Container Security Context

CheckStatusNotes
allowPrivilegeEscalation: false✅No privilege escalation
readOnlyRootFilesystem: true✅Read-only filesystem
capabilities.drop: [ALL]✅All Linux capabilities dropped
runAsNonRoot: true (container-level)✅Redundant with pod-level
privileged: false✅Not running privileged

RBAC

CheckStatusNotes
Service account conditional✅serviceAccount.create flag
RBAC least privilege✅Only required resources
No cluster-admin role✅No cluster-wide permissions
Role binding to service account✅Namespace-scoped
Automount token configurable✅serviceAccount.automount flag

Image Security

CheckStatusNotes
Image digest pinned⚠️Uses tag, not digest
No :latest tag✅Uses appVersion
Pull policy: IfNotPresent✅Default pull policy
Private registry support✅imagePullSecrets configurable
Image scanned in CI✅Trivy scan in pipeline

5. Resource Management

Resource Limits

CheckStatusNotes
CPU request set✅100m
CPU limit set✅500m
Memory request set✅128Mi
Memory limit set✅512Mi
Memory request < limit✅128Mi < 512Mi
CPU request < limit✅100m < 500m
Resources configurable via values✅resources in values.yaml
QoS class: Burstable✅Requests < limits

Horizontal Pod Autoscaler

CheckStatusNotes
HPA conditional✅autoscaling.enabled flag
Min replicas >= 2✅minReplicas: 3
Max replicas reasonable✅maxReplicas: 10
CPU target < 90%✅80%
Memory target < 90%✅80%
HPA API version: autoscaling/v2✅Current API

Pod Disruption Budget

CheckStatusNotes
PDB conditional✅podDisruptionBudget.enabled flag
minAvailable or maxUnavailable set✅minAvailable: 1
PDB API version: policy/v1✅Current API

6. Health Probes

Liveness Probe

CheckStatusNotes
Liveness probe defined✅HTTP GET /health
initialDelaySeconds set✅30 seconds
periodSeconds reasonable✅10 seconds
timeoutSeconds set✅5 seconds
failureThreshold reasonable✅3 failures
Probe configurable via values✅livenessProbe in values

Readiness Probe

CheckStatusNotes
Readiness probe defined✅HTTP GET /ready
initialDelaySeconds set✅5 seconds
periodSeconds reasonable✅5 seconds
timeoutSeconds set✅3 seconds
failureThreshold reasonable✅3 failures
Probe configurable via values✅readinessProbe in values

Startup Probe (if applicable)

CheckStatusNotes
Startup probe defined✅For slow-starting apps
failureThreshold high enough✅30 failures
periodSeconds: 10✅5 min startup window

7. Deployment Strategy

CheckStatusNotes
Strategy configurable✅RollingUpdate default
Max unavailable set✅25%
Max surge set✅25%
Revision history limit✅10
Progress deadline seconds✅600

8. Testing

Helm Tests

CheckStatusNotes
tests/ directory present✅Helm test templates
Test connects to service✅curl to service endpoint
Test verifies health✅HTTP 200 from /health
Test runs via helm test✅Test pod uses helm.sh/hook: test

Linting

CheckStatusNotes
helm lint passes✅No warnings
kubeval passes✅Schema validation
kube-score passes✅Best practices check
yamllint passes✅YAML formatting

## Explanation

A Helm chart review checklist ensures charts are production-ready. The review covers eight areas: structure, values, templates, security, resources, probes, deployment strategy, and testing. Each area has specific checks that catch common issues before they reach production.

Chart structure verifies that all required files exist and follow conventions. Chart.yaml must use apiVersion v2 (Helm 3), values.yaml must have sensible defaults, and values.schema.json must validate types. Missing files or invalid structure cause Helm commands to fail.

Values review checks that defaults are safe and no secrets are hardcoded. Secrets in values.yaml are a common security issue — they end up in version control. The chart should use external secret stores or --set for secrets. All values should have comments explaining their purpose.

Template review checks Kubernetes API versions, label consistency, and conditional rendering. Labels must follow the app.kubernetes.io/* convention for discoverability. Selectors must match labels across deployments and services. Trailing whitespace in templates causes rendering issues.

Security review is the most critical section. Pods must run as non-root with read-only filesystems and no privilege escalation. All Linux capabilities should be dropped. RBAC should follow least privilege — no cluster-admin roles. Images should not use :latest tags and should be scanned for vulnerabilities.

Resource management ensures pods have CPU and memory requests and limits. Without limits, a single pod can consume all cluster resources. The HPA should be configurable with sensible defaults. The PDB ensures availability during voluntary disruptions.

Health probes tell Kubernetes when a pod is alive and ready. Liveness probes detect deadlocks and trigger restarts. Readiness probes control traffic routing. Without probes, Kubernetes can't distinguish a healthy pod from a broken one.

## Variants

| Context | Approach | Notes |
|---------|----------|-------|
| Library chart | Focus on named templates | No resources rendered |
| Operator chart | Add CRD review | Custom resource definitions |
| StatefulSet chart | Add PVC and ordering checks | VolumeClaimTemplates, podManagementPolicy |
| Job/CronJob chart | Add restartPolicy and concurrency checks | BackoffLimit, concurrencyRule |
| Multi-tenant | Add namespace isolation checks | NetworkPolicies, PSPs |
| Air-gapped | Verify no external dependencies | All images in private registry |

## What Works

1. Use values.schema.json — catches type errors before deployment
2. Pin image digests, not just tags — tags can be mutated
3. Drop all capabilities, add back only needed ones — least privilege
4. Set resource requests and limits — prevent resource starvation
5. Define both liveness and readiness probes — Kubernetes needs both
6. Use helm lint and kube-score in CI — automate the checklist
7. Document every value in values.yaml — users need to know what each does

## Common Mistakes

1. Secrets in values.yaml — committed to version control
2. No resource limits — pods consume all cluster resources
3. No probes — Kubernetes can't detect unhealthy pods
4. Running as root — unnecessary privilege
5. No PDB — voluntary disruptions cause downtime
6. Using :latest tag — unpredictable deployments
7. No values.schema.json — type errors not caught until deployment

## FAQ

### Should we pin image digests instead of tags?

Yes, for production charts. Tags are mutable — the same tag can point to different images over time. Digests are immutable — the same digest always points to the same image. Pinning digests ensures reproducible deployments. The trade-off is that updating the image requires updating the chart, but this is desirable for production.

### How do we handle secrets in Helm charts?

Never put secrets in values.yaml. Use one of these approaches: (1) --set for individual secrets at deploy time, (2) External secret stores like HashiCorp Vault or AWS Secrets Manager with the External Secrets Operator, (3) Sealed Secrets for encrypting secrets in Git. All three keep secrets out of version control.

### What is the difference between helm lint and kube-score?

helm lint checks chart structure and template syntax — it catches Helm-specific issues like invalid Chart.yaml or template errors. kube-score checks the rendered Kubernetes manifests against best practices — it catches Kubernetes-specific issues like missing probes or resource limits. Use both: helm lint for chart quality, kube-score for manifest quality.

### Should we use Helm hooks for database migrations?

It depends. Helm hooks like post-install and post-upgrade can run migrations automatically. This is convenient but risky: if the migration fails, the release is in an inconsistent state. For critical databases, run migrations outside Helm as a separate step in the CI/CD pipeline. For simple migrations, Helm hooks are acceptable.

### How do we test Helm charts?

Three levels: (1) helm lint for syntax and structure, (2) helm template + kubeval for manifest validation, (3) helm test for runtime tests against a deployed chart. For CI, run helm lint and helm template on every PR. For staging, deploy the chart and run helm test. For production, use helm upgrade --dry-run to preview changes.

Frequently Asked Questions

Should we pin image digests instead of tags?

Yes, for production charts. Tags are mutable — the same tag can point to different images over time. Digests are immutable — the same digest always points to the same image. Pinning digests ensures reproducible deployments. The trade-off is that updating the image requires updating the chart, but this is desirable for production.

How do we handle secrets in Helm charts?

Never put secrets in values.yaml. Use one of these approaches: (1) --set for individual secrets at deploy time, (2) External secret stores like HashiCorp Vault or AWS Secrets Manager with the External Secrets Operator, (3) Sealed Secrets for encrypting secrets in Git. All three keep secrets out of version control.

What is the difference between helm lint and kube-score?

helm lint checks chart structure and template syntax — it catches Helm-specific issues like invalid Chart.yaml or template errors. kube-score checks the rendered Kubernetes manifests against best practices — it catches Kubernetes-specific issues like missing probes or resource limits. Use both: helm lint for chart quality, kube-score for manifest quality.

Should we use Helm hooks for database migrations?

It depends. Helm hooks like post-install and post-upgrade can run migrations automatically. This is convenient but risky: if the migration fails, the release is in an inconsistent state. For critical databases, run migrations outside Helm as a separate step in the CI/CD pipeline. For simple migrations, Helm hooks are acceptable.

How do we test Helm charts?

Three levels: (1) helm lint for syntax and structure, (2) helm template + kubeval for manifest validation, (3) helm test for runtime tests against a deployed chart. For CI, run helm lint and helm template on every PR. For staging, deploy the chart and run helm test. For production, use helm upgrade --dry-run to preview changes.