- name
- helm-chart-development
- description
- Builds, templates, and manages Helm charts with Chart.yaml, values.yaml, and Go template files for reliable Kubernetes application deployment and versioned releases.
- license
- MIT
- compatibility
- opencode
- metadata
- {"version":"1.0.0","domain":"cncf","role":"implementation","scope":"implementation","output-format":"code","triggers":"helm chart development, helm templating, helm values, helm debugging, chartmuseum, helm registry, helm lint, chart.yaml","archetypes":["tactical","generation"],"anti_triggers":["kustomize overlays","argo cd workflows"],"response_profile":{"verbosity":"medium","directive_strength":"high","abstraction_level":"operational"},"related-skills":"cncf-kubernetes, coding-kubernetes-deployments-management, cncf-prometheus"}
# Helm Chart Development
Implements the full lifecycle of Helm chart creation, templating, packaging, and distribution — from initial `helm create` scaffolding through production-ready chart releases on a registry or ChartMuseum.
## TL;DR Checklist
- [ ] `Chart.yaml` declares `apiVersion: v2`, a stable `version`, and `appVersion`
- [ ] `values.yaml` provides sensible defaults and documents every field with comments
- [ ] Templates use `{{-` / `-}}` whitespace control to avoid blank lines in rendered YAML
- [ ] `{{ include }}` helpers keep repeated logic DRY and namespaced per release
- [ ] `helm lint <chart-dir>` passes before any commit
- [ ] `helm template .` renders cleanly with default and overridden values
- [ ] `helm package <chart-dir>` produces a tarball with correct semantic version
- [ ] Chart is pushed to a registry or ChartMuseum with `helm push` or `helm push chart.tgz chartmuseum://...`
---
## When to Use
Use this skill when:
- You are authoring a new Helm chart from scratch or converting an existing deployment to a chart
- You need to parameterize a Kubernetes manifest so teams can install it with custom values
- You are debugging a chart that renders incorrectly or fails `helm lint`
- You are packaging and distributing a chart to a Helm registry or ChartMuseum for team use
- You need to create reusable template helpers (labels, ingress, service) across multiple template files
---
## When NOT to Use
Avoid this skill for:
- Pure Kustomize overlay management — use Kustomize-native tooling instead
- Argo CD application-of-application workflows — Helm is for packaging, Argo CD is for GitOps sync
- Generating one-off manifests for non-Helm users — output pure YAML directly instead
- Managing cluster-wide operators with multi-chart dependencies — use `helm install` with proper `requirements.yaml`/`dependencies` in Chart.yaml
---
## Core Workflow
### Phase 1: Scaffolding and Structure
1. **Create the chart scaffold** — Run `helm create <chart-name>` in an empty directory. This generates `Chart.yaml`, `values.yaml`, and `templates/` with a starter `deployment.yaml`, `service.yaml`, and `helpers.tpl`.
2. **Audit the scaffold** — Read every generated file. The scaffold is intentionally generic: update `Chart.yaml` metadata, remove files you do not need (e.g., `ingress.yaml` if the app has no ingress), and rename files to match your application's resources.
3. **Define the directory layout** — A production chart follows this structure:
```
my-chart/
├── Chart.yaml # Chart metadata
├── values.yaml # Default values
├── values.schema.json # Optional JSON Schema validation
├── templates/
│ ├── _helpers.tpl # Reusable named templates
│ ├── deployment.yaml
│ ├── service.yaml
│ └── NOTES.txt
└── .helmignore # Files to exclude from packaging
```
**Checkpoint:** Run `helm lint my-chart/` and verify it reports no errors or only warnings you have intentionally accepted.
### Phase 2: Chart.yaml and Values
4. **Write Chart.yaml** — Use `apiVersion: v2` for modern Helm charts. Set `version` to a semantic version (e.g., `1.2.0`) and `appVersion` to the application's version string:
```yaml
apiVersion: v2
name: my-chart
description: A Helm chart for deploying my application
type: application
version: 1.2.0
appVersion: "3.1.0"
keywords:
- web
- app
maintainers:
- name: Platform Team
email: platform@example.com
```
**Checkpoint:** Confirm `version` is valid semver and `appVersion` is quoted if it contains non-numeric characters.
5. **Write values.yaml** — Provide defaults for every configurable aspect of your chart. Every top-level key in `values.yaml` is overridable at install time via `--set` or `--values`. Comment each section to document its purpose:
```yaml
# Replicas for the deployment
replicaCount: 2
# Container image configuration
image:
repository: myregistry/my-app
pullPolicy: IfNotPresent
# Override the image tag explicitly
tag: ""
# Service exposure
service:
type: ClusterIP
port: 80
targetPort: 8080
# Resource limits and requests
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
# Autoscaling configuration (disabled by default)
autoscaling:
enabled: false
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 80
```
**Checkpoint:** Run `helm template my-chart/ --set replicaCount=5` and verify the rendered output reflects the override.
### Phase 3: Templating
6. **Implement _helpers.tpl** — Define named templates for labels, selectors, and any repeated patterns. Use the chart's fullname to namespace helper output:
```
{{/*
Generate basic labels including app.kubernetes.io/version.
*/}}
{{- define "my-chart.labels" -}}
include "my-chart.selectorLabels" .
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels used by Deployments and Services.
*/}}
{{- define "my-chart.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
```
7. **Write resource templates** — In `templates/deployment.yaml`, reference values and helpers:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-chart.fullname" . }}
labels:
{{- include "my-chart.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "my-chart.selectorLabels" . | nindent 8 }}
template:
metadata:
labels:
{{- include "my-chart.selectorLabels" . | nindent 10 }}
spec:
containers:
- name: {{ .Chart.Name }}
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
readinessProbe:
httpGet:
path: /ready
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
```
8. **Handle conditional blocks** — Use `if/else` for optional features (ingress, HPA, secrets) and `range` for lists:
```yaml
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "my-chart.fullname" . }}
spec:
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "my-chart.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
```
**Checkpoint:** Run `helm template my-chart/ -f custom-values.yaml` with at least two different value sets and verify both render correctly.
### Phase 4: Validation, Packaging, and Distribution
9. **Validate the chart** — Run the full validation suite:
```bash
# Lint for structural issues
helm lint my-chart/
# Lint with strict mode (catches warnings as errors)
helm lint --strict my-chart/
# Render with default values
helm template my-chart/
# Render with custom overrides
helm template my-chart/ --set replicaCount=3 --set image.tag=v2.0.0
# Install in dry-run mode (no actual cluster interaction)
helm install --dry-run --debug my-release my-chart/
```
10. **Package the chart** — Create a versioned tarball for distribution:
```bash
helm package my-chart/
# Produces: my-chart-1.2.0.tgz
```
11. **Push to a registry or ChartMuseum** — Distribute the packaged chart:
```bash
# Push to a Helm OCI registry
helm push my-chart-1.2.0.tgz oci://registry.example.com/charts
# Push to a ChartMuseum instance
helm push my-chart-1.2.0.tgz chartmuseum://http://chartmuseum.internal
# Or upload manually
curl --data-binary @"my-chart-1.2.0.tgz" http://chartmuseum.internal/api/charts
```
**Checkpoint:** Install the packaged chart in a test cluster using `helm install test-release ./my-chart-1.2.0.tgz` and verify all resources are healthy.
---
## Implementation Patterns
### Pattern 1: Standard Deployment with Resource Management
This is the baseline deployment pattern for stateless workloads. It includes readiness/liveness probes, resource requests/limits, and proper label inheritance from helpers.
**deployment.yaml**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-chart.fullname" . }}
labels:
{{- include "my-chart.labels" . | nindent 4 }}
{{- with .Values.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
strategy:
{{- toYaml .Values.strategy | nindent 4 }}
selector:
matchLabels:
{{- include "my-chart.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-chart.selectorLabels" . | nindent 8 }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "my-chart.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
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: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 10
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- 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 }}
```
**values.yaml (deployment-related section)**
```yaml
replicaCount: 2
image:
repository: myregistry/my-app
pullPolicy: IfNotPresent
tag: ""
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
env:
APP_ENV: production
LOG_LEVEL: info
nodeSelector: {}
tolerations: []
affinity: {}
autoscaling:
enabled: false
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 80
```
### Pattern 2: Service Discovery with Headless and ClusterIP Services
This pattern covers two service types in one chart: a standard ClusterIP for internal communication and an optional headless service for direct pod addressing (useful for stateful apps or service meshes).
**service.yaml**
```yaml
{{- if .Values.service.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "my-chart.fullname" . }}
labels:
{{- include "my-chart.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "my-chart.selectorLabels" . | nindent 4 }}
{{- end }}
{{- if .Values.headlessService.enabled }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "my-chart.fullname" . }}-headless
labels:
{{- include "my-chart.labels" . | nindent 4 }}
spec:
type: ClusterIP
clusterIP: None
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "my-chart.selectorLabels" . | nindent 4 }}
{{- end }}
```
**values.yaml (service-related section)**
```yaml
service:
enabled: true
type: ClusterIP
port: 80
targetPort: 8080
headlessService:
enabled: false
```
### Pattern 3: HorizontalPodAutoscaler with Custom Metrics
Use this pattern when the application scales based on CPU, memory, or custom metrics (e.g., queue depth).
**hpa.yaml**
```yaml
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "my-chart.fullname" . }}
labels:
{{- include "my-chart.labels" . | nindent 4 }}
GitHub에서 보기