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/nameapp.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 tolabels, never toselectorLabels
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-namesucceeds) image.tag: ""— falls back to.Chart.AppVersionin 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
securityContextdefaults:runAsNonRoot: true,runAsUser: 1000,readOnlyRootFilesystem: true,allowPrivilegeEscalation: false,capabilities.drop: [ALL]resources.requestsandresources.limitsalways present — include memory limit, omit CPU limit to avoid throttlingimagePullPolicy: IfNotPresent— neverAlwaysunless the chart requires itrbac.create: true— boolean controlling whether RBAC resources are createdserviceAccount.create: trueandserviceAccount.name: ""— name falls back tofullnametemplate 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--namespaceat 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:
typefor every fieldrequiredarray for fields with no safe defaultdescriptionmatching 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:
| Check | Severity |
|---|---|
Missing _helpers.tpl | Critical |
| No resource requests/limits | Critical |
namespace: hardcoded in template metadata | High |
| No liveness/readiness probes | High |
| 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 selectorLabels | High |
| Mutable labels in selectorLabels (breaks upgrades) | High |
imagePullPolicy: Always without justification | Medium |
rbac.create not a boolean or missing | Medium |
serviceAccount.create / serviceAccount.name pattern missing | Medium |
No NOTES.txt | Medium |
No .helmignore | Low |
| Missing Chart.yaml fields (description, appVersion) | Medium |
automountServiceAccountToken: true | Medium |
| Values keys not camelCase | Low |
| Values comments don't start with key name | Low |
| Deeply nested values (>3 levels) — prefer flat | Low |
| Lists in values where maps would work | Low |
| Unquoted string values in values.yaml | Low |
| Undocumented values.yaml keys | Medium |
No values.schema.json | Medium |
| Default values require cluster-specific knowledge | High |
Optional templates not gated on enabled: | Medium |
| Template file names use camelCase instead of dashes | Low |
| Multiple resources in one template file | Low |
| Defined templates not namespaced with chart name | High |
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
| Check | Severity | Fix |
|---|---|---|
No pod securityContext | Critical | Add runAsNonRoot: true, runAsUser: 1000, fsGroup: 1000, seccompProfile.type: RuntimeDefault |
| Container running as root | Critical | Set runAsNonRoot: true, runAsUser: 1000 |
readOnlyRootFilesystem: false | High | Set to true; add emptyDir volume for /tmp |
| Capabilities not dropped | High | capabilities.drop: [ALL]; add back only what is needed |
privileged: true | Critical | Remove; use specific capabilities instead |
allowPrivilegeEscalation: true | High | Set to false |
No seccompProfile | Medium | Set seccompProfile.type: RuntimeDefault |
RBAC
| Check | Severity | Fix |
|---|---|---|
| No dedicated ServiceAccount | Medium | Create one; do not use default SA |
automountServiceAccountToken: true | Medium | Set to false unless pod needs K8s API access |
| ClusterRole instead of Role | Medium | Use namespace-scoped Role unless cluster-wide is justified |
| Wildcard verbs or resources | Critical | Use explicit verbs and resource names |
Network and Secrets
| Check | Severity | Fix |
|---|---|---|
| No NetworkPolicy | Medium | Add default-deny ingress + explicit allow |
| Secrets in values.yaml defaults | Critical | Use empty strings with comments; reference external secrets |
| No PodDisruptionBudget | Medium | Add PDB with minAvailable: 1 for HA workloads |
hostNetwork: true | High | Remove unless required (e.g., CNI plugin) |
hostPID: true or hostIPC: true | Critical | Never in application charts |
Image tag latest or mutable | High | Pin 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:
| Pattern | Risk | Action |
|---|---|---|
selector: changed on Deployment/StatefulSet | Critical — will fail | Rename release or delete + recreate |
storageClassName changed on PVC | Critical | Manual migration required |
kind: changed (e.g. Deployment → StatefulSet) | Critical | Delete old resource first |
ServiceAccount name changed | High | Old RBAC bindings become orphaned |
ConfigMap or Secret removed | High | Verify no pods still reference them |
| Resource limits decreased >50% | Medium | Risk of OOMKill on rollout |
| Replica count → 1 | Medium | HA 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
typefrom the YAML value (string,integer,boolean,object,array) - Sets
descriptionfrom 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
$defsfor 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
ERRin.learnings/ERRORS.md - Reusable pattern that saved time → log as
LRNin.learnings/LEARNINGS.md
Use /platform-skills:self-improve log for each entry worth keeping.