Helm bjw-s Chart Generator
Published as
helm-chart-generatoruntil v2.x. Update anything still pointing at the old name.
Library version matrix
| common | Kubernetes | Helm | Status |
|---|---|---|---|
5.1.0 |
>= 1.31 |
>= 3.18 |
Default — latest stable, all examples target it |
4.6.2 |
>= 1.25 |
>= 3.14 |
Legacy — pin when the cluster can't meet 5.x reqs |
Everything documented here works on common 5.x by default. When a
pattern is not available on 4.x it's tagged (5.x only) so
agents pinned to the legacy track can skip it. See
references/migration-4-to-5.md for
the full 4 → 5 upgrade procedure.
New in common 5.1.0
Drop-in over 5.0.x — no values change is required to upgrade. Four
additions, each with a worked example in
references/patterns.md:
- DaemonSets accept
strategy/rollingUpdateand render a realupdateStrategy. On 5.0.x both keys were silently ignored for this controller type. serviceAccount.<id>.automountServiceAccountTokensets the field on the ServiceAccount object itself. It does not replace the pod-level key: the library always writesautomountServiceAccountTokeninto the pod spec (defaultfalse), and the pod spec wins. Set both.route.<id>.namespaceOverridedeploys a Route into another namespace, and the library then emits the matchingReferenceGrantautomatically. Turn that off withroute.<id>.referenceGrant.enabled: false.rollingUpdatetakes the upstream key names —maxSurgeandmaxUnavailable. The oldsurge/unavailableshorthands still work but are deprecated and disappear in 6.0. StatefulSets gainedrollingUpdate.maxUnavailable, which the cluster only honours with theMaxUnavailableStatefulSetfeature gate — alpha and off by default up to Kubernetes 1.34, beta from 1.35 with the default varying by patch release.
One behavioral change: an invalid strategy is now rejected by the values
schema instead of a template fail, so helm lint reports it earlier and
the message names the valid values per controller type.
Migration 4.x → 5.x at a glance
Five things to know — full details in
references/migration-4-to-5.md:
automountServiceAccountToken: falseis now the default. Flip it back totrueper-pod if the workload needs to call the Kubernetes API.- A default unprivileged ServiceAccount is created for every release.
Opt out with
global.createDefaultServiceAccount: falsewhen you reference an externally-managed SA. rawResourceswas restructured — manifest content moved out ofspec:into amanifest:wrapper, and labels/annotations now live undermetadata:. Only relevant if you userawResources(rare).- ServiceMonitor / PodMonitor
jobLabeldefaults toapp.kubernetes.io/name. Override if your Prometheus rules depended on the oldmetadata.namedefault. - Minimums bumped: Kubernetes ≥ 1.31, Helm ≥ 3.18.
Quick Start Workflow
- Understand the app. Image, ports, environment, storage (config, data, logs), ingress, and whether sidecars or init containers apply.
- Generate the base from
assets/templates/:Chart.yaml,values.yaml,templates/common.yaml,templates/NOTES.txt. - Build
values.yamlin order: controllers and containers, then services, ingress, persistence, secrets and configMaps. - Validate:
helm dependency update(writesChart.lock), thenvalidate_chart.py, thenhelm lintandhelm template.
Core Structure
my-app/
├── Chart.yaml # Metadata and dependencies
├── values.yaml # Configuration
└── templates/
├── common.yaml # Includes the bjw-s library
└── NOTES.txt # Post-install instructions
templates/common.yaml is always exactly one line:
{{- include "bjw-s.common.loader.all" . }}. NOTES.txt covers how to
reach the app, default credentials if any, and the next configuration
step.
Chart.yaml
apiVersion: v2
name: <app-name>
description: <brief description>
type: application
version: 1.0.0
appVersion: "<app version>"
dependencies:
- name: common
repository: https://bjw-s-labs.github.io/helm-charts
version: 5.1.0 # Default. Pin to 4.6.2 for legacy clusters (K8s < 1.31 / Helm < 3.18).
values.yaml Structure
Follow this order for clarity:
# 1. Default Pod options (optional)
defaultPodOptions:
# 5.x default is false; set to true only if the pod calls the K8s API.
automountServiceAccountToken: false
securityContext: {}
annotations: {}
# 2. Controllers (required)
controllers:
main: # or custom name
containers:
main: # or custom name
image: {}
env: {}
probes: {}
# 3. Service (required if exposing)
service:
main:
controller: main
ports: {}
# 4. Ingress (optional)
ingress:
main:
className: ""
hosts: []
# 5. Persistence (optional)
persistence:
config:
type: persistentVolumeClaim
# or: emptyDir, configMap, secret, nfs, hostPath, ephemeral
# 6. ConfigMaps/Secrets (optional)
configMaps: {}
secrets: {}
Common Patterns
The baseline, a single container with a service and a PVC:
controllers:
main:
containers:
main:
image:
repository: nginx
tag: "1.25-alpine"
pullPolicy: IfNotPresent
service:
main:
controller: main
ports:
http:
port: 80
persistence:
config:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 1Gi
globalMounts:
- path: /config
A sidecar is one more entry under containers: with
dependsOn: <container> to order startup.
See references/patterns.md for worked examples:
- Multi-controller setups
- Init containers
- VPN sidecars (gluetun)
- Code-server sidecars
- Shared volumes between containers
- Private registries with
imagePullSecrets - StatefulSets with headless service
Plus one section per version-gated key listed below.
Version-gated features
These are ignored or rejected below the version in the Since column.
Each has a worked example in
references/patterns.md:
| Key | Since | What it buys you |
|---|---|---|
horizontalPodAutoscaler |
5.0.0 |
Autoscaling bound to a controller identifier |
podMonitor |
5.0.0 |
Prometheus scraping without a Service |
persistence.*.type: ephemeral |
5.0.0 |
Per-pod PVC, deleted with the pod |
resizePolicy (pod + container) |
5.0.0 |
In-place CPU/memory resize, no pod recreation |
networkpolicies |
5.0.0 |
Auto-targets the only controller when it is unambiguous |
strategy on a DaemonSet |
5.1.0 |
Real updateStrategy instead of a silently dropped key |
serviceAccount.*.automountServiceAccountToken |
5.1.0 |
Declares the token policy on the SA itself, for consumers outside the chart |
route.*.namespaceOverride |
5.1.0 |
Cross-namespace Route with an auto-generated ReferenceGrant |
rollingUpdate.maxSurge / .maxUnavailable |
5.1.0 |
Upstream key names; surge / unavailable are deprecated |
Best Practices
These shape every generated chart. The reasoning, and the long form, are
in references/best-practices.md.
- Pin image tags, never
:latest. Requests and limits on every container. Liveness and readiness probes at minimum. - Non-root
securityContext. Secrets for sensitive data,imagePullSecretsfor private registries. - Reference services by identifier, not by name.
- Name controllers and containers for what they do, not
main; name services after their controller or their purpose. - Keep
automountServiceAccountToken: false(the 5.x default). When the workload genuinely calls the K8s API, pair it with an explicit ServiceAccount and RBAC rather than the auto-created default. globalMountsfor simple cases,advancedMountsfor multi-container,existingClaimfor pre-created PVCs,type: ephemeralfor scratch space tied to the pod (5.x only).
Validation
After generating a chart:
# 1. Fetch dependencies (required before helm commands)
cd /path/to/chart
helm dependency update
# 2. Validate structure
uv run skills/helm-bjw-s-chart/scripts/validate_chart.py /path/to/chart
# Or with JSON output for CI:
uv run skills/helm-bjw-s-chart/scripts/validate_chart.py --json /path/to/chart
# 3. Helm validation
helm lint .
helm template . --debug
# 4. Dry-run installation
helm install --dry-run --debug my-release .
The validator warns when the chart still pins common 4.x, when
rawResources uses the legacy spec: shape (removed in 5.x), when
an external ServiceAccount is referenced without
global.createDefaultServiceAccount: false, when Chart.lock is
missing, or when a declared dependency has no vendored tarball under
charts/.
Pre-Deploy Checklist
Before deploying to a cluster, verify:
- All image tags are pinned (no
:latest) - Resources (requests + memory limits) are set on every container
- Health probes configured (liveness + readiness minimum)
-
securityContextset: non-root,readOnlyRootFilesystem, drop ALL capabilities -
automountServiceAccountToken: falseunless explicitly needed - If using an external ServiceAccount,
global.createDefaultServiceAccount: falseis set - If
rawResourcesis in play, manifest uses the 5.xmanifest:wrapper (not legacyspec:) - Secrets reference external sources, not hardcoded values
-
helm dependency updaterun, withChart.lockand the populatedcharts/published (see Publishing the Chart) -
helm lintpasses with no errors
Publishing the Chart
A published chart must be self-contained: both Chart.lock and a
populated charts/ have to be present before helm package runs, or
offline consumers cannot resolve the common library. Run
helm dependency update first, then package. Either commit charts/ to
git, or gitignore it and run helm dependency build in the pipeline.
Full rationale and the two strategies:
references/best-practices.md, section
"Publishing and Dependency Vendoring".
Common Issues
Services not found: Use identifier not name in ingress paths
Mounts not working: Check globalMounts vs advancedMounts usage
Names too long: Use nameOverride or fullnameOverride in global settings
Controller not starting: Check dependsOn order for init/sidecar containers
Unexpected ServiceAccount appears (5.x): Set global.createDefaultServiceAccount: false or define your own SA
Pod can't talk to the K8s API (5.x): Set automountServiceAccountToken: true on the pod AND grant RBAC
strategy rejected by helm lint (5.1+): It is a string, never a map, and the valid values depend on the controller type
Cross-namespace Route can't reach the Service: The ReferenceGrant is only emitted from namespaceOverride, not from a hand-written backendRefs.namespace
References
references/migration-4-to-5.md- Full 4 → 5 upgrade procedurereferences/patterns.md- Common deployment patterns, including Gateway API HTTPRoutereferences/best-practices.md- Kubernetes/Helm best practicesreferences/values-schema.md- Complete values.yaml referencereferences/compose-mapping.md- Docker Compose → bjw-s values.yaml mappingassets/templates/- Base templates for quick start