astronomer/helm-chart
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.
npx skills add https://github.com/astronomer/astronomer --skill helm-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.
charts/airflow/ by helm dep update (run make charts). Sub-chart values are nested under the airflow: key in values.yaml.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.
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.
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).
# In the deployment template
{{- if .Values.myComponent.livenessProbe }}
livenessProbe: {{ tpl (toYaml .Values.myComponent.livenessProbe) $ | nindent 12 }}
{{- else }}
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
{{- end }}
# In values.yaml
myComponent:
# -- Custom liveness probe configuration (overrides default)
livenessProbe: {}
# -- Custom readiness probe configuration (overrides default)
readinessProbe: {}
Use helm-docs comment pattern:
# -- Brief description of what this value does
# @default -- value
myKey: value
myObject:
# -- Nested key description
nestedKey: value
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 }}
Create values.schema.json:
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1,
"description": "Number of replicas"
}
}
}
prek run --all-files# ✓ GOOD: Standard Kubernetes labels
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" . }}
# ❌ BAD: Non-standard labels
metadata:
labels:
app: myapp
version: v1
MAJOR.MINOR.PATCH
MAJOR: Breaking changes (incompatible values schema changes)
MINOR: New features (backward compatible)
PATCH: Bug fixes (backward compatible)
| 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 version (your release)
version: 2.1.0
# App version (upstream application)
appVersion: "3.5.2"
# Chart.yaml
dependencies:
- name: postgresql
version: "12.x.x" # Use range for flexibility
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
# Update dependencies
helm dependency update mychart/
# Build dependencies (download to charts/)
helm dependency build mychart/
# List dependencies
helm dependency list mychart/
# Chart.lock (auto-generated, commit to git)
dependencies:
- name: postgresql
repository: https://charts.bitnami.com/bitnami
version: 12.1.6
digest: sha256:abc123...
generated: "2024-01-15T10:30:00Z"
# Exact version
version: "12.1.6"
# Patch range (12.1.x)
version: "~12.1.0"
# Minor range (12.x.x)
version: "^12.0.0"
# Greater than
version: ">=12.0.0"
# Range
version: ">=12.0.0 <13.0.0"
dependencies:
- name: postgresql
version: "12.x.x"
repository: https://charts.bitnami.com/bitnami
import-values:
- child: primary.service
parent: database
# Or import all
- child: null
parent: postgresql
# Add new fields with defaults
newFeature:
enabled: false # Default to off for existing users
# 1. Deprecate in MINOR release
# values.yaml
legacyField: "" # @deprecated Use newField instead
# 2. Add migration helper
{{- if .Values.legacyField }}
{{- fail "legacyField is deprecated, please use newField" }}
{{- end }}
# 3. Remove in MAJOR release
# Test upgrade from previous version
helm upgrade myrelease mychart/ \
--dry-run \
--debug \
-f old-values.yaml
# Diff changes
helm diff upgrade myrelease mychart/ -f values.yaml
templates/ with helm.sh/hook: test annotationhelm install or helm upgradehelm test RELEASE_NAMETest passes: Pod exits with code 0
Test fails: Pod exits with non-zero code
| 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) |
| 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 |
| 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 |
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:
restartPolicy: NeverlatestCommon test images:
curlimages/curl:8.7.1 - HTTP endpoint checksbusybox:1.36 - Basic shell utilitiesbitnami/kubectl:1.29 - Kubernetes API queriespostgres:16-alpine - Database connectivityapiVersion: 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
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
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
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
mychart/
├── templates/
│ ├── tests/
│ │ ├── test-service-connectivity.yaml
│ │ ├── test-config-validation.yaml
│ │ └── test-readiness.yaml
│ ├── deployment.yaml
│ └── service.yaml
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 }}
# metrics test
{{- end }}
Use helm.sh/hook-weight (lower runs first):
# Test 1: Run first
metadata:
annotations:
helm.sh/hook-weight: "-5"
---
# Test 2: Run second
metadata:
annotations:
helm.sh/hook-weight: "0"
# Run tests
helm test my-release
helm test my-release --logs
helm test my-release --timeout 10m
helm test my-release -n my-namespace
# Debugging
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
Each test should verify one thing well:
# Good: Single focused test
metadata:
name: "{{ .Release.Name }}-test-health"
# Avoid: Tests multiple unrelated things
metadata:
name: "{{ .Release.Name }}-test-everything"
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
| 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
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
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:
# Avoid
command: ["curl", "-H", "Authorization: Bearer super-secret-key", "http://service/"]
# Better
env:
- name: TEST_TOKEN
valueFrom:
secretKeyRef:
name: test-credentials
key: token
| 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 |
helm lint# REQUIRED security settings
podSecurityContext:
runAsNonRoot: true # ✓ Never run as root
fsGroup: 1000 # ✓ Set filesystem group
seccompProfile:
type: RuntimeDefault # ✓ Use seccomp
securityContext:
allowPrivilegeEscalation: false # ✓ Block privilege escalation
readOnlyRootFilesystem: true # ✓ Immutable container
runAsNonRoot: true # ✓ Non-root user
runAsUser: 1000 # ✓ Specific UID
capabilities:
drop:
- ALL # ✓ Drop all capabilities
# ❌ BAD: Privileged container
securityContext:
privileged: true
# ❌ BAD: Running as root
securityContext:
runAsUser: 0
# ❌ BAD: Writable root filesystem
securityContext:
readOnlyRootFilesystem: false
# ❌ BAD: Host namespaces
hostNetwork: true
hostPID: true
hostIPC: true
# ❌ BAD: Dangerous volume mounts
volumes:
- name: host
hostPath:
path: /
# ❌ BAD: Secrets in environment variables (prefer mounted secrets)
env:
- name: DB_PASSWORD
value: "hardcoded-password"
# ❌ BAD: No resource limits
resources: {}
# ✓ GOOD: Minimal permissions
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
resourceNames: ["my-config"] # Even better: specific resources
# ❌ BAD: Overly permissive
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
# ❌ BAD: Cluster-wide access when namespace-scoped sufficient
kind: ClusterRole # Should be Role if namespace-scoped
# ✓ GOOD: Specific tag
image:
repository: myapp
tag: "v1.2.3" # Specific version
# ❌ BAD: Latest tag
image:
repository: myapp
tag: "latest" # Mutable, unpredictable
# ✓ GOOD: Digest pinning for critical apps
image:
repository: myapp@sha256:abc123...
# Basic linting
helm lint mychart/
# Strict mode
helm lint mychart/ --strict
# With values
helm lint mychart/ -f values-production.yaml
# Best practices
helm template myrelease mychart/ | polaris audit --audit-path -
# Deprecated APIs
helm template myrelease mychart/ | pluto detect -
# NSA security framework
helm template myrelease mychart/ | kubescape scan framework nsa -
| 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 |
🔴 **Critical: Security - Privileged Container**
The container is running as privileged which grants full host access.
securityContext:
privileged: true
securityContext:
privileged: false
allowPrivilegeEscalation: false
🟠 Major: Missing Resource Limits
No resource limits defined. This can lead to resource starvation.
# Add to values.yaml
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.
# Current
{{ toYaml .Values.labels | indent 4 }}
# Suggested
{{- 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
name: Helm Chart CI
on:
push:
paths:
pull_request:
paths:
jobs:
lint:
runs-on: ubuntu-latest
steps:
run: helm lint charts/*
test:
runs-on: ubuntu-latest
steps:
run: helm plugin install https://github.com/helm-unittest/helm-unittest
run: helm unittest charts/*
template:
runs-on: ubuntu-latest
steps:
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:
run: |
helm package charts/*
### 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
A Helm chart for deploying MyApp on Kubernetes.
helm repo add myrepo https://charts.example.com
helm install myrelease myrepo/myapp
| Parameter | Description | Default |
| ------------------ | ------------------ | ---------------------- |
| replicaCount | Number of replicas | 1 |
| image.repository | Image repository | myapp |
| image.tag | Image tag | "" (uses appVersion) |
Breaking changes:
image.name renamed to image.repositoryMigration:
# 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/
Take astronomer/helm-chart from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.