
Helm Chart Builder
- 565 installs
- 23.5k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
helm-chart-builder is a Claude Code skill that generates production-ready Helm charts with Kubernetes templates, helpers, and values schemas for developers who deploy services to clusters.
About
helm-chart-builder is a Helm chart patterns reference skill from alirezarezvani/claude-skills that scaffolds complete Kubernetes packaging for production releases. It documents a minimal production chart with Chart.yaml, values.yaml, .helmignore, and seven core templates—deployment, service, serviceaccount, helpers, NOTES.txt, and a connection test—plus an expanded layout adding ingress, HPA, PDB, NetworkPolicy, ConfigMap, Secret, and values.schema.json validation. Developers reach for helm-chart-builder when containerizing a new microservice, standardizing cluster deployments, or replacing hand-rolled YAML with Helm best practices. The skill emphasizes _helpers.tpl conventions, test hooks, and schema-validated values so agents output charts ready for helm install rather than demo manifests.
- Generates minimal and full production Helm chart structures including all standard Kubernetes resources
- Includes standard _helpers.tpl with name, fullname, chart, and selector label definitions
- Creates values.yaml with comprehensive configuration options and JSON schema validation support
- Produces deployment, service, ingress, HPA, PDB, NetworkPolicy, ConfigMap and Secret templates
- Outputs NOTES.txt and test-connection.yaml for improved operator experience
Helm Chart Builder by the numbers
- 565 all-time installs (skills.sh)
- Ranked #254 of 1,440 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/alirezarezvani/claude-skills --skill helm-chart-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 565 |
|---|---|
| repo stars | ★ 23.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
How do you scaffold a production Helm chart?
Generate complete, production-ready Helm charts for Kubernetes deployments with best-practice templates and helpers.
Who is it for?
Backend and platform engineers who need standardized, production-grade Helm charts for Kubernetes microservice deployments.
Skip if: Developers deploying only to serverless platforms or local Docker Compose without any Kubernetes cluster target.
When should I use this skill?
User asks to create a Helm chart, Kubernetes deployment templates, values.yaml structure, or helm install packaging for a service.
What you get
Chart.yaml, values.yaml, _helpers.tpl, Kubernetes manifest templates, NOTES.txt, and optional values.schema.json
- Helm chart directory
- values.yaml defaults
- Kubernetes manifest templates
By the numbers
- Minimal production chart includes 7 core template and metadata files
- Full production chart adds 8+ additional templates including ingress, HPA, PDB, and NetworkPolicy
Files
Helm Chart Builder
Production-grade Helm charts. Sensible defaults. Secure by design. No cargo-culting.
Opinionated Helm workflow that turns ad-hoc Kubernetes manifests into maintainable, testable, reusable charts. Covers chart structure, values design, template patterns, dependency management, and security hardening.
Not a Helm tutorial — a set of concrete decisions about how to build charts that operators trust and developers don't fight.
---
Slash Commands
| Command | What it does |
|---|---|
/helm:create | Scaffold a production-ready Helm chart with best-practice structure |
/helm:review | Analyze an existing chart for issues — missing labels, hardcoded values, template anti-patterns |
/helm:security | Audit chart for security issues — RBAC, network policies, pod security, secrets handling |
---
When This Skill Activates
Recognize these patterns from the user:
- "Create a Helm chart for this service"
- "Review my Helm chart"
- "Is this chart secure?"
- "Design a values.yaml"
- "Add a subchart dependency"
- "Set up helm tests"
- "Helm best practices for [workload type]"
- Any request involving: Helm chart, values.yaml, Chart.yaml, templates, helpers, _helpers.tpl, subcharts, helm lint, helm test
If the user has a Helm chart or wants to package Kubernetes resources → this skill applies.
---
Workflow
/helm:create — Chart Scaffolding
1. Identify workload type
- Web service (Deployment + Service + Ingress)
- Worker (Deployment, no Service)
- CronJob (CronJob + ServiceAccount)
- Stateful service (StatefulSet + PVC + Headless Service)
- Library chart (no templates, only helpers)
2. Scaffold chart structure
mychart/
├── Chart.yaml # Chart metadata and dependencies
├── values.yaml # Default configuration
├── values.schema.json # Optional: JSON Schema for values validation
├── .helmignore # Files to exclude from packaging
├── templates/
│ ├── _helpers.tpl # Named templates and helper functions
│ ├── deployment.yaml # Workload resource
│ ├── service.yaml # Service exposure
│ ├── ingress.yaml # Ingress (if applicable)
│ ├── serviceaccount.yaml # ServiceAccount
│ ├── hpa.yaml # HorizontalPodAutoscaler
│ ├── pdb.yaml # PodDisruptionBudget
│ ├── networkpolicy.yaml # NetworkPolicy
│ ├── configmap.yaml # ConfigMap (if needed)
│ ├── secret.yaml # Secret (if needed)
│ ├── NOTES.txt # Post-install usage instructions
│ └── tests/
│ └── test-connection.yaml
└── charts/ # Subcharts (dependencies)3. Apply Chart.yaml best practices
METADATA
├── apiVersion: v2 (Helm 3 only — never v1)
├── name: matches directory name exactly
├── version: semver (chart version, not app version)
├── appVersion: application version string
├── description: one-line summary of what the chart deploys
└── type: application (or library for shared helpers)
DEPENDENCIES
├── Pin dependency versions with ~X.Y.Z (patch-level float)
├── Use condition field to make subcharts optional
├── Use alias for multiple instances of same subchart
└── Run helm dependency update after changes4. Generate values.yaml with documentation
- Every value has an inline comment explaining purpose and type
- Sensible defaults that work for development
- Override-friendly structure (flat where possible, nested only when logical)
- No hardcoded cluster-specific values (image registry, domain, storage class)
5. Validate
python3 scripts/chart_analyzer.py mychart/
helm lint mychart/
helm template mychart/ --debug/helm:review — Chart Analysis
1. Check chart structure
| Check | Severity | Fix |
|---|---|---|
| Missing _helpers.tpl | High | Create helpers for common labels and selectors |
| No NOTES.txt | Medium | Add post-install instructions |
| No .helmignore | Low | Create one to exclude .git, CI files, tests |
| Missing Chart.yaml fields | Medium | Add description, appVersion, maintainers |
| Hardcoded values in templates | High | Extract to values.yaml with defaults |
2. Check template quality
| Check | Severity | Fix |
|---|---|---|
| Missing standard labels | High | Use app.kubernetes.io/* labels via _helpers.tpl |
| No resource requests/limits | Critical | Add resources section with defaults in values.yaml |
| Hardcoded image tag | High | Use {{ .Values.image.repository }}:{{ .Values.image.tag }} |
| No imagePullPolicy | Medium | Default to IfNotPresent, overridable |
| Missing liveness/readiness probes | High | Add probes with configurable paths and ports |
| No pod anti-affinity | Medium | Add preferred anti-affinity for HA |
| Duplicate template code | Medium | Extract into named templates in _helpers.tpl |
3. Check values.yaml quality
python3 scripts/values_validator.py mychart/values.yaml4. Generate review report
HELM CHART REVIEW — [chart name]
Date: [timestamp]
CRITICAL: [count]
HIGH: [count]
MEDIUM: [count]
LOW: [count]
[Detailed findings with fix recommendations]/helm:security — Security Audit
1. Pod security audit
| Check | Severity | Fix |
|---|---|---|
| No securityContext | Critical | Add runAsNonRoot, readOnlyRootFilesystem |
| Running as root | Critical | Set runAsNonRoot: true, runAsUser: 1000 |
| Writable root filesystem | High | Set readOnlyRootFilesystem: true + emptyDir for tmp |
| All capabilities retained | High | Drop ALL, add only specific needed caps |
| Privileged container | Critical | Set privileged: false, use specific capabilities |
| No seccomp profile | Medium | Set seccompProfile.type: RuntimeDefault |
| allowPrivilegeEscalation true | High | Set allowPrivilegeEscalation: false |
2. RBAC audit
| Check | Severity | Fix |
|---|---|---|
| No ServiceAccount | Medium | Create dedicated SA, don't use default |
| automountServiceAccountToken true | Medium | Set to false unless pod needs K8s API access |
| ClusterRole instead of Role | Medium | Use namespace-scoped Role unless cluster-wide needed |
| Wildcard permissions | Critical | Use specific resource names and verbs |
| No RBAC at all | Low | Acceptable if pod doesn't need K8s API access |
3. Network and secrets audit
| Check | Severity | Fix |
|---|---|---|
| No NetworkPolicy | Medium | Add default-deny ingress + explicit allow rules |
| Secrets in values.yaml | Critical | Use external secrets operator or sealed-secrets |
| No PodDisruptionBudget | Medium | Add PDB with minAvailable for HA workloads |
| hostNetwork: true | High | Remove unless absolutely required (e.g., CNI plugin) |
| hostPID or hostIPC | Critical | Never use in application charts |
4. Generate security report
SECURITY AUDIT — [chart name]
Date: [timestamp]
CRITICAL: [count]
HIGH: [count]
MEDIUM: [count]
LOW: [count]
[Detailed findings with remediation steps]---
Tooling
scripts/chart_analyzer.py
CLI utility for static analysis of Helm chart directories.
Features:
- Chart structure validation (required files, directory layout)
- Template anti-pattern detection (hardcoded values, missing labels, no resource limits)
- Chart.yaml metadata checks
- Standard labels verification (app.kubernetes.io/*)
- Security baseline checks
- JSON and text output
Usage:
# Analyze a chart directory
python3 scripts/chart_analyzer.py mychart/
# JSON output
python3 scripts/chart_analyzer.py mychart/ --output json
# Security-focused analysis
python3 scripts/chart_analyzer.py mychart/ --securityscripts/values_validator.py
CLI utility for validating values.yaml against best practices.
Features:
- Documentation coverage (inline comments)
- Type consistency checks
- Hardcoded secrets detection
- Default value quality analysis
- Structure depth analysis
- Naming convention validation
- JSON and text output
Usage:
# Validate values.yaml
python3 scripts/values_validator.py values.yaml
# JSON output
python3 scripts/values_validator.py values.yaml --output json
# Strict mode (fail on warnings)
python3 scripts/values_validator.py values.yaml --strict---
Template Patterns
Pattern 1: Standard Labels (_helpers.tpl)
{{/*
Common labels for all resources.
*/}}
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels (subset of common labels — must be immutable).
*/}}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}Pattern 2: Conditional Resources
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "mychart.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}Pattern 3: Security-Hardened Pod Spec
spec:
serviceAccountName: {{ include "mychart.serviceAccountName" . }}
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: {{ .Chart.Name }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
resources:
{{- toYaml .Values.resources | nindent 8 }}
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}---
Values Design Principles
STRUCTURE
├── Flat over nested (image.tag > container.spec.image.tag)
├── Group by resource (service.*, ingress.*, resources.*)
├── Use enabled: true/false for optional resources
├── Document every key with inline YAML comments
└── Provide sensible development defaults
NAMING
├── camelCase for keys (replicaCount, not replica_count)
├── Boolean keys: use adjectives (enabled, required) not verbs
├── Nested keys: max 3 levels deep
└── Match upstream conventions (image.repository, image.tag, image.pullPolicy)
ANTI-PATTERNS
├── Hardcoded cluster URLs or domains
├── Secrets as default values
├── Empty strings where null is correct
├── Deeply nested structures (>3 levels)
├── Undocumented values
└── values.yaml that doesn't work without overrides---
Dependency Management
SUBCHARTS
├── Use Chart.yaml dependencies (not requirements.yaml — Helm 3)
├── Pin versions: version: ~15.x.x (patch float)
├── Use condition: to make optional: condition: postgresql.enabled
├── Use alias: for multiple instances of same chart
├── Override subchart values under subchart name key in values.yaml
└── Run helm dependency update before packaging
LIBRARY CHARTS
├── type: library in Chart.yaml — no templates directory
├── Export named templates only — no rendered resources
├── Use for shared labels, annotations, security contexts
└── Version independently from application charts---
Proactive Triggers
Flag these without being asked:
- No _helpers.tpl → Create one. Every chart needs standard labels and fullname helpers.
- Hardcoded image tag in template → Extract to values.yaml. Tags must be overridable.
- No resource requests/limits → Add them. Pods without limits can starve the node.
- Running as root → Add securityContext. No exceptions for production charts.
- No NOTES.txt → Create one. Users need post-install instructions.
- Secrets in values.yaml defaults → Remove them. Use placeholders with comments explaining how to provide secrets.
- No liveness/readiness probes → Add them. Kubernetes needs to know if the pod is healthy.
- Missing app.kubernetes.io labels → Add via _helpers.tpl. Required for proper resource tracking.
---
Installation
One-liner (any tool)
git clone https://github.com/alirezarezvani/claude-skills.git
cp -r claude-skills/engineering/helm-chart-builder ~/.claude/skills/Multi-tool install
./scripts/convert.sh --skill helm-chart-builder --tool codex|gemini|cursor|windsurf|openclawOpenClaw
clawhub install cs-helm-chart-builder---
Related Skills
- senior-devops — Broader DevOps scope (CI/CD, IaC, monitoring). Complementary — use helm-chart-builder for chart-specific work, senior-devops for pipeline and infrastructure.
- docker-development — Container building. Complementary — docker-development builds the images, helm-chart-builder deploys them to Kubernetes.
- ci-cd-pipeline-builder — Pipeline construction. Complementary — helm-chart-builder defines the deployment artifact, ci-cd-pipeline-builder automates its delivery.
- senior-security — Application security. Complementary — helm-chart-builder covers Kubernetes-level security (RBAC, pod security), senior-security covers application-level threats.
Helm Chart Patterns Reference
Standard Chart Structure
Minimal Production Chart
mychart/
├── Chart.yaml
├── values.yaml
├── .helmignore
└── templates/
├── _helpers.tpl
├── deployment.yaml
├── service.yaml
├── serviceaccount.yaml
├── NOTES.txt
└── tests/
└── test-connection.yamlFull Production Chart
mychart/
├── Chart.yaml
├── values.yaml
├── values.schema.json # JSON Schema validation
├── .helmignore
├── templates/
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── serviceaccount.yaml
│ ├── hpa.yaml
│ ├── pdb.yaml
│ ├── networkpolicy.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── NOTES.txt
│ └── tests/
│ └── test-connection.yaml
└── charts/ # Managed by helm dependency update---
_helpers.tpl — Standard Helpers
Every chart needs these. Copy and adapt.
{{/*
Expand the name of the chart.
*/}}
{{- define "mychart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
Truncated at 63 chars because some Kubernetes name fields are limited.
*/}}
{{- define "mychart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "mychart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels.
*/}}
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
{{ include "mychart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels (immutable — used in matchLabels).
*/}}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use.
*/}}
{{- define "mychart.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "mychart.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}Why These Helpers Matter
- Name truncation — Kubernetes names max at 63 characters. Always trunc.
- Selector labels separate from common labels — selectors are immutable after creation. Adding
app.kubernetes.io/versionto selectors breaks upgrades. - nameOverride vs fullnameOverride —
nameOverridereplaces the chart name portion,fullnameOverridereplaces everything.
---
Deployment Patterns
Standard Web Service
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "mychart.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "mychart.serviceAccountName" . }}
automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}Worker (No Service)
# Same as above but without ports, probes, or Service resource
# Use for background workers, queue consumers, cron jobs
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: {{ toYaml .Values.command | nindent 8 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}---
Conditional Resource Patterns
Optional Ingress
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "mychart.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}Optional HPA
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "mychart.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}---
PodDisruptionBudget
{{- if .Values.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
{{- if .Values.pdb.minAvailable }}
minAvailable: {{ .Values.pdb.minAvailable }}
{{- end }}
{{- if .Values.pdb.maxUnavailable }}
maxUnavailable: {{ .Values.pdb.maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
{{- end }}---
Test Connection Template
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "mychart.fullname" . }}-test-connection"
labels:
{{- include "mychart.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "mychart.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never---
NOTES.txt Pattern
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mychart.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
kubectl get --namespace {{ .Release.Namespace }} svc {{ include "mychart.fullname" . }} -w
{{- else if contains "ClusterIP" .Values.service.type }}
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "mychart.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
echo "Visit http://127.0.0.1:{{ .Values.service.port }}"
{{- end }}---
Dependency Management
Chart.yaml with Dependencies
apiVersion: v2
name: myapp
version: 1.0.0
appVersion: "2.5.0"
dependencies:
- name: postgresql
version: ~15.5.0
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
- name: redis
version: ~19.0.0
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
- name: common
version: ~2.0.0
repository: https://charts.bitnami.com/bitnami
tags:
- bitnami-commonOverriding Subchart Values
# values.yaml — subchart values go under the dependency name
postgresql:
enabled: true
auth:
database: myapp
username: myapp
primary:
resources:
requests:
cpu: 250m
memory: 256Mi
redis:
enabled: falseCommands
# Download dependencies
helm dependency update mychart/
# List dependencies
helm dependency list mychart/
# Build (same as update but doesn't update Chart.lock)
helm dependency build mychart/---
Troubleshooting Checklist
| Symptom | Likely Cause | Fix |
|---|---|---|
| Template renders empty | Missing {{- if }} or wrong value path | helm template --debug to see rendered output |
| Upgrade fails on selector change | Selector labels changed between versions | Never change selectorLabels — they're immutable |
| Values not applying | Wrong nesting in values override | Check indentation and key paths |
| Subchart not rendering | Missing condition: or dependency not updated | Run helm dependency update |
| Name too long | Kubernetes 63-char limit | Ensure trunc 63 in _helpers.tpl |
| RBAC permission denied | ServiceAccount missing or wrong Role | Check SA exists and RoleBinding is correct |
Values.yaml Design Reference
Design Principles
1. Every Value Is Documented
# Bad — what does this mean?
replicaCount: 1
maxSurge: 25%
# Good — clear purpose, type, and constraints
# -- Number of pod replicas. Ignored when autoscaling.enabled is true.
replicaCount: 1
# -- Maximum number of pods above desired count during rolling update (int or percentage).
maxSurge: 25%2. Sensible Defaults That Work
A user should be able to helm install mychart . with zero overrides and get a working deployment.
# Bad — broken without override
image:
repository: "" # Fails: no image
tag: "" # Fails: no tag
# Good — works out of the box
image:
repository: nginx # Default image for development
tag: "" # Defaults to .Chart.AppVersion in template
pullPolicy: IfNotPresent3. Flat Over Nested
# Bad — 5 levels deep, painful to override
container:
spec:
security:
context:
runAsNonRoot: true
# Good — 2 levels, easy to override with --set
securityContext:
runAsNonRoot: trueRule of thumb: Max 3 levels of nesting. If you need more, redesign.
4. Group by Resource
# Good — grouped by Kubernetes resource
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
hosts: []
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10---
Standard Values Structure
Recommended Layout Order
# -- Number of pod replicas
replicaCount: 1
# -- Override chart name
nameOverride: ""
# -- Override fully qualified app name
fullnameOverride: ""
image:
# -- Container image repository
repository: myapp
# -- Image pull policy
pullPolicy: IfNotPresent
# -- Image tag (defaults to .Chart.AppVersion)
tag: ""
# -- Image pull secrets for private registries
imagePullSecrets: []
serviceAccount:
# -- Create a ServiceAccount
create: true
# -- Annotations for the ServiceAccount
annotations: {}
# -- ServiceAccount name (generated from fullname if not set)
name: ""
# -- Automount the service account token
automount: false
# -- Pod annotations
podAnnotations: {}
# -- Additional pod labels
podLabels: {}
# -- Pod security context
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
# -- Container security context
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
service:
# -- Service type
type: ClusterIP
# -- Service port
port: 80
ingress:
# -- Enable ingress
enabled: false
# -- Ingress class name
className: ""
# -- Ingress annotations
annotations: {}
# -- Ingress hosts
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
# -- Ingress TLS configuration
tls: []
# -- Container resource requests and limits
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
# -- Liveness probe configuration
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 20
# -- Readiness probe configuration
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 5
periodSeconds: 10
autoscaling:
# -- Enable horizontal pod autoscaler
enabled: false
# -- Minimum replicas
minReplicas: 1
# -- Maximum replicas
maxReplicas: 10
# -- Target CPU utilization percentage
targetCPUUtilizationPercentage: 80
# -- Target memory utilization percentage (optional)
# targetMemoryUtilizationPercentage: 80
pdb:
# -- Enable PodDisruptionBudget
enabled: false
# -- Minimum available pods
minAvailable: 1
# -- Maximum unavailable pods (alternative to minAvailable)
# maxUnavailable: 1
# -- Node selector constraints
nodeSelector: {}
# -- Tolerations for pod scheduling
tolerations: []
# -- Affinity rules for pod scheduling
affinity: {}
# -- Additional volumes
volumes: []
# -- Additional volume mounts
volumeMounts: []---
Anti-Patterns
1. Secrets in Default Values
# BAD — secret visible in chart package, git history, Helm release
database:
password: "mysecretpassword"
apiKey: "sk-abc123"
# GOOD — empty defaults with documentation
database:
# -- Database password (required). Provide via --set or external secret.
password: ""
# -- API key. Use external-secrets or sealed-secrets in production.
apiKey: ""2. Cluster-Specific Defaults
# BAD — won't work on any other cluster
ingress:
host: app.my-company.internal
storageClass: gp3
registry: 123456789.dkr.ecr.us-east-1.amazonaws.com
# GOOD — generic defaults
ingress:
host: chart-example.local
storageClass: "" # Uses cluster default
image:
repository: myapp # Override for private registry3. Boolean Naming
# BAD — unclear, verb-based
createServiceAccount: true
doAutoScale: false
skipTLS: true
# GOOD — adjective-based, consistent
serviceAccount:
create: true # "Is it created?" reads naturally
autoscaling:
enabled: false # "Is it enabled?" reads naturally
tls:
insecureSkipVerify: false # Matches Go/K8s convention4. Undocumented Values
# BAD — what are these? What types? What are valid options?
foo: bar
maxRetries: 3
mode: advanced
workers: 4
# GOOD — purpose, type, and constraints are clear
# -- Operation mode. Options: "simple", "advanced", "debug"
mode: advanced
# -- Number of background worker threads (1-16)
workers: 4
# -- Maximum retry attempts for failed API calls
maxRetries: 35. Empty String vs Null
# BAD — ambiguous: is empty string intentional?
annotations: ""
nodeSelector: ""
# GOOD — null/empty map means "not set"
annotations: {}
nodeSelector: {}
# Or simply omit optional values---
Override Patterns
Hierarchy (lowest to highest priority)
1. values.yaml in chart 2. Parent chart's values.yaml (for subcharts) 3. -f custom-values.yaml (left to right, last wins) 4. --set key=value (highest priority)
Common Override Scenarios
# Production override file
helm install myapp . -f values-production.yaml
# Quick override with --set
helm install myapp . --set replicaCount=3 --set image.tag=v2.1.0
# Multiple value files (last wins)
helm install myapp . -f values-base.yaml -f values-production.yaml -f values-secrets.yamlvalues-production.yaml Pattern
# Production overrides only — don't repeat defaults
replicaCount: 3
image:
tag: "v2.1.0"
pullPolicy: IfNotPresent
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "2"
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 20
ingress:
enabled: true
className: nginx
hosts:
- host: app.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: app-tls
hosts:
- app.example.com---
Type Safety with values.schema.json
Basic Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["replicaCount", "image"],
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1,
"description": "Number of pod replicas"
},
"image": {
"type": "object",
"required": ["repository"],
"properties": {
"repository": {
"type": "string",
"minLength": 1,
"description": "Container image repository"
},
"tag": {
"type": "string",
"description": "Image tag"
},
"pullPolicy": {
"type": "string",
"enum": ["Always", "IfNotPresent", "Never"],
"description": "Image pull policy"
}
}
},
"service": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["ClusterIP", "NodePort", "LoadBalancer"],
"description": "Kubernetes service type"
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "Service port number"
}
}
}
}
}Why Use Schema
- Fails fast —
helm installrejects invalid values before rendering templates - Documents types — self-documenting valid options (enums, ranges)
- IDE support — editors can autocomplete and validate values files
- CI safety — catches typos in value overrides early
---
Testing Values
helm lint
# Basic lint
helm lint mychart/
# Lint with override values
helm lint mychart/ -f values-production.yaml
# Lint with --set
helm lint mychart/ --set replicaCount=0 # Should fail schemahelm template
# Render templates locally
helm template myrelease mychart/
# Render with overrides to verify
helm template myrelease mychart/ -f values-production.yaml
# Debug mode (shows computed values)
helm template myrelease mychart/ --debug
# Render specific template
helm template myrelease mychart/ -s templates/deployment.yamlChecklist for New Values
| Check | Question |
|---|---|
| Documented? | Does the key have an inline comment? |
| Default works? | Can you helm install without overriding? |
| Type clear? | Is it obvious if this is string, int, bool, list, map? |
| Overridable? | Can it be set with --set? (avoid deeply nested) |
| No secrets? | Are default values free of passwords/tokens? |
| camelCase? | Does it follow Helm naming convention? |
| Flat enough? | Is nesting 3 levels or less? |
#!/usr/bin/env python3
"""
helm-chart-builder: Chart Analyzer
Static analysis of Helm chart directories for structural issues, template
anti-patterns, missing labels, hardcoded values, and security baseline checks.
Usage:
python scripts/chart_analyzer.py mychart/
python scripts/chart_analyzer.py mychart/ --output json
python scripts/chart_analyzer.py mychart/ --security
"""
import argparse
import json
import re
import sys
from pathlib import Path
# --- Analysis Rules ---
REQUIRED_FILES = [
{"path": "Chart.yaml", "severity": "critical", "message": "Missing Chart.yaml — not a valid Helm chart"},
{"path": "values.yaml", "severity": "high", "message": "Missing values.yaml — chart has no configurable defaults"},
{"path": "templates/_helpers.tpl", "severity": "high", "message": "Missing _helpers.tpl — no shared label/name helpers"},
{"path": "templates/NOTES.txt", "severity": "medium", "message": "Missing NOTES.txt — no post-install instructions for users"},
{"path": ".helmignore", "severity": "low", "message": "Missing .helmignore — CI files, .git, tests may be packaged"},
]
CHART_YAML_CHECKS = [
{"field": "apiVersion", "severity": "critical", "message": "Missing apiVersion in Chart.yaml"},
{"field": "name", "severity": "critical", "message": "Missing name in Chart.yaml"},
{"field": "version", "severity": "critical", "message": "Missing version in Chart.yaml"},
{"field": "description", "severity": "medium", "message": "Missing description in Chart.yaml"},
{"field": "appVersion", "severity": "medium", "message": "Missing appVersion in Chart.yaml — operators won't know what app version is deployed"},
{"field": "type", "severity": "low", "message": "Missing type in Chart.yaml — defaults to 'application'"},
]
TEMPLATE_ANTI_PATTERNS = [
{
"id": "TP001",
"severity": "high",
"pattern": r'image:\s*["\']?[a-z][a-z0-9./-]+:[a-z0-9][a-z0-9._-]*["\']?\s*$',
"message": "Hardcoded image tag in template — must use .Values.image.repository and .Values.image.tag",
"fix": 'Use: image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"',
},
{
"id": "TP002",
"severity": "high",
"pattern": r'replicas:\s*\d+\s*$',
"message": "Hardcoded replica count — must be configurable via values",
"fix": "Use: replicas: {{ .Values.replicaCount }}",
},
{
"id": "TP003",
"severity": "medium",
"pattern": r'port:\s*\d+\s*$',
"message": "Hardcoded port number — should be configurable via values",
"fix": "Use: port: {{ .Values.service.port }}",
},
{
"id": "TP004",
"severity": "high",
"pattern": r'(?:name|namespace):\s*[a-z][a-z0-9-]+\s*$',
"message": "Hardcoded name/namespace — should use template helpers",
"fix": 'Use: name: {{ include "mychart.fullname" . }}',
},
{
"id": "TP005",
"severity": "medium",
"pattern": r'nodePort:\s*\d+',
"message": "Hardcoded nodePort — should be configurable or avoided",
"fix": "Use: nodePort: {{ .Values.service.nodePort }} with conditional",
},
]
SECURITY_CHECKS = [
{
"id": "SC001",
"severity": "critical",
"check": "no_security_context",
"message": "No securityContext found in any template — pods run as root with full capabilities",
"fix": "Add pod and container securityContext with runAsNonRoot, readOnlyRootFilesystem, drop ALL capabilities",
},
{
"id": "SC002",
"severity": "critical",
"check": "privileged_container",
"message": "Privileged container detected — full host access",
"fix": "Remove privileged: true. Use specific capabilities instead",
},
{
"id": "SC003",
"severity": "high",
"check": "no_run_as_non_root",
"message": "No runAsNonRoot: true — container may run as root",
"fix": "Add runAsNonRoot: true to pod securityContext",
},
{
"id": "SC004",
"severity": "high",
"check": "no_readonly_rootfs",
"message": "No readOnlyRootFilesystem — container filesystem is writable",
"fix": "Add readOnlyRootFilesystem: true and use emptyDir for writable paths",
},
{
"id": "SC005",
"severity": "medium",
"check": "no_network_policy",
"message": "No NetworkPolicy template — all pod-to-pod traffic allowed",
"fix": "Add a NetworkPolicy template with default-deny ingress and explicit allow rules",
},
{
"id": "SC006",
"severity": "medium",
"check": "automount_sa_token",
"message": "automountServiceAccountToken not set to false — pod can access K8s API",
"fix": "Set automountServiceAccountToken: false unless the pod needs K8s API access",
},
{
"id": "SC007",
"severity": "high",
"check": "host_network",
"message": "hostNetwork: true — pod shares host network namespace",
"fix": "Remove hostNetwork unless absolutely required (e.g., CNI plugin)",
},
{
"id": "SC008",
"severity": "critical",
"check": "host_pid_ipc",
"message": "hostPID or hostIPC enabled — pod can see host processes/IPC",
"fix": "Remove hostPID and hostIPC — never needed in application charts",
},
]
LABEL_PATTERNS = [
r"app\.kubernetes\.io/name",
r"app\.kubernetes\.io/instance",
r"app\.kubernetes\.io/version",
r"app\.kubernetes\.io/managed-by",
r"helm\.sh/chart",
]
# --- Demo Chart ---
DEMO_CHART_YAML = """apiVersion: v2
name: demo-app
version: 0.1.0
"""
DEMO_VALUES_YAML = """replicaCount: 1
image:
repository: nginx
tag: latest
pullPolicy: Always
service:
type: ClusterIP
port: 80
"""
DEMO_DEPLOYMENT = """apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
spec:
replicas: 3
template:
spec:
containers:
- name: demo-app
image: nginx:1.25
ports:
- containerPort: 80
"""
def parse_yaml_simple(content):
"""Simple key-value parser for YAML (stdlib only)."""
result = {}
for line in content.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if ":" in stripped and not stripped.startswith("-"):
key, _, val = stripped.partition(":")
key = key.strip()
val = val.strip().strip("'\"")
if val:
result[key] = val
return result
def check_structure(chart_dir):
"""Check chart directory for required files."""
findings = []
for check in REQUIRED_FILES:
path = chart_dir / check["path"]
if not path.exists():
findings.append({
"id": "ST" + str(REQUIRED_FILES.index(check) + 1).zfill(3),
"severity": check["severity"],
"message": check["message"],
"fix": f"Create {check['path']}",
"file": check["path"],
})
return findings
def check_chart_yaml(chart_dir):
"""Validate Chart.yaml metadata."""
findings = []
chart_path = chart_dir / "Chart.yaml"
if not chart_path.exists():
return findings
content = chart_path.read_text(encoding="utf-8")
parsed = parse_yaml_simple(content)
for check in CHART_YAML_CHECKS:
if check["field"] not in parsed:
findings.append({
"id": "CY" + str(CHART_YAML_CHECKS.index(check) + 1).zfill(3),
"severity": check["severity"],
"message": check["message"],
"fix": f"Add '{check['field']}:' to Chart.yaml",
"file": "Chart.yaml",
})
# Check apiVersion value
if parsed.get("apiVersion") == "v1":
findings.append({
"id": "CY007",
"severity": "medium",
"message": "apiVersion: v1 is Helm 2 format — use v2 for Helm 3",
"fix": "Change apiVersion to v2",
"file": "Chart.yaml",
})
# Check version is semver
version = parsed.get("version", "")
if version and not re.match(r"^\d+\.\d+\.\d+", version):
findings.append({
"id": "CY008",
"severity": "high",
"message": f"Version '{version}' is not valid semver",
"fix": "Use semver format: MAJOR.MINOR.PATCH (e.g., 1.0.0)",
"file": "Chart.yaml",
})
return findings
def check_templates(chart_dir):
"""Scan templates for anti-patterns."""
findings = []
templates_dir = chart_dir / "templates"
if not templates_dir.exists():
return findings
template_files = list(templates_dir.glob("*.yaml")) + list(templates_dir.glob("*.yml")) + list(templates_dir.glob("*.tpl"))
all_content = ""
for tpl_file in template_files:
content = tpl_file.read_text(encoding="utf-8")
all_content += content + "\n"
rel_path = tpl_file.relative_to(chart_dir)
for rule in TEMPLATE_ANTI_PATTERNS:
# Skip patterns that would false-positive on template expressions
for match in re.finditer(rule["pattern"], content, re.MULTILINE):
line = match.group(0).strip()
# Skip if the line contains a template expression
if "{{" in line or "}}" in line:
continue
findings.append({
"id": rule["id"],
"severity": rule["severity"],
"message": rule["message"],
"fix": rule["fix"],
"file": str(rel_path),
"line": line[:80],
})
# Check for standard labels
helpers_file = templates_dir / "_helpers.tpl"
if helpers_file.exists():
helpers_content = helpers_file.read_text(encoding="utf-8")
for label_pattern in LABEL_PATTERNS:
if not re.search(label_pattern, helpers_content) and not re.search(label_pattern, all_content):
label_name = label_pattern.replace("\\.", ".")
findings.append({
"id": "LB001",
"severity": "high",
"message": f"Standard label '{label_name}' not found in helpers or templates",
"fix": f"Add {label_name} to the labels helper in _helpers.tpl",
"file": "templates/_helpers.tpl",
"line": "(label not found)",
})
# Check for resource limits
if "resources:" not in all_content and template_files:
findings.append({
"id": "TP006",
"severity": "critical",
"message": "No resource requests/limits in any template — pods can consume unlimited node resources",
"fix": "Add resources section: {{ toYaml .Values.resources | nindent 12 }}",
"file": "templates/",
"line": "(no resources block found)",
})
# Check for probes
if "livenessProbe" not in all_content and "readinessProbe" not in all_content and template_files:
has_deployment = any("Deployment" in f.read_text(encoding="utf-8") for f in template_files if f.suffix in (".yaml", ".yml"))
if has_deployment:
findings.append({
"id": "TP007",
"severity": "high",
"message": "No liveness/readiness probes — Kubernetes cannot detect unhealthy pods",
"fix": "Add livenessProbe and readinessProbe with configurable values",
"file": "templates/deployment.yaml",
"line": "(no probes found)",
})
return findings
def check_security(chart_dir):
"""Run security-focused checks."""
findings = []
templates_dir = chart_dir / "templates"
if not templates_dir.exists():
return findings
template_files = list(templates_dir.glob("*.yaml")) + list(templates_dir.glob("*.yml"))
all_content = ""
for tpl_file in template_files:
all_content += tpl_file.read_text(encoding="utf-8") + "\n"
for check in SECURITY_CHECKS:
triggered = False
if check["check"] == "no_security_context":
if "securityContext" not in all_content and template_files:
triggered = True
elif check["check"] == "privileged_container":
if re.search(r"privileged:\s*true", all_content):
triggered = True
elif check["check"] == "no_run_as_non_root":
if "securityContext" in all_content and "runAsNonRoot" not in all_content:
triggered = True
elif check["check"] == "no_readonly_rootfs":
if "securityContext" in all_content and "readOnlyRootFilesystem" not in all_content:
triggered = True
elif check["check"] == "no_network_policy":
np_file = templates_dir / "networkpolicy.yaml"
if not np_file.exists() and "NetworkPolicy" not in all_content:
triggered = True
elif check["check"] == "automount_sa_token":
if "automountServiceAccountToken" not in all_content and template_files:
triggered = True
elif check["check"] == "host_network":
if re.search(r"hostNetwork:\s*true", all_content):
triggered = True
elif check["check"] == "host_pid_ipc":
if re.search(r"host(?:PID|IPC):\s*true", all_content):
triggered = True
if triggered:
findings.append({
"id": check["id"],
"severity": check["severity"],
"message": check["message"],
"fix": check["fix"],
"file": "templates/",
})
# Check for secrets in values.yaml
values_path = chart_dir / "values.yaml"
if values_path.exists():
values_content = values_path.read_text(encoding="utf-8")
for match in re.finditer(r"^(\s*\S*(?:password|secret|token|apiKey|api_key)\s*:\s*)(\S+)", values_content, re.MULTILINE | re.IGNORECASE):
val = match.group(2).strip("'\"")
if val and val not in ("null", "~", '""', "''", "changeme", "CHANGEME", "TODO"):
findings.append({
"id": "SC009",
"severity": "critical",
"message": f"Potential secret in values.yaml default: {match.group(0).strip()[:60]}",
"fix": "Remove default secret values. Use empty string or null with documentation",
"file": "values.yaml",
"line": match.group(0).strip()[:80],
})
return findings
def analyze_chart(chart_dir, output_format="text", security_focus=False):
"""Run full chart analysis."""
findings = []
findings.extend(check_structure(chart_dir))
findings.extend(check_chart_yaml(chart_dir))
findings.extend(check_templates(chart_dir))
if security_focus:
findings.extend(check_security(chart_dir))
# Filter to security-relevant items only
security_ids = {"SC001", "SC002", "SC003", "SC004", "SC005", "SC006", "SC007", "SC008", "SC009"}
security_severities = {"critical", "high"}
findings = [f for f in findings if f["id"] in security_ids or f["severity"] in security_severities]
else:
findings.extend(check_security(chart_dir))
# Deduplicate
seen = set()
unique = []
for f in findings:
key = (f["id"], f.get("line", ""), f.get("file", ""))
if key not in seen:
seen.add(key)
unique.append(f)
findings = unique
# Sort by severity
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
findings.sort(key=lambda f: severity_order.get(f["severity"], 4))
# Score
deductions = {"critical": 25, "high": 15, "medium": 5, "low": 2}
score = max(0, 100 - sum(deductions.get(f["severity"], 0) for f in findings))
counts = {
"critical": sum(1 for f in findings if f["severity"] == "critical"),
"high": sum(1 for f in findings if f["severity"] == "high"),
"medium": sum(1 for f in findings if f["severity"] == "medium"),
"low": sum(1 for f in findings if f["severity"] == "low"),
}
# Chart metadata
chart_yaml_path = chart_dir / "Chart.yaml"
chart_meta = parse_yaml_simple(chart_yaml_path.read_text(encoding="utf-8")) if chart_yaml_path.exists() else {}
result = {
"score": score,
"chart_name": chart_meta.get("name", chart_dir.name),
"chart_version": chart_meta.get("version", "unknown"),
"app_version": chart_meta.get("appVersion", "unknown"),
"findings": findings,
"finding_counts": counts,
}
if output_format == "json":
print(json.dumps(result, indent=2))
return result
# Text output
print(f"\n{'=' * 60}")
print(f" Helm Chart Analysis Report")
print(f"{'=' * 60}")
print(f" Score: {score}/100")
print(f" Chart: {result['chart_name']} v{result['chart_version']}")
print(f" App Version: {result['app_version']}")
print()
print(f" Findings: {counts['critical']} critical | {counts['high']} high | {counts['medium']} medium | {counts['low']} low")
print(f"{'─' * 60}")
for f in findings:
icon = {"critical": "!!!", "high": "!!", "medium": "!", "low": "~"}.get(f["severity"], "?")
print(f"\n [{f['id']}] {icon} {f['severity'].upper()}")
print(f" {f['message']}")
if "file" in f:
print(f" File: {f['file']}")
if "line" in f:
print(f" Line: {f['line']}")
print(f" Fix: {f['fix']}")
if not findings:
print("\n No issues found. Chart looks good.")
print(f"\n{'=' * 60}\n")
return result
def run_demo():
"""Run analysis on demo chart data."""
import tempfile
import os
with tempfile.TemporaryDirectory() as tmpdir:
chart_dir = Path(tmpdir) / "demo-app"
chart_dir.mkdir()
(chart_dir / "Chart.yaml").write_text(DEMO_CHART_YAML)
(chart_dir / "values.yaml").write_text(DEMO_VALUES_YAML)
templates_dir = chart_dir / "templates"
templates_dir.mkdir()
(templates_dir / "deployment.yaml").write_text(DEMO_DEPLOYMENT)
return chart_dir, analyze_chart
def main():
parser = argparse.ArgumentParser(
description="helm-chart-builder: Helm chart static analyzer"
)
parser.add_argument("chartdir", nargs="?", help="Path to Helm chart directory (omit for demo)")
parser.add_argument(
"--output", "-o",
choices=["text", "json"],
default="text",
help="Output format (default: text)",
)
parser.add_argument(
"--security",
action="store_true",
help="Security-focused analysis only",
)
args = parser.parse_args()
if args.chartdir:
chart_dir = Path(args.chartdir)
if not chart_dir.is_dir():
print(f"Error: Not a directory: {args.chartdir}", file=sys.stderr)
sys.exit(1)
analyze_chart(chart_dir, args.output, args.security)
else:
print("No chart directory provided. Running demo analysis...\n")
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
chart_dir = Path(tmpdir) / "demo-app"
chart_dir.mkdir()
(chart_dir / "Chart.yaml").write_text(DEMO_CHART_YAML)
(chart_dir / "values.yaml").write_text(DEMO_VALUES_YAML)
templates_dir = chart_dir / "templates"
templates_dir.mkdir()
(templates_dir / "deployment.yaml").write_text(DEMO_DEPLOYMENT)
analyze_chart(chart_dir, args.output, args.security)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
helm-chart-builder: Values Validator
Validate values.yaml files against Helm best practices — documentation coverage,
type consistency, naming conventions, default quality, and security.
Usage:
python scripts/values_validator.py values.yaml
python scripts/values_validator.py values.yaml --output json
python scripts/values_validator.py values.yaml --strict
"""
import argparse
import json
import re
import sys
from pathlib import Path
# --- Demo values.yaml ---
DEMO_VALUES = """# Default values for demo-app
replicaCount: 1
image:
repository: nginx
tag: latest
pullPolicy: Always
service:
type: ClusterIP
port: 80
ingress:
enabled: false
resources: {}
PASSWORD: supersecret123
db_password: changeme
api-key: sk-12345
deeply:
nested:
structure:
that:
goes:
too:
deep: true
undocumented_value: something
AnotherValue: 42
snake_case_key: bad
"""
# --- Validation Rules ---
NAMING_PATTERN = re.compile(r"^[a-z][a-zA-Z0-9]*$") # camelCase
SNAKE_CASE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(_[a-z0-9]+)+$") # snake_case
UPPER_CASE_PATTERN = re.compile(r"^[A-Z]") # Starts with uppercase
SECRET_KEY_PATTERNS = [
re.compile(r"(?:password|secret|token|apiKey|api_key|api-key|private_key|credentials)", re.IGNORECASE),
]
KNOWN_STRUCTURES = {
"image": ["repository", "tag", "pullPolicy"],
"service": ["type", "port"],
"ingress": ["enabled"],
"resources": [],
"serviceAccount": ["create", "name"],
"autoscaling": ["enabled", "minReplicas", "maxReplicas"],
}
def parse_values(content):
"""Parse values.yaml into structured data with metadata.
Returns a list of entries with key paths, values, depth, and comment info.
"""
entries = []
key_stack = []
indent_stack = [0]
prev_comment = None
for line_num, line in enumerate(content.splitlines(), 1):
stripped = line.strip()
# Track comments for documentation coverage
if stripped.startswith("#"):
prev_comment = stripped
continue
if not stripped:
prev_comment = None
continue
indent = len(line) - len(line.lstrip())
# Pop stack for dedented lines
while len(indent_stack) > 1 and indent <= indent_stack[-1]:
indent_stack.pop()
if key_stack:
key_stack.pop()
# Parse key: value
match = re.match(r"^(\S+)\s*:\s*(.*)", stripped)
if match and not stripped.startswith("-"):
key = match.group(1)
raw_value = match.group(2).strip()
# Check for inline comment
inline_comment = None
if "#" in raw_value:
val_part, _, comment_part = raw_value.partition("#")
raw_value = val_part.strip()
inline_comment = comment_part.strip()
# Build full key path
full_path = ".".join(key_stack + [key])
depth = len(key_stack) + 1
# Determine value type
value_type = "unknown"
if not raw_value or raw_value == "":
value_type = "map"
key_stack.append(key)
indent_stack.append(indent)
elif raw_value in ("true", "false"):
value_type = "boolean"
elif raw_value == "null" or raw_value == "~":
value_type = "null"
elif raw_value == "{}":
value_type = "empty_map"
elif raw_value == "[]":
value_type = "empty_list"
elif re.match(r"^-?\d+$", raw_value):
value_type = "integer"
elif re.match(r"^-?\d+\.\d+$", raw_value):
value_type = "float"
elif raw_value.startswith('"') or raw_value.startswith("'"):
value_type = "string"
else:
value_type = "string"
has_doc = prev_comment is not None or inline_comment is not None
entries.append({
"key": key,
"full_path": full_path,
"value": raw_value,
"value_type": value_type,
"depth": depth,
"line": line_num,
"has_documentation": has_doc,
"comment": prev_comment or inline_comment,
})
prev_comment = None
else:
prev_comment = None
return entries
def validate_naming(entries):
"""Check key naming conventions."""
findings = []
for entry in entries:
key = entry["key"]
# Skip map entries (they're parent keys)
if entry["value_type"] == "map":
# Parent keys should still be camelCase
pass
if SNAKE_CASE_PATTERN.match(key):
findings.append({
"severity": "medium",
"category": "naming",
"message": f"Key '{entry['full_path']}' uses snake_case — Helm convention is camelCase",
"fix": f"Rename to camelCase: {to_camel_case(key)}",
"line": entry["line"],
})
elif UPPER_CASE_PATTERN.match(key) and not key.isupper():
findings.append({
"severity": "medium",
"category": "naming",
"message": f"Key '{entry['full_path']}' starts with uppercase — use camelCase",
"fix": f"Rename: {key[0].lower() + key[1:]}",
"line": entry["line"],
})
elif "-" in key:
findings.append({
"severity": "medium",
"category": "naming",
"message": f"Key '{entry['full_path']}' uses kebab-case — Helm convention is camelCase",
"fix": f"Rename to camelCase: {to_camel_case(key)}",
"line": entry["line"],
})
return findings
def validate_documentation(entries):
"""Check documentation coverage."""
findings = []
total = len(entries)
documented = sum(1 for e in entries if e["has_documentation"])
if total > 0:
coverage = (documented / total) * 100
if coverage < 50:
findings.append({
"severity": "high",
"category": "documentation",
"message": f"Only {coverage:.0f}% of values have comments ({documented}/{total})",
"fix": "Add inline YAML comments explaining purpose, type, and valid options for each value",
"line": 0,
})
elif coverage < 80:
findings.append({
"severity": "medium",
"category": "documentation",
"message": f"{coverage:.0f}% documentation coverage ({documented}/{total}) — aim for 80%+",
"fix": "Add comments for undocumented values",
"line": 0,
})
# Flag specific undocumented top-level keys
for entry in entries:
if entry["depth"] == 1 and not entry["has_documentation"]:
findings.append({
"severity": "low",
"category": "documentation",
"message": f"Top-level key '{entry['key']}' has no comment",
"fix": f"Add a comment above '{entry['key']}' explaining its purpose",
"line": entry["line"],
})
return findings
def validate_defaults(entries):
"""Check default value quality."""
findings = []
for entry in entries:
# Check for :latest tag
if entry["key"] == "tag" and entry["value"] in ("latest", '"latest"', "'latest'"):
findings.append({
"severity": "high",
"category": "defaults",
"message": f"image.tag defaults to 'latest' — not reproducible",
"fix": "Use a specific version tag or reference .Chart.AppVersion in template",
"line": entry["line"],
})
# Check pullPolicy
if entry["key"] == "pullPolicy" and entry["value"] in ("Always", '"Always"', "'Always'"):
findings.append({
"severity": "low",
"category": "defaults",
"message": "imagePullPolicy defaults to 'Always' — 'IfNotPresent' is better for production",
"fix": "Change default to IfNotPresent (Always is appropriate for :latest only)",
"line": entry["line"],
})
# Check empty resources
if entry["key"] == "resources" and entry["value_type"] == "empty_map":
findings.append({
"severity": "medium",
"category": "defaults",
"message": "resources defaults to {} — no requests or limits set",
"fix": "Provide default resource requests (e.g., cpu: 100m, memory: 128Mi)",
"line": entry["line"],
})
return findings
def validate_secrets(entries):
"""Check for secrets in default values."""
findings = []
for entry in entries:
for pattern in SECRET_KEY_PATTERNS:
if pattern.search(entry["full_path"]):
val = entry["value"].strip("'\"")
if val and val not in ("", "null", "~", "{}", "[]", "changeme", "CHANGEME", "TODO", '""', "''"):
findings.append({
"severity": "critical",
"category": "security",
"message": f"Potential secret with default value: {entry['full_path']} = {val[:30]}...",
"fix": "Remove default. Use empty string, null, or 'changeme' placeholder with comment",
"line": entry["line"],
})
break
return findings
def validate_depth(entries):
"""Check nesting depth."""
findings = []
max_depth = max((e["depth"] for e in entries), default=0)
if max_depth > 4:
deep_entries = [e for e in entries if e["depth"] > 4]
for entry in deep_entries[:3]: # Report first 3
findings.append({
"severity": "medium",
"category": "structure",
"message": f"Deeply nested key ({entry['depth']} levels): {entry['full_path']}",
"fix": "Flatten structure — max 3-4 levels deep for usability",
"line": entry["line"],
})
return findings
def to_camel_case(name):
"""Convert snake_case or kebab-case to camelCase."""
parts = re.split(r"[-_]", name)
return parts[0].lower() + "".join(p.capitalize() for p in parts[1:])
def generate_report(content, output_format="text", strict=False):
"""Generate full validation report."""
entries = parse_values(content)
findings = []
findings.extend(validate_naming(entries))
findings.extend(validate_documentation(entries))
findings.extend(validate_defaults(entries))
findings.extend(validate_secrets(entries))
findings.extend(validate_depth(entries))
if strict:
# Elevate medium to high, low to medium
for f in findings:
if f["severity"] == "medium":
f["severity"] = "high"
elif f["severity"] == "low":
f["severity"] = "medium"
# Sort by severity
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
findings.sort(key=lambda f: severity_order.get(f["severity"], 4))
# Score
deductions = {"critical": 25, "high": 15, "medium": 5, "low": 2}
score = max(0, 100 - sum(deductions.get(f["severity"], 0) for f in findings))
counts = {
"critical": sum(1 for f in findings if f["severity"] == "critical"),
"high": sum(1 for f in findings if f["severity"] == "high"),
"medium": sum(1 for f in findings if f["severity"] == "medium"),
"low": sum(1 for f in findings if f["severity"] == "low"),
}
# Stats
total_keys = len(entries)
documented = sum(1 for e in entries if e["has_documentation"])
max_depth = max((e["depth"] for e in entries), default=0)
result = {
"score": score,
"total_keys": total_keys,
"documented_keys": documented,
"documentation_coverage": f"{(documented / total_keys * 100):.0f}%" if total_keys > 0 else "N/A",
"max_depth": max_depth,
"findings": findings,
"finding_counts": counts,
}
if output_format == "json":
print(json.dumps(result, indent=2))
return result
# Text output
print(f"\n{'=' * 60}")
print(f" Values.yaml Validation Report")
print(f"{'=' * 60}")
print(f" Score: {score}/100")
print(f" Keys: {total_keys} | Documented: {documented} ({result['documentation_coverage']})")
print(f" Max Depth: {max_depth}")
print()
print(f" Findings: {counts['critical']} critical | {counts['high']} high | {counts['medium']} medium | {counts['low']} low")
print(f"{'─' * 60}")
for f in findings:
icon = {"critical": "!!!", "high": "!!", "medium": "!", "low": "~"}.get(f["severity"], "?")
print(f"\n {icon} {f['severity'].upper()} [{f['category']}]")
print(f" {f['message']}")
if f.get("line", 0) > 0:
print(f" Line: {f['line']}")
print(f" Fix: {f['fix']}")
if not findings:
print("\n No issues found. Values file looks good.")
print(f"\n{'=' * 60}\n")
return result
def main():
parser = argparse.ArgumentParser(
description="helm-chart-builder: values.yaml best-practice validator"
)
parser.add_argument("valuesfile", nargs="?", help="Path to values.yaml (omit for demo)")
parser.add_argument(
"--output", "-o",
choices=["text", "json"],
default="text",
help="Output format (default: text)",
)
parser.add_argument(
"--strict",
action="store_true",
help="Strict mode — elevate warnings to higher severity",
)
args = parser.parse_args()
if args.valuesfile:
path = Path(args.valuesfile)
if not path.exists():
print(f"Error: File not found: {args.valuesfile}", file=sys.stderr)
sys.exit(1)
content = path.read_text(encoding="utf-8")
else:
print("No values file provided. Running demo validation...\n")
content = DEMO_VALUES
generate_report(content, args.output, args.strict)
if __name__ == "__main__":
main()
Related skills
How it compares
Use helm-chart-builder when the deliverable is Helm chart scaffolding; use general DevOps skills when the task is CI pipeline wiring without Kubernetes packaging.
FAQ
What files does helm-chart-builder generate?
helm-chart-builder documents a minimal production chart with Chart.yaml, values.yaml, .helmignore, _helpers.tpl, deployment.yaml, service.yaml, serviceaccount.yaml, NOTES.txt, and tests/test-connection.yaml. The full layout adds ingress, HPA, PDB, NetworkPolicy, ConfigMap, Secret
When should developers use helm-chart-builder?
Developers should use helm-chart-builder when packaging a service for Kubernetes with Helm instead of raw YAML. The skill targets repeatable cluster releases with helpers, tests, and optional schema validation—not application business logic.
Is Helm Chart Builder safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.