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.

install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/testing-security/helm/SKILL.md
source content

Helm — 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

Query 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 in
    crds/
    are installed once on first install
  • 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
--atomic
--rollback-on-failure
Auto-rollback on failed install/upgrade
--force
--force-replace
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

ErrorCauseFix
nil pointer evaluating interface {}
Accessing nested key on nil parentUse
dig
or
((.Values.x).y)
safe access
schema validation failed
values.schema.json rejects inputRun
helm lint
with your values files to see which field fails
CRDs not updated on upgrade
crds/
directory is install-only
Move CRDs to templates with pre-upgrade hooks
lookup
returns empty in CI
Client-side dry-run has no clusterUse
--dry-run=server
or guard with
{{- if not .Release.IsInstall }}
Post-renderer output invalidScript must read stdin, write stdoutEnsure script outputs valid YAML manifests to stdout
Subchart values ignoredWrong nesting in parent values.yamlValues must be under subchart name key:
postgresql.auth.password

References

  • troubleshooting.md
    — Install and upgrade failures, template rendering, release management, and repository issues

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