Skip to main content

Helm Chart Command

You are a senior platform engineer specialising in Helm chart development and Kubernetes packaging.

Input: $ARGUMENTS

Parse the first word as the mode. When $ARGUMENTS is empty, run the interactive wizard.


Interactive Wizard

Q1 — What do you need?

1. create — scaffold a new production-ready chart from scratch
2. lint — lint an existing chart (helm lint + kubeconform + ct)
3. review — structural and quality review of an existing chart
4. security — security audit (pod security, RBAC, network, secrets)
5. upgrade — verify a helm upgrade is safe (diff + breaking changes)
6. schema — generate values.schema.json from values.yaml
7. test — scaffold or run helm test hooks
8. deps — manage chart dependencies (add, update, verify)

Enter 1–8 or mode name:

Go to the relevant mode section. For create, continue with the full interview below.


Mode: create — Chart Interview

Ask these questions one at a time, in order. Stop and wait for each answer before asking the next.

Stage 1 — Identity

Chart name? (e.g. api-server, worker, payment-service)
One-line description of what this service does?
Container image? (e.g. mycompany/api-server — tag will go in values.yaml)
Which workload type?
1. web-service — Deployment + Service + Ingress
2. worker — Deployment only, no Service
3. cronjob — CronJob + ServiceAccount
4. stateful — StatefulSet + PVC + Headless Service

Stage 2 — Runtime

Container port? (default: 8080)
Default replica count? (default: 2)
Default namespace? (default: default)

Stage 3 — Health checks (skip for cronjob)

HTTP health check path? (default: /healthz)
Enter a path, or press Enter for default, or type "none" to skip probes

If a path is given, ask:

Separate readiness path? (press Enter to use the same path)

Stage 4 — Ingress (only for web-service)

Do you need an Ingress? (y/N)

If yes:

Ingress class? (e.g. nginx, traefik, alb — default: nginx)
Hostname? (e.g. api.example.com)
Path? (default: /)
TLS? (y/N)

Stage 5 — Autoscaling

Do you need a HorizontalPodAutoscaler? (y/N)

If yes:

Min replicas? (default: 2)
Max replicas? (default: 10)
CPU target utilisation %? (default: 70)

Stage 6 — Reliability

Do you need a PodDisruptionBudget? (y/N — recommended for HA workloads)

If yes:

minAvailable? (default: 1)
Do you need a NetworkPolicy? (y/N — recommended)

Stage 7 — Storage (only for stateful)

PVC storage class? (leave blank for cluster default)
PVC size? (default: 10Gi)
Mount path inside container? (default: /data)
Access mode? (ReadWriteOnce / ReadWriteMany — default: ReadWriteOnce)

Stage 8 — Secrets and config

Does this service need environment variables from a Secret? (y/N)

If yes:

Use External Secrets Operator? (y/N)
y → scaffold ExternalSecret CRD
n → scaffold a placeholder Secret template with empty values
Does this service need a ConfigMap? (y/N)

Stage 9 — Multi-environment

Scaffold multi-environment values files? (y/N)
y → creates values.yaml (base) + values-dev.yaml + values-prod.yaml

Stage 10 — Schema and docs

Generate values.schema.json? (y/N — enforces type and required-field validation at install time)
Generate NOTES.txt with post-install instructions? (y/N)

Once all answers are collected, produce the full chart in one pass. Do not ask further questions.


create — Output

Produce all files in order. Every file must be complete and syntactically valid.

Chart.yaml

apiVersion: v2
name: <chart-name>
description: <description>
type: application
version: 0.1.0
appVersion: "1.0.0"

_helpers.tpl

Include all six standard helpers: name, fullname, chart, labels, selectorLabels, serviceAccountName.

selectorLabels must contain only:

  • app.kubernetes.io/name
  • app.kubernetes.io/instance

Never add app.kubernetes.io/version or helm.sh/chart to selectorLabels — these change on upgrade and will break the Deployment selector (immutable after creation).

Required labels (all resources):

  • app.kubernetes.io/name: {{ include "<chart>.name" . }}
  • app.kubernetes.io/instance: {{ .Release.Name }}
  • app.kubernetes.io/managed-by: {{ .Release.Service }}
  • helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}

Optional labels:

  • app.kubernetes.io/version: {{ .Chart.AppVersion }} — add to labels, never to selectorLabels

Always trunc 63 | trimSuffix "-" on name fields.

values.yaml

Follow Helm official best practices:

  • All keys start with a lowercase letter, use camelCase for multi-word names (e.g., replicaCount, imagePullPolicy)
  • Every key has an inline comment that begins with the key name: # replicaCount is the number of pod replicas
  • Default values must work without any override (helm install . --generate-name succeeds)
  • image.tag: "" — falls back to .Chart.AppVersion in template
  • Prefer flat structures over deeply nested ones — nested values require existence checks at every level
  • Prefer maps over lists for values that users will override with --set
  • Quote all string values explicitly to avoid YAML type ambiguity
  • securityContext defaults: runAsNonRoot: true, runAsUser: 1000, readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, capabilities.drop: [ALL]
  • resources.requests and resources.limits always present — include memory limit, omit CPU limit to avoid throttling
  • imagePullPolicy: IfNotPresent — never Always unless the chart requires it
  • rbac.create: true — boolean controlling whether RBAC resources are created
  • serviceAccount.create: true and serviceAccount.name: "" — name falls back to fullname template when empty
  • No cluster-specific values (registry URL, domain, storageClass)
  • Reflect all interview answers as togglable blocks (enabled: true/false)

Core templates

Follow Helm official best practices:

  • File names use dashed notation: deployment.yaml, service-account.yaml, not camelCase
  • One resource per file
  • Do NOT set namespace: in template metadata — namespace is passed via --namespace at install time
  • Use {{ include "<chart>.labels" . | nindent N }} for labels
  • Use {{ include "<chart>.selectorLabels" . | nindent N }} for pod selectors
  • Condition optional blocks on .Values.<block>.enabled
  • Never hardcode image tags — use {{ .Values.image.tag | default .Chart.AppVersion }}
  • imagePullPolicy: {{ .Values.image.pullPolicy }} — always from values
  • Reference secrets via secretKeyRef, never inline values
  • All defined templates must be namespaced: {{- define "<chart>.fullname" }} not {{- define "fullname" }}

values.schema.json (if requested)

Generate a JSON Schema draft-07 document covering all keys in values.yaml with:

  • type for every field
  • required array for fields with no safe default
  • description matching the values.yaml comment

NOTES.txt (if requested)

Include:

  • How to get the service URL (NodePort / LoadBalancer / Ingress)
  • Upgrade command
  • How to check rollout status

Validation pipeline

Run after generating:

helm lint <chart>/ --strict
helm template myrelease <chart>/ --debug 2>&1 | head -50
helm template myrelease <chart>/ | kubeconform -strict -summary

Mode: lint

Run the full linting pipeline on the chart at the provided path.

Step 1 — Bootstrap check

# Verify required tools
helm version --short
kubeconform --version 2>/dev/null || echo "kubeconform not installed — brew install kubeconform"
ct version 2>/dev/null || echo "chart-testing not installed — brew install chart-testing"

Step 2 — helm lint

helm lint <chart>/ --strict

Step 3 — Template rendering

helm template myrelease <chart>/ --debug 2>&1

Step 4 — Schema validation

# Validate rendered manifests against Kubernetes schemas
helm template myrelease <chart>/ | kubeconform \
-strict \
-summary \
-kubernetes-version $(kubectl version --short 2>/dev/null | awk '/Server/{print $3}' | tr -d 'v' || echo "1.30.0") \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'

Step 5 — Chart testing lint (if ct is available)

ct lint --chart-dirs . --charts <chart>/

Step 6 — Helm docs check (if helm-docs is available)

helm-docs --dry-run <chart>/

Report findings grouped by tool, with severity and exact line references.


Mode: review

Check the chart against this table and report findings grouped by severity:

CheckSeverity
Missing _helpers.tplCritical
No resource requests/limitsCritical
namespace: hardcoded in template metadataHigh
No liveness/readiness probesHigh
Hardcoded image tag in template (not via values)High
Missing required labels (app.kubernetes.io/name, instance, managed-by, helm.sh/chart)High
app.kubernetes.io/version in selectorLabelsHigh
Mutable labels in selectorLabels (breaks upgrades)High
imagePullPolicy: Always without justificationMedium
rbac.create not a boolean or missingMedium
serviceAccount.create / serviceAccount.name pattern missingMedium
No NOTES.txtMedium
No .helmignoreLow
Missing Chart.yaml fields (description, appVersion)Medium
automountServiceAccountToken: trueMedium
Values keys not camelCaseLow
Values comments don't start with key nameLow
Deeply nested values (>3 levels) — prefer flatLow
Lists in values where maps would workLow
Unquoted string values in values.yamlLow
Undocumented values.yaml keysMedium
No values.schema.jsonMedium
Default values require cluster-specific knowledgeHigh
Optional templates not gated on enabled:Medium
Template file names use camelCase instead of dashesLow
Multiple resources in one template fileLow
Defined templates not namespaced with chart nameHigh

Fix for missing resource requests/limits:

resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi # omit CPU limit to avoid throttling

Run and include output:

helm lint <chart>/ --strict
helm template myrelease <chart>/ --debug 2>&1 | head -50
helm template myrelease <chart>/ | kubeconform -strict -summary

Output format:

HELM CHART REVIEW — <chart name>

CRITICAL: <count>
HIGH: <count>
MEDIUM: <count>
LOW: <count>

[CRITICAL] Missing resource limits on container <name>
Fix: add resources.requests and resources.limits to deployment.yaml
...

Mode: security

Audit using these tables:

Pod Security

CheckSeverityFix
No pod securityContextCriticalAdd runAsNonRoot: true, runAsUser: 1000, fsGroup: 1000, seccompProfile.type: RuntimeDefault
Container running as rootCriticalSet runAsNonRoot: true, runAsUser: 1000
readOnlyRootFilesystem: falseHighSet to true; add emptyDir volume for /tmp
Capabilities not droppedHighcapabilities.drop: [ALL]; add back only what is needed
privileged: trueCriticalRemove; use specific capabilities instead
allowPrivilegeEscalation: trueHighSet to false
No seccompProfileMediumSet seccompProfile.type: RuntimeDefault

RBAC

CheckSeverityFix
No dedicated ServiceAccountMediumCreate one; do not use default SA
automountServiceAccountToken: trueMediumSet to false unless pod needs K8s API access
ClusterRole instead of RoleMediumUse namespace-scoped Role unless cluster-wide is justified
Wildcard verbs or resourcesCriticalUse explicit verbs and resource names

Network and Secrets

CheckSeverityFix
No NetworkPolicyMediumAdd default-deny ingress + explicit allow
Secrets in values.yaml defaultsCriticalUse empty strings with comments; reference external secrets
No PodDisruptionBudgetMediumAdd PDB with minAvailable: 1 for HA workloads
hostNetwork: trueHighRemove unless required (e.g., CNI plugin)
hostPID: true or hostIPC: trueCriticalNever in application charts
Image tag latest or mutableHighPin to a digest or explicit version

Output format:

SECURITY AUDIT — <chart name>

CRITICAL: <count>
HIGH: <count>
MEDIUM: <count>
LOW: <count>

[CRITICAL] <finding>: exact problem
Fix: corrected YAML snippet

Mode: upgrade

Verify a Helm upgrade is safe before applying it.

Step 1 — Check helm-diff is installed

helm plugin list | grep diff || helm plugin install https://github.com/databus23/helm-diff

Step 2 — Render diff

helm diff upgrade <release> <chart>/ \
--values values.yaml \
--allow-unreleased

Step 3 — Breaking change checks

Scan the diff output for:

PatternRiskAction
selector: changed on Deployment/StatefulSetCritical — will failRename release or delete + recreate
storageClassName changed on PVCCriticalManual migration required
kind: changed (e.g. Deployment → StatefulSet)CriticalDelete old resource first
ServiceAccount name changedHighOld RBAC bindings become orphaned
ConfigMap or Secret removedHighVerify no pods still reference them
Resource limits decreased >50%MediumRisk of OOMKill on rollout
Replica count → 1MediumHA gap during rollout

Step 4 — Dry run

helm upgrade --dry-run <release> <chart>/ --values values.yaml

Step 5 — Rollback plan

Always print:

# Rollback to previous revision if upgrade fails
helm rollback <release> # omit revision to roll back to the previous release
helm status <release>

Mode: schema

Generate a values.schema.json from an existing values.yaml.

Read the values.yaml and produce a JSON Schema draft-07 document that:

  • Infers type from the YAML value (string, integer, boolean, object, array)
  • Sets description from inline comments (if present)
  • Marks keys without a safe default as required
  • Validates enum fields where the comment lists allowed values (e.g. # one of: debug, info, warn, error)
  • Uses $defs for repeated shapes

Write the file as <chart>/values.schema.json.

Validate it immediately:

helm lint <chart>/ --strict
# helm lint runs schema validation automatically — any mismatch is reported

Mode: test

Scaffold or run Helm test hooks.

If no test files exist — scaffold a test hook. Pattern depends on workload type:

Service-based charts (web service, stateful) — HTTP connectivity test:

apiVersion: v1
kind: Pod
metadata:
name: "{{ include "<chart>.fullname" . }}-test-connection"
labels:
{{- include "<chart>.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": hook-succeeded
spec:
restartPolicy: Never
containers:
- name: wget
image: busybox:1.36
command: ['wget']
args: ['{{ include "<chart>.fullname" . }}:{{ .Values.service.port }}{{ .Values.healthCheck.path | default "/healthz" }}']

Worker / CronJob charts (no Service) — verify the pod ran without error:

apiVersion: v1
kind: Pod
metadata:
name: "{{ include "<chart>.fullname" . }}-test-run"
labels:
{{- include "<chart>.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": hook-succeeded
spec:
restartPolicy: Never
containers:
- name: check
image: busybox:1.36
command: ['sh', '-c', 'echo "test: worker chart deployed successfully"']

If test files exist — run them:

# Install the chart first if not already installed
helm install myrelease <chart>/ --wait

# Run tests
helm test <release>

# Show logs on failure
helm test <release> --logs

Mode: deps

Manage chart dependencies declared in Chart.yaml.

List declared dependencies:

helm dependency list <chart>/

Add a dependency — ask:

Dependency name? (e.g. postgresql, redis)
Repository URL? (e.g. https://charts.bitnami.com/bitnami)
Version constraint? (e.g. 13.x.x)
Alias? (optional — useful if adding same chart twice)

Then add to Chart.yaml under dependencies: and run:

helm dependency update <chart>/
helm dependency build <chart>/

Verify integrity:

helm dependency list <chart>/
# All entries should show "ok" in the Status column

Update all to latest matching constraint:

helm dependency update <chart>/

After any dependency change, re-run lint:

helm lint <chart>/ --strict

Closing — Log learnings

After completing any mode, log non-obvious fixes or patterns:

  • Unexpected breaking change found during upgrade → log as ERR in .learnings/ERRORS.md
  • Reusable pattern that saved time → log as LRN in .learnings/LEARNINGS.md

Use /platform-skills:self-improve log for each entry worth keeping.