Departments / devops / helm-charts

helm-charts

Use when a Kubernetes application needs a new Helm chart or a substantial revision to an existing one. Scaffolds Chart.yaml, values.yaml with sensible defaults, and templates for Deployment, Service, Ingress, ConfigMap, and HPA, then validates with helm lint and helm diff against the live cluster.

Department

DevOps

Safety

writes-local
Writes locally

Supported stacks

helm+k8s

When to use

Do not use this skill when the answer is Kustomize + overlays (simpler for small apps) or Argo CD ApplicationSet without templating needs.

Inputs

Outputs

Tool dependencies

Procedure

0. Detect the stack

Confirm Helm + Kubernetes is the packaging system the user wants before scaffolding:

helm version --short 2>/dev/null                      # Helm 3.x installed?
kubectl config current-context 2>/dev/null            # K8s cluster reachable?
ls charts/ helm/ 2>/dev/null                          # existing Helm charts?
ls kustomize/ overlays/ 2>/dev/null                   # Kustomize instead?
ls terraform/ main.tf 2>/dev/null | head              # Terraform-managed K8s resources?
find . -maxdepth 2 -name 'Chart.yaml' 2>/dev/null     # existing chart in this repo?

This skill supports only helm+k8s. If detection shows the repo uses Kustomize (no Chart.yaml, an overlays/ tree), Terraform with the Kubernetes provider writing K8s resources directly, CDK8s, or a raw-YAML workflow, STOP and report. Producing a Helm chart alongside a competing packaging system creates two sources of truth and is a common regret.

1. Scaffold

helm create charts/checkout-api

Then rewrite every file — the default scaffold is verbose and dated. Keep only the structure.

2. Chart.yaml

apiVersion: v2
name: checkout-api
description: Checkout API for the storefront
type: application
version: 0.1.0          # chart version; bump on any template change
appVersion: "1.4.2"     # app version; informational
kubeVersion: ">=1.28.0-0"
home: https://github.com/acme/checkout-api
maintainers:
  - name: platform-team
    email: platform@acme.example

3. values.yaml with defaults that pass prod review

replicaCount: 2

image:
  repository: ghcr.io/acme/checkout-api
  pullPolicy: IfNotPresent
  tag: ""   # falls back to .Chart.AppVersion
  digest: "" # preferred: pin by digest

imagePullSecrets: []

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

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

podSecurityContext:
  runAsNonRoot: true
  runAsUser: 10001
  fsGroup: 10001
  seccompProfile:
    type: RuntimeDefault

securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

ingress:
  enabled: false
  className: nginx
  annotations: {}
  hosts: []
  tls: []

resources:
  requests:
    cpu: 100m
    memory: 256Mi
  limits:
    cpu: 1000m
    memory: 512Mi

livenessProbe:
  httpGet: { path: /healthz, port: http }
  initialDelaySeconds: 20
  periodSeconds: 15

readinessProbe:
  httpGet: { path: /ready, port: http }
  initialDelaySeconds: 5
  periodSeconds: 5

startupProbe:
  httpGet: { path: /healthz, port: http }
  failureThreshold: 30
  periodSeconds: 5

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

pdb:
  enabled: true
  minAvailable: 1

config: {}      # rendered into ConfigMap as-is
secretRefs: []  # list of existing Secret names mounted as envFrom

nodeSelector: {}
tolerations: []
affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: checkout-api
          topologyKey: kubernetes.io/hostname

4. templates/_helpers.tpl

{{- define "checkout-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end }}

{{- define "checkout-api.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end }}

{{- define "checkout-api.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ include "checkout-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "checkout-api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "checkout-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{- define "checkout-api.image" -}}
{{- $repo := .Values.image.repository -}}
{{- if .Values.image.digest -}}
{{ $repo }}@{{ .Values.image.digest }}
{{- else -}}
{{ $repo }}:{{ .Values.image.tag | default .Chart.AppVersion }}
{{- end -}}
{{- end }}

5. templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "checkout-api.fullname" . }}
  labels: {{- include "checkout-api.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels: {{- include "checkout-api.selectorLabels" . | nindent 6 }}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 0
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
      labels: {{- include "checkout-api.selectorLabels" . | nindent 8 }}
    spec:
      serviceAccountName: {{ include "checkout-api.fullname" . }}
      securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }}
      terminationGracePeriodSeconds: 30
      containers:
        - name: app
          image: {{ include "checkout-api.image" . }}
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          envFrom:
            - configMapRef:
                name: {{ include "checkout-api.fullname" . }}
            {{- range .Values.secretRefs }}
            - secretRef:
                name: {{ . }}
            {{- end }}
          livenessProbe:  {{- toYaml .Values.livenessProbe  | nindent 12 }}
          readinessProbe: {{- toYaml .Values.readinessProbe | nindent 12 }}
          startupProbe:   {{- toYaml .Values.startupProbe   | nindent 12 }}
          resources:      {{- toYaml .Values.resources      | nindent 12 }}
          securityContext: {{- toYaml .Values.securityContext | nindent 12 }}
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}
      {{- with .Values.nodeSelector }}
      nodeSelector: {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations: {{- toYaml . | nindent 8 }}
      {{- end }}
      affinity: {{- toYaml .Values.affinity | nindent 8 }}

6. templates/service.yaml, ingress.yaml, configmap.yaml, hpa.yaml, pdb.yaml, serviceaccount.yaml

Keep each template under 80 lines; use helpers for repeated metadata.

hpa.yaml:

{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {{ include "checkout-api.fullname" . }}
  labels: {{- include "checkout-api.labels" . | nindent 4 }}
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {{ include "checkout-api.fullname" . }}
  minReplicas: {{ .Values.autoscaling.minReplicas }}
  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}

pdb.yaml:

{{- if and .Values.pdb.enabled (gt (int .Values.replicaCount) 1) }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {{ include "checkout-api.fullname" . }}
  labels: {{- include "checkout-api.labels" . | nindent 4 }}
spec:
  minAvailable: {{ .Values.pdb.minAvailable }}
  selector:
    matchLabels: {{- include "checkout-api.selectorLabels" . | nindent 6 }}
{{- end }}

7. Validate

helm lint charts/checkout-api -f charts/checkout-api/values-prod.yaml
helm template checkout-api charts/checkout-api -f charts/checkout-api/values-prod.yaml \
  | kubeconform -strict -summary -kubernetes-version 1.30.0 -

# Diff against live cluster (requires helm-diff plugin)
helm -n prod diff upgrade checkout-api charts/checkout-api \
  -f charts/checkout-api/values-prod.yaml

8. CI with chart-testing

ct lint --chart-dirs charts --target-branch main
ct install --chart-dirs charts --target-branch main

ct spins up a kind cluster, installs the chart, and runs any templates/tests/*.yaml Job definitions.

Examples

Example 1 — Stateless HTTP service

Input: chart_name=checkout-api, image=ghcr.io/acme/checkout-api, port=8080, ingress_host=checkout.acme.example, enable_hpa=true, secrets=["db", "stripe"], configmap={LOG_LEVEL: info}.

Generates the full chart above; values-prod.yaml overrides: replicaCount: 3, HPA min=3, max=20, ingress.enabled=true with the given host and cert-manager annotations, resources.requests.cpu: 500m.

Example 2 — Background worker, no ingress

Input: chart_name=checkout-worker, port=, ingress_host=, enable_hpa=false, secrets=["db", "sqs"].

service.yaml template is omitted (no port), ingress.yaml is skipped, HPA renders with custom metric (SQS queue depth via KEDA ScaledObject) instead of CPU. PDB still present. livenessProbe uses exec: ["/app/health"] since there is no HTTP endpoint.

Constraints

Quality checks

Customise for your organisation

helm-charts

The LLM will rewrite this skill for your environment. Your API key and form inputs stay in your browser — only the skill and your environment go to OpenRouter.

One line. Be specific — cloud, language, framework, orchestrator.

Free text that steers the rewrite. Leave blank if nothing specific.

cost estimate: