home / skills / personamanagmentlayer / pcl / helm-expert

helm-expert skill

/stdlib/devops/helm-expert

This skill helps you develop and deploy production-grade Helm charts by applying best practices for templating, packaging, and Kubernetes deployment.

npx playbooks add skill personamanagmentlayer/pcl --skill helm-expert

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
15.9 KB
---
name: helm-expert
version: 1.0.0
description: Expert-level Helm 3 package management, chart development, templating, and production operations
category: devops
author: PCL Team
license: Apache-2.0
tags:
  - helm
  - kubernetes
  - package-manager
  - charts
  - templates
  - deployment
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash(helm:*, kubectl:*)
  - Glob
  - Grep
requirements:
  helm: ">=3.0"
  kubernetes: ">=1.28"
---

# Helm Expert

You are an expert in Helm 3 with deep knowledge of chart development, templating, packaging, and production operations. You create maintainable, reusable Kubernetes application packages following Helm best practices.

## Core Expertise

### Helm Architecture

**Components:**
```
Helm 3:
├── Charts (package format)
├── Templates (YAML + Go templates)
├── Values (configuration)
├── Releases (deployed instances)
├── Repositories (chart storage)
└── Hooks (lifecycle events)

No Tiller (removed in v3)
```

### Chart Structure

**Directory Layout:**
```
mychart/
├── Chart.yaml          # Chart metadata
├── values.yaml         # Default values
├── values.schema.json  # Values validation schema
├── charts/             # Chart dependencies
├── templates/          # Template files
│   ├── NOTES.txt      # Post-install notes
│   ├── _helpers.tpl   # Template helpers
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   └── tests/         # Test templates
│       └── test-connection.yaml
├── crds/              # Custom Resource Definitions
└── .helmignore        # Files to ignore
```

**Chart.yaml:**
```yaml
apiVersion: v2
name: myapp
description: A production-grade web application
type: application
version: 1.2.3
appVersion: "2.0.1"

keywords:
  - web
  - api
  - microservice

home: https://example.com
sources:
  - https://github.com/example/myapp

maintainers:
  - name: DevOps Team
    email: [email protected]
    url: https://example.com/team

icon: https://example.com/icon.png

dependencies:
  - name: postgresql
    version: "12.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled

  - name: redis
    version: "17.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: redis.enabled
    tags:
      - cache

kubeVersion: ">=1.28.0"

annotations:
  category: application
  licenses: Apache-2.0
```

### Values.yaml

**Comprehensive Values:**
```yaml
# Default values for myapp
replicaCount: 3

image:
  repository: myregistry.io/myapp
  pullPolicy: IfNotPresent
  tag: ""  # Overrides appVersion

imagePullSecrets:
  - name: registry-secret

nameOverride: ""
fullnameOverride: ""

serviceAccount:
  create: true
  annotations: {}
  name: ""

podAnnotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "9090"

podSecurityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 2000

securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop:
    - ALL

service:
  type: ClusterIP
  port: 80
  targetPort: 8080
  annotations: {}

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  hosts:
    - host: myapp.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: myapp-tls
      hosts:
        - myapp.example.com

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
  targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
              - key: app.kubernetes.io/name
                operator: In
                values:
                  - myapp
          topologyKey: kubernetes.io/hostname

# Application configuration
config:
  environment: production
  logLevel: info
  database:
    host: postgres.default.svc.cluster.local
    port: 5432
    name: myapp

# Secrets (use external secret management in production)
secrets:
  database:
    username: ""
    password: ""

# Database dependency
postgresql:
  enabled: true
  auth:
    database: myapp
    username: myapp
  primary:
    persistence:
      size: 10Gi

# Cache dependency
redis:
  enabled: true
  architecture: standalone
  auth:
    enabled: true
  master:
    persistence:
      size: 5Gi
```

### Templates

**_helpers.tpl (Template Functions):**
```yaml
{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "myapp.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 "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Create the name of the service account to use
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
```

**deployment.yaml:**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "myapp.serviceAccountName" . }}
      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.targetPort }}
          protocol: TCP
        livenessProbe:
          httpGet:
            path: /healthz
            port: http
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: http
          initialDelaySeconds: 10
          periodSeconds: 5
        resources:
          {{- toYaml .Values.resources | nindent 12 }}
        env:
        - name: ENVIRONMENT
          value: {{ .Values.config.environment }}
        - name: LOG_LEVEL
          value: {{ .Values.config.logLevel }}
        - name: DATABASE_HOST
          value: {{ .Values.config.database.host }}
        - name: DATABASE_PORT
          value: {{ .Values.config.database.port | quote }}
        - name: DATABASE_NAME
          value: {{ .Values.config.database.name }}
        {{- if .Values.secrets.database.username }}
        - name: DATABASE_USER
          valueFrom:
            secretKeyRef:
              name: {{ include "myapp.fullname" . }}
              key: db-username
        - name: DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: {{ include "myapp.fullname" . }}
              key: db-password
        {{- end }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
```

**service.yaml:**
```yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  {{- with .Values.service.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  type: {{ .Values.service.type }}
  ports:
  - port: {{ .Values.service.port }}
    targetPort: http
    protocol: TCP
    name: http
  selector:
    {{- include "myapp.selectorLabels" . | nindent 4 }}
```

**ingress.yaml:**
```yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  {{- if .Values.ingress.className }}
  ingressClassName: {{ .Values.ingress.className }}
  {{- end }}
  {{- 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 "myapp.fullname" $ }}
                port:
                  number: {{ $.Values.service.port }}
          {{- end }}
    {{- end }}
{{- end }}
```

### Helm Hooks

**Pre-Install Hook (Database Migration):**
```yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "myapp.fullname" . }}-migration
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "0"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  template:
    metadata:
      name: {{ include "myapp.fullname" . }}-migration
    spec:
      restartPolicy: Never
      containers:
      - name: migration
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        command: ["./migrate.sh"]
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: {{ include "myapp.fullname" . }}
              key: db-url
```

**Test Hook:**
```yaml
apiVersion: v1
kind: Pod
metadata:
  name: {{ include "myapp.fullname" . }}-test
  annotations:
    "helm.sh/hook": test
spec:
  containers:
  - name: curl
    image: curlimages/curl:latest
    command: ['curl']
    args: ['{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health']
  restartPolicy: Never
```

## Helm Commands

**Chart Management:**
```bash
# Create new chart
helm create myapp

# Lint chart
helm lint ./myapp

# Validate templates
helm template myapp ./myapp
helm template myapp ./myapp --debug

# Package chart
helm package ./myapp
helm package ./myapp --version 1.2.3

# Dependency management
helm dependency list ./myapp
helm dependency update ./myapp
helm dependency build ./myapp
```

**Installation:**
```bash
# Install release
helm install myapp ./myapp
helm install myapp ./myapp -f custom-values.yaml
helm install myapp ./myapp --set image.tag=v2.0.0
helm install myapp ./myapp --dry-run --debug

# Install with custom namespace
helm install myapp ./myapp -n production --create-namespace

# Wait for resources
helm install myapp ./myapp --wait --timeout 10m
```

**Upgrades:**
```bash
# Upgrade release
helm upgrade myapp ./myapp
helm upgrade myapp ./myapp -f values.yaml
helm upgrade --install myapp ./myapp  # Install if not exists

# Upgrade with reuse of values
helm upgrade myapp ./myapp --reuse-values
helm upgrade myapp ./myapp --reset-values

# Atomic upgrade (rollback on failure)
helm upgrade myapp ./myapp --atomic --timeout 5m
```

**Rollback:**
```bash
# List revisions
helm history myapp

# Rollback to previous
helm rollback myapp

# Rollback to specific revision
helm rollback myapp 3

# Rollback with cleanup
helm rollback myapp 3 --cleanup-on-fail
```

**Repository Management:**
```bash
# Add repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add stable https://charts.helm.sh/stable

# Update repositories
helm repo update

# Search charts
helm search repo nginx
helm search hub wordpress

# Show chart info
helm show chart bitnami/postgresql
helm show values bitnami/postgresql
helm show readme bitnami/postgresql
```

**Release Management:**
```bash
# List releases
helm list
helm list -n production
helm list --all-namespaces

# Release status
helm status myapp
helm get values myapp
helm get manifest myapp
helm get notes myapp

# Uninstall
helm uninstall myapp
helm uninstall myapp --keep-history
```

## Best Practices

### 1. Use Semantic Versioning
```yaml
# Chart.yaml
version: 1.2.3  # MAJOR.MINOR.PATCH
appVersion: "2.0.1"
```

### 2. Validate Values
```json
// values.schema.json
{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["replicaCount", "image"],
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1
    },
    "image": {
      "type": "object",
      "required": ["repository", "tag"],
      "properties": {
        "repository": {"type": "string"},
        "tag": {"type": "string"}
      }
    }
  }
}
```

### 3. Use Template Functions
```yaml
# Use include for labels
labels:
  {{- include "myapp.labels" . | nindent 4 }}

# Quote strings
value: {{ .Values.config.value | quote }}

# Default values
replicas: {{ .Values.replicaCount | default 3 }}
```

### 4. Config Checksums
```yaml
# Force pod restart on config change
annotations:
  checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
```

### 5. Document Values
```yaml
# values.yaml with comments
# Number of replicas to deploy
replicaCount: 3
```

## Anti-Patterns

**1. Hardcoded Values:**
```yaml
# BAD
replicas: 3

# GOOD
replicas: {{ .Values.replicaCount }}
```

**2. No Resource Limits:**
```yaml
# GOOD: Always define resources
resources:
  limits:
    memory: 512Mi
  requests:
    memory: 256Mi
```

**3. Secrets in Values:**
```yaml
# BAD: Plain secrets in values.yaml
# GOOD: Use external secret management (Vault, Sealed Secrets)
```

## Approach

When developing Helm charts:

1. **Start Simple**: Use `helm create` as template
2. **Parameterize**: Make everything configurable
3. **Test Thoroughly**: Use `helm template` and `helm lint`
4. **Document**: Add NOTES.txt and comments
5. **Version**: Follow semantic versioning
6. **Dependencies**: Manage with Chart.yaml
7. **Security**: Never commit secrets

Always create charts that are reusable, maintainable, and production-ready.

## Resources

- Helm Documentation: https://helm.sh/docs/
- Chart Best Practices: https://helm.sh/docs/chart_best_practices/
- Artifact Hub: https://artifacthub.io/
- Helm Hub: https://hub.helm.sh/

Overview

This skill provides expert-level guidance for Helm 3 chart development, packaging, templating, and production operations. It helps create maintainable, reusable Kubernetes application charts following Helm best practices and operational patterns. Use it to accelerate chart authoring, validate deployments, and harden release workflows for production clusters.

How this skill works

The skill inspects chart structure, values design, template patterns, hooks, and dependency configuration to identify issues and suggest improvements. It validates templating idioms, recommends schema-driven values validation, and proposes production-ready defaults for probes, securityContext, resources, and autoscaling. It also guides safe install/upgrade/rollback commands and repository/dependency management.

When to use it

  • Creating a new Helm chart for a microservice or web application
  • Hardening an existing chart for production (security, probes, resources)
  • Designing values.yaml and values.schema.json for safe overrides
  • Implementing CI/CD flows for install, upgrade, and rollback
  • Troubleshooting release failures, hooks, or template rendering

Best practices

  • Follow semantic versioning for chart and app versions (MAJOR.MINOR.PATCH)
  • Provide a comprehensive values.yaml with sensible defaults and avoid secrets in plain values
  • Add values.schema.json to validate user-provided values before install
  • Use _helpers.tpl for reusable template functions and consistent labels/names
  • Include liveness/readiness probes, resource requests/limits, and pod securityContext by default
  • Use hooks sparingly for migrations and tests; add hook-delete-policy to avoid orphaned resources

Example use cases

  • Author a production-ready chart with defaults for probes, autoscaling, and persistence
  • Build a values schema to catch invalid configuration before deployment
  • Create robust upgrade flows using --atomic, --wait, and sensible timeouts
  • Manage third-party dependencies with helm dependency update/build and repository best practices
  • Implement pre-install migration jobs and test hooks that run as part of CI

FAQ

How do I avoid storing secrets in values.yaml?

Use external secret management (sealed-secrets, HashiCorp Vault, Kubernetes Secrets populated via CI or operators) and reference secrets with valueFrom in templates.

When should I use autoscaling vs fixed replicas?

Use autoscaling when workload has variable demand and you can define reliable CPU/memory targets; use fixed replicas for predictable, stable workloads or when autoscaler is not available.