| name | helm-chart |
| description | Use for Helm chart work - creating charts, modifying existing charts, values design, testing. For the Astronomer airflow-chart repository, this is the umbrella chart whose single dependency is the upstream Apache Airflow chart. |
Helm Chart Work Guide
Working on the airflow-chart
This repository (astronomer/airflow-chart) is Astronomer's Helm chart for Apache Airflow. The chart is named airflow (see Chart.yaml), but the repo and the packaged release are airflow-chart.
It is an umbrella (parent) chart: it declares the upstream Astronomer Airflow chart as a dependency (dependencies: in Chart.yaml, pinned to a *-astro version) and layers Astronomer-specific resources on top. A single values.yaml controls both the parent chart's own resources and the bundled airflow sub-chart.
- The sub-chart is downloaded to
charts/airflow/ by helm dep update (run make charts). Sub-chart values are nested under the airflow: key in values.yaml.
- The parent chart's own resources live at the top-level
templates/ directory and add things like the DAG-deploy server, git-sync relay, auth sidecars, OpenShift SCC, logging sidecar, and ingress.
For testing, use the chart-tests skill — it covers render_chart(), sub-chart values nesting under airflow, parametrized tests, and uv run pytest usage.
Template layout
The parent chart's templates live under top-level templates/. Two patterns coexist — follow whichever the surrounding code already uses; we do not retroactively rename files.
Component subdirectories. Multi-file components get their own subdirectory, and the component name is repeated in the filename so the file is identifiable from its basename alone: api-server/, dag-deploy/, flower/, git-sync-relay/, webserver/.
templates/dag-deploy/dag-server-statefulset.yaml
templates/git-sync-relay/git-sync-relay-deployment.yaml
templates/api-server/api-server-auth-sidecar-configmap.yaml
Bare top-level files. Single-purpose resources sit directly under templates/:
templates/ingress.yaml
templates/extra-objects.yaml
templates/airflow-scc-anyuid.yaml
templates/generate-ssl.yaml
templates/logging-sidecar-configmap.yaml
In both patterns: lowercase with hyphens, helpers live in templates/_helpers.yaml, and the K8s object kind (deployment, configmap, networkpolicy, service, serviceaccount, role, rolebinding, statefulset, pvc, ingress, …) is part of the filename so resources are easy to locate. Templates belonging to the bundled Airflow sub-chart live under charts/airflow/templates/ and follow upstream's conventions — do not restyle them.
Probe customization
Every container should expose livenessProbe and readinessProbe as overridable values so operators can tune health checks for their environments (slow-starting components, alternative probe methods, higher failure thresholds).
{{- if .Values.myComponent.livenessProbe }}
livenessProbe: {{ tpl (toYaml .Values.myComponent.livenessProbe) $ | nindent 12 }}
{{- else }}
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
{{- end }}
myComponent:
livenessProbe: {}
readinessProbe: {}
Values Documentation
Use helm-docs comment pattern:
myKey: value
myObject:
nestedKey: value
Template Helpers
Define common patterns in _helpers.tpl:
{{- define "myapp.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
Schema Validation
Create values.schema.json:
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1,
"description": "Number of replicas"
}
}
}
Best Practices
- Reference Helm Chart Best Practices
- Use helpers for reusable patterns
- Provide sensible security defaults
- Include comprehensive values documentation
- Add values.schema.json for IDE support
- All changes must pass pre-commit checks with
prek run --all-files
Labels & Annotations
metadata:
labels:
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ include "myapp.chart" . }}
metadata:
labels:
app: myapp
version: v1
Versioning Strategy
Semantic Versioning for Charts
MAJOR.MINOR.PATCH
MAJOR: Breaking changes (incompatible values schema changes)
MINOR: New features (backward compatible)
PATCH: Bug fixes (backward compatible)
Version Bump Guidelines
| Change Type | Version Bump | Example |
|---|
| Breaking values change | MAJOR | image.name → image.repository |
| Remove deprecated field | MAJOR | Remove legacyMode |
| New optional feature | MINOR | Add metrics.enabled |
| New template | MINOR | Add servicemonitor.yaml |
| Bug fix | PATCH | Fix label selector |
| Documentation | PATCH | Update README |
| Dependency update (minor) | PATCH | PostgreSQL 12.1.0 → 12.1.5 |
| Dependency update (major) | MINOR+ | PostgreSQL 11.x → 12.x |
Chart.yaml Version Management
version: 2.1.0
appVersion: "3.5.2"
Dependency Management
Adding Dependencies
dependencies:
- name: postgresql
version: "12.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
Dependency Commands
helm dependency update mychart/
helm dependency build mychart/
helm dependency list mychart/
Chart.lock Management
dependencies:
- name: postgresql
repository: https://charts.bitnami.com/bitnami
version: 12.1.6
digest: sha256:abc123...
generated: "2024-01-15T10:30:00Z"
Dependency Version Ranges
version: "12.1.6"
version: "~12.1.0"
version: "^12.0.0"
version: ">=12.0.0"
version: ">=12.0.0 <13.0.0"
Import Values from Dependencies
dependencies:
- name: postgresql
version: "12.x.x"
repository: https://charts.bitnami.com/bitnami
import-values:
- child: primary.service
parent: database
- child: null
parent: postgresql
Upgrade Strategies
Non-Breaking Upgrades
newFeature:
enabled: false
Breaking Changes
legacyField: ""
{{- if .Values.legacyField }}
{{- fail "legacyField is deprecated, please use newField" }}
{{- end }}
Upgrade Testing
helm upgrade myrelease mychart/ \
--dry-run \
--debug \
-f old-values.yaml
helm diff upgrade myrelease mychart/ -f values.yaml
Testing
How Helm Tests Work
- Define test pods in
templates/ with helm.sh/hook: test annotation
- Install chart with
helm install or helm upgrade
- Run tests with
helm test RELEASE_NAME
- Helm creates pods, executes them, reports results
Test passes: Pod exits with code 0
Test fails: Pod exits with non-zero code
Test Annotations
| Annotation | Purpose |
|---|
helm.sh/hook: test | Marks pod as a test (runs during helm test) |
helm.sh/hook: test-success | Runs after successful release |
helm.sh/hook: test-failure | Runs after failed release |
helm.sh/hook-weight: | Controls execution order (lower = first) |
helm.sh/hook-delete-policy: | Controls cleanup (hook-succeeded, never) |
Test Analysis: What to Test
| Resource Type | Test Considerations |
|---|
| Services | Endpoint reachability, DNS, correct ports |
| Deployments/StatefulSets | Pod readiness, replica count, rollout status |
| Ingress | Route reachability, TLS certificates |
| ConfigMaps/Secrets | Values present, mounted correctly |
| PVCs | Volume mounted, read/write access |
| CRDs | Custom resource creation, reconciliation |
Test Categories
| Category | Purpose | Examples |
|---|
| Smoke | Quick "is it alive" | Service health, pod readiness |
| Functional | Verify specific behavior | API responses, database connectivity |
| Integration | Verify external interactions | Upstream services, third-party APIs |
| Data Validation | Verify deployed state | ConfigMap content, environment variables |
Basic Test Pod Structure
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-test-connectivity"
annotations:
helm.sh/hook: test-success
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
containers:
- name: test
image: curlimages/curl:8.7.1
command:
- sh
- -c
- |
set -e
curl -f http://myapp-service:8080/health
restartPolicy: Never
Best practices:
- Use
restartPolicy: Never
- Use lightweight images (curlimages/curl, busybox, alpine)
- One test pod tests one thing well
- Exit code 0 = pass, non-zero = fail
- Pin images to specific versions, never use
latest
Common test images:
curlimages/curl:8.7.1 - HTTP endpoint checks
busybox:1.36 - Basic shell utilities
bitnami/kubectl:1.29 - Kubernetes API queries
postgres:16-alpine - Database connectivity
Test Examples
Service Connectivity Test
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-api-test"
annotations:
helm.sh/hook: test-success
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
containers:
- name: api-test
image: curlimages/curl:8.7.1
command:
- sh
- -c
- |
set -e
# Test main endpoint
curl -f http://{{ .Release.Name }}-service:8080/health || exit 1
# Verify response content
curl -s http://{{ .Release.Name }}-service:8080/health | grep -q "status.*ok" || exit 1
{{- if .Values.auth.enabled }}
# Test authenticated endpoint
curl -f http://{{ .Release.Name }}-service:8080/secure \
-H "Authorization: Bearer {{ .Values.auth.testToken }}" || exit 1
{{- end }}
restartPolicy: Never
Configuration Validation Test
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-config-test"
annotations:
helm.sh/hook: test-success
spec:
containers:
- name: config-test
image: busybox:1.36
command:
- sh
- -c
- |
set -e
test -f /app/config.yaml || exit 1
grep -q "logLevel: {{ .Values.logLevel }}" /app/config.yaml || exit 1
grep -q "database:" /app/config.yaml || exit 1
volumeMounts:
- name: config
mountPath: /app/config.yaml
subPath: config.yaml
volumes:
- name: config
configMap:
name: {{ include "myapp.fullname" . }}-config
restartPolicy: Never
Deployment Readiness Test
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-readiness-test"
annotations:
helm.sh/hook: test-success
spec:
serviceAccountName: {{ include "myapp.fullname" . }}-test-sa
containers:
- name: kubectl-test
image: bitnami/kubectl:1.29
command:
- sh
- -c
- |
set -e
kubectl get deployment {{ .Release.Name }} -n {{ .Release.Namespace }} -o json | \
jq -e '.status.readyReplicas == {{ .Values.replicaCount }}' || exit 1
restartPolicy: Never
Database Connection Test
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-db-test"
annotations:
helm.sh/hook: test-success
spec:
containers:
- name: db-test
image: postgres:16-alpine
command:
- sh
- -c
- |
set -e
nc -zv {{ .Values.database.host }} {{ .Values.database.port }} || exit 1
PGPASSWORD={{ .Values.database.password }} \
psql -h {{ .Values.database.host }} -p {{ .Values.database.port }} \
-U {{ .Values.database.user }} -d {{ .Values.database.name }} \
-c "SELECT 1;" || exit 1
restartPolicy: Never
Test Organization
mychart/
├── templates/
│ ├── tests/
│ │ ├── test-service-connectivity.yaml
│ │ ├── test-config-validation.yaml
│ │ └── test-readiness.yaml
│ ├── deployment.yaml
│ └── service.yaml
Conditional Testing
Enable/disable tests globally:
{{- if .Values.tests.enabled }}
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-test"
annotations:
helm.sh/hook: test-success
spec:
{{- end }}
In values.yaml:
tests:
enabled: true
Test specific configurations:
{{- if .Values.metrics.enabled }}
{{- end }}
Test Execution Order
Use helm.sh/hook-weight (lower runs first):
metadata:
annotations:
helm.sh/hook-weight: "-5"
---
metadata:
annotations:
helm.sh/hook-weight: "0"
Running and Debugging
helm test my-release
helm test my-release --logs
helm test my-release --timeout 10m
helm test my-release -n my-namespace
kubectl get pods -n namespace -l helm.sh/hook=test
kubectl logs my-release-test-connectivity -n namespace
kubectl describe pod my-release-test-connectivity -n namespace
Test Design Considerations
Test Independence
Each test should verify one thing well:
metadata:
name: "{{ .Release.Name }}-test-health"
metadata:
name: "{{ .Release.Name }}-test-everything"
Resource Management
Set limits to prevent exhaustion:
spec:
containers:
- name: test
image: curlimages/curl:8.7.1
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
Cleanup Policies
| Policy | Behavior | Use Case |
|---|
hook-succeeded | Delete after passing | Normal operation |
never | Never delete | Debugging |
For debugging:
metadata:
annotations:
helm.sh/hook-delete-policy: never
Error Handling
Provide clear failure messages:
command:
- sh
- -c
- |
set -e
if ! curl -f http://service:8080/health; then
echo "ERROR: Service health check failed"
echo "Troubleshooting: kubectl get svc, kubectl logs -l app=myapp"
exit 1
fi
Security
Use least privilege RBAC:
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "myapp.fullname" . }}-test-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "myapp.fullname" . }}-test-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "myapp.fullname" . }}-test-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "myapp.fullname" . }}-test-role
subjects:
- kind: ServiceAccount
name: {{ include "myapp.fullname" . }}-test-sa
Don't embed secrets:
command: ["curl", "-H", "Authorization: Bearer super-secret-key", "http://service/"]
env:
- name: TEST_TOKEN
valueFrom:
secretKeyRef:
name: test-credentials
key: token
Common Test Failures
| Symptom | Cause | Solution |
|---|
| Image pull errors | Wrong image/registry | Verify image name and pull secrets |
| Connection refused | Service not ready | Add readiness test, increase timeout |
| Permission denied | Insufficient RBAC | Add service account and role |
| Command not found | Wrong base image | Use image with required tools |
| Timeout | Service startup too slow | Increase timeout or add retry logic |
Review & Quality Assurance
Review Checklist
Structure & Organization
Values Design
Security
Quality
Security Review
Pod Security Checklist
podSecurityContext:
runAsNonRoot: true
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
Security Anti-Patterns
securityContext:
privileged: true
securityContext:
runAsUser: 0
securityContext:
readOnlyRootFilesystem: false
hostNetwork: true
hostPID: true
hostIPC: true
volumes:
- name: host
hostPath:
path: /
env:
- name: DB_PASSWORD
value: "hardcoded-password"
resources: {}
RBAC Review
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
resourceNames: ["my-config"]
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
kind: ClusterRole
Image Security
image:
repository: myapp
tag: "v1.2.3"
image:
repository: myapp
tag: "latest"
image:
repository: myapp@sha256:abc123...
Automated Review Tools
helm lint mychart/
helm lint mychart/ --strict
helm lint mychart/ -f values-production.yaml
helm template myrelease mychart/ | polaris audit --audit-path -
helm template myrelease mychart/ | pluto detect -
helm template myrelease mychart/ | kubescape scan framework nsa -
Code Review Comments
Severity Levels
| Level | Description | Action |
|---|
| 🔴 Critical | Security vulnerability, data loss risk | Must fix before merge |
| 🟠 Major | Best practice violation, significant issue | Should fix |
| 🟡 Minor | Style, minor improvement | Nice to have |
| 🔵 Suggestion | Alternative approach | Consider |
Example Review Comments
🔴 **Critical: Security - Privileged Container**
The container is running as privileged which grants full host access.
```yaml
# Current
securityContext:
privileged: true
# Suggested
securityContext:
privileged: false
allowPrivilegeEscalation: false
```
🟠 Major: Missing Resource Limits
No resource limits defined. This can lead to resource starvation.
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
🟡 Minor: Use nindent instead of indent
nindent handles newlines automatically and is more reliable.
{{ toYaml .Values.labels | indent 4 }}
{{- toYaml .Values.labels | nindent 4 }}
🔵 Suggestion: Consider using a helper
This pattern is repeated in multiple templates. Consider extracting to _helpers.tpl.
---
## CI/CD Integration
### GitHub Actions Workflow
```yaml
name: Helm Chart CI
on:
push:
paths:
- "charts/**"
pull_request:
paths:
- "charts/**"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-helm@v3
- name: Lint charts
run: helm lint charts/*
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-helm@v3
- name: Install helm-unittest
run: helm plugin install https://github.com/helm-unittest/helm-unittest
- name: Run tests
run: helm unittest charts/*
template:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-helm@v3
- name: Template charts
run: |
for chart in charts/*; do
helm template test $chart --debug
done
release:
needs: [lint, test, template]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Package and push
run: |
helm package charts/*
# Push to registry
Chart Releaser (cr)
cr package charts/mychart
cr upload --owner org --git-repo charts
cr index --owner org --git-repo charts --push
Documentation
README Template
# MyApp Helm Chart


## Description
A Helm chart for deploying MyApp on Kubernetes.
## Prerequisites
- Kubernetes 1.23+
- Helm 3.10+
- PV provisioner (if persistence enabled)
## Installing
```bash
helm repo add myrepo https://charts.example.com
helm install myrelease myrepo/myapp
```
## Configuration
| Parameter | Description | Default |
| ------------------ | ------------------ | ---------------------- |
| `replicaCount` | Number of replicas | `1` |
| `image.repository` | Image repository | `myapp` |
| `image.tag` | Image tag | `""` (uses appVersion) |
## Upgrading
### From 1.x to 2.x
Breaking changes:
- `image.name` renamed to `image.repository`
- Minimum Kubernetes version is now 1.23
Migration:
```yaml
# Old (1.x)
image:
name: myapp
# New (2.x)
image:
repository: myapp
tag: "2.3.4"
```
Auto-Generate Docs (helm-docs)
helm-docs --chart-search-root=charts/