Awesome-omni-skill helm
Helm chart authoring — library charts, values.schema.json, subcharts, _helpers.tpl, helm-unittest. Use when building complex Helm charts. NOT for basic helm install/upgrade.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/testing-security/helm" ~/.claude/skills/diegosouzapw-awesome-omni-skill-helm-f21782 && rm -rf "$T"
skills/testing-security/helm/SKILL.mdHelm — Advanced Patterns
Beyond basics — patterns that trip up experienced engineers and aren't well-captured in training data.
Docs: https://helm.sh/docs/ | Sprig: https://masterminds.github.io/sprig/
Library Charts
Shared template logic across multiple charts. Not deployable on their own.
# Chart.yaml apiVersion: v2 name: mylib type: library # Not "application" version: 0.1.0
Merge Utility Pattern
The canonical way to let consumers override library-defined resources:
# mylib/templates/_util.tpl {{- define "mylib.util.merge" -}} {{- $top := first . -}} {{- $overrides := fromYaml (include (index . 1) $top) | default (dict ) -}} {{- $base := fromYaml (include (index . 2) $top) | default (dict ) -}} {{- toYaml (merge $overrides $base) -}} {{- end -}}
Consumer chart uses it to override specific fields while inheriting defaults:
# mychart/templates/deployment.yaml {{- include "mylib.util.merge" (list . "mychart.deployment.overrides" "mylib.deployment") }} # mychart/templates/_overrides.tpl {{- define "mychart.deployment.overrides" -}} spec: replicas: {{ .Values.replicaCount }} {{- end -}}
Shared Label/Annotation Templates
# mylib/templates/_labels.tpl {{- define "mylib.labels" -}} app.kubernetes.io/name: {{ .Chart.Name }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 }} {{- end -}} {{- define "mylib.selectorLabels" -}} app.kubernetes.io/name: {{ .Chart.Name }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}}
Values Schema Validation
values.schema.json validates values at install/upgrade/lint/template time. Catches config errors before they hit the cluster.
{ "$schema": "https://json-schema.org/draft-07/schema#", "type": "object", "required": ["image", "service"], "properties": { "replicaCount": { "type": "integer", "minimum": 1, "maximum": 50 }, "image": { "type": "object", "required": ["repository"], "properties": { "repository": { "type": "string", "minLength": 1 }, "tag": { "type": "string" }, "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } }, "additionalProperties": false }, "resources": { "type": "object", "properties": { "requests": { "$ref": "#/definitions/resourceBlock" }, "limits": { "$ref": "#/definitions/resourceBlock" } } } }, "definitions": { "resourceBlock": { "type": "object", "properties": { "cpu": { "type": "string", "pattern": "^[0-9]+m?$" }, "memory": { "type": "string", "pattern": "^[0-9]+(Mi|Gi)$" } } } } }
Generate from values files:
helm plugin install https://github.com/losisin/helm-values-schema-json && helm schema -input values.yaml
The lookup
Function
lookupQuery live cluster state during rendering. Returns empty dict in
--dry-run=client, works with --dry-run=server.
# Only create Secret if it doesn't already exist (preserve generated passwords across upgrades) {{- if not (lookup "v1" "Secret" .Release.Namespace "myapp-credentials") }} apiVersion: v1 kind: Secret metadata: name: myapp-credentials type: Opaque data: password: {{ randAlphaNum 32 | b64enc | quote }} {{- end }}
# Iterate over existing namespaces to deploy NetworkPolicies {{- range (lookup "v1" "Namespace" "" "").items }} {{- if hasKey .metadata.labels "app.kubernetes.io/managed-by" }} # ... generate NetworkPolicy for {{ .metadata.name }} {{- end }} {{- end }}
Gotcha:
lookup returns nil in dry-run client mode. Guard with {{- if .Release.IsInstall }} or use --dry-run=server.
Template Pitfalls & Solutions
Nil Map Panics
# WRONG — panics if .Values.ingress is nil {{- if .Values.ingress.enabled }} # CORRECT — safe navigation {{- if ((.Values.ingress).enabled) }} # Also works with dig (Sprig) {{- if (dig "ingress" "enabled" false .Values) }}
Whitespace Control
# Produces clean YAML without extra blank lines {{- if .Values.podAnnotations }} annotations: {{- toYaml .Values.podAnnotations | nindent 8 }} {{- end }}
- on {{- trims left whitespace; -}} trims right. Without them, blank lines accumulate.
Type Coercion
# Values from --set are always strings. "true" != true # Use quote + explicit type handling {{- if eq (toString .Values.debug) "true" }} # Or in schema, coerce with: {"type": ["boolean", "string"]}
Range Scope
# WRONG — $ is the global scope, . changes inside range {{- range .Values.hosts }} host: {{ . }} service: {{ $.Values.serviceName }} # Use $ to access root {{- end }}
CRD Management
Two approaches, both with tradeoffs:
crds/
Directory (Helm 3)
crds/- CRDs in
are installed once on first installcrds/ - Never updated or deleted by Helm (by design)
- Simple but no lifecycle management
Hook-Based CRDs
# templates/crds.yaml {{- if .Values.installCRDs }} apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: "helm.sh/hook": pre-install,pre-upgrade "helm.sh/hook-weight": "-5" name: myresources.example.com spec: # ... {{- end }}
Gives upgrade control but risks deleting CRDs (and all CRs) on uninstall. Add
"helm.sh/resource-policy": keep annotation.
Post-Renderers
Mutate rendered manifests before apply. Useful for injecting sidecars, labels, or policy annotations without modifying chart templates.
⚠️ Helm 4 breaking change: Post-renderers must be installed Helm plugins — passing an executable path (
--post-renderer ./script.sh) no longer works. Use --post-renderer <plugin-name> with a properly installed plugin.
Helm 3 (executable path — still works in Helm 3.x):
helm upgrade --install myapp ./mychart --post-renderer ./inject-labels.sh
#!/bin/bash # inject-labels.sh — add labels via kustomize cat > /tmp/kustomization.yaml <<EOF apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - rendered.yaml commonLabels: company.io/cost-center: platform EOF cat <&0 > /tmp/rendered.yaml kustomize build /tmp
Helm 4 (plugin name — required in Helm 4, also works in Helm 3.14+):
# Install a post-renderer plugin, then reference it by name helm plugin install https://github.com/example/helm-post-renderer-kustomize helm upgrade --install myapp ./mychart --post-renderer kustomize
See Helm 4 plugin system (HIP-0026) and example plugins for WebAssembly-based post-renderer plugins.
Multi-Tenant / Subchart Patterns
Global Values
# Parent values.yaml global: imageRegistry: 123456789.dkr.ecr.us-east-1.amazonaws.com storageClass: longhorn # Accessible in subcharts as .Values.global.imageRegistry
Conditional Subcharts
# Chart.yaml dependencies: - name: postgresql version: "15.x" repository: "oci://registry-1.docker.io/bitnamicharts" condition: postgresql.enabled import-values: - child: primary.service parent: database
import-values pulls subchart values into parent scope — avoids deeply nested postgresql.primary.service.port paths.
Per-Environment Overrides
# Layered values: base → environment → secrets helm upgrade --install myapp ./mychart \ -f values.yaml \ -f values-production.yaml \ -f secrets-production.yaml.dec \ --set image.tag=${GIT_SHA}
Secrets Handling
Charts should never contain secrets. Integration patterns:
# External Secrets Operator — reference in templates apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: {{ include "myapp.fullname" . }}-db spec: refreshInterval: 1h secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: {{ include "myapp.fullname" . }}-db data: - secretKey: password remoteRef: key: {{ .Values.externalSecrets.dbPasswordKey }}
# SOPS-encrypted values (decrypt before helm) # values-secrets.yaml.enc → decrypt with sops helm secrets upgrade myapp ./mychart -f values.yaml -f values-secrets.yaml
Testing with helm-unittest
# tests/deployment_test.yaml suite: deployment templates: - templates/deployment.yaml tests: - it: should not render when disabled set: enabled: false asserts: - hasDocuments: count: 0 - it: should set resource limits from values set: resources: limits: memory: 1Gi asserts: - equal: path: spec.template.spec.containers[0].resources.limits.memory value: 1Gi - it: should fail without required values set: image.repository: "" asserts: - failedTemplate: {}
helm plugin install https://github.com/helm-unittest/helm-unittest helm unittest ./mychart --color
Helm 4 CLI Flag Renames
Helm 4 (released November 2025) renamed two common flags. The old names still work but emit deprecation warnings — update scripts and CI pipelines:
| Old flag (Helm 3) | New flag (Helm 4) | Notes |
|---|---|---|
| | Auto-rollback on failed install/upgrade |
| | Delete and re-create resources on conflict |
# Helm 4 — use new flag names helm upgrade --install myapp ./mychart --rollback-on-failure --timeout 10m helm upgrade myapp ./mychart --force-replace
Debugging
# Render one template with debug output helm template myapp ./mychart -s templates/deployment.yaml --debug 2>&1 # Diff what would change (requires helm-diff plugin) helm diff upgrade myapp ./mychart -f values-production.yaml # Validate against cluster without applying helm upgrade --install myapp ./mychart --dry-run=server --debug # Check computed values after all overlays helm get values myapp -n production -a # -a = all (computed)
Common Advanced Errors
| Error | Cause | Fix |
|---|---|---|
| Accessing nested key on nil parent | Use or safe access |
| values.schema.json rejects input | Run with your values files to see which field fails |
| CRDs not updated on upgrade | directory is install-only | Move CRDs to templates with pre-upgrade hooks |
returns empty in CI | Client-side dry-run has no cluster | Use or guard with |
| Post-renderer output invalid | Script must read stdin, write stdout | Ensure script outputs valid YAML manifests to stdout |
| Subchart values ignored | Wrong nesting in parent values.yaml | Values must be under subchart name key: |
References
— Install and upgrade failures, template rendering, release management, and repository issuestroubleshooting.md
Cross-References
- argocd — GitOps deployment of Helm releases via ArgoCD Applications
- crossplane — Helm provider for deploying charts as Crossplane managed resources
- external-secrets — Inject secrets into Helm releases without embedding them in values
- cert-manager — TLS certificate management commonly deployed via Helm
- kueue — Job queueing — Helm charts for ML workloads often need Kueue integration
- operator-sdk — Building operators that use Helm charts as the reconciliation engine
- github-actions — CI/CD for Helm chart linting, packaging, and publishing
- docker-buildx — Build container images referenced by Helm charts
- prometheus-grafana — kube-prometheus-stack deployed via Helm
- gpu-operator — GPU Operator deployed and configured via Helm