Skip to Content
Examples & recipes

Examples & recipes

Every example on this page is drawn directly from the library’s own test fixtures (tests/fixtures/{minimal,full,stateful,daemon}/values.yaml), which are rendered and validated against vendored Kubernetes schemas on every change — these aren’t illustrative snippets, they’re the same values the gate exercises.

Minimal: the smallest working chart

The floor for a working render: an image and one Service.

image: repository: example/minimal tag: "1.0.0" service: enabled: true ports: - name: http port: 80 targetPort: http protocol: TCP

Everything else — Pod Security Standards hardening, labels, the ServiceAccount with automount disabled — comes from the library’s zero-config defaults. See Getting started for how to scaffold this.

StatefulSet with persistence and a scraped headless Service

workload: type: StatefulSet image: repository: example/stateful tag: "1.0.0" service: enabled: true ports: - name: http port: 80 targetPort: http protocol: TCP internalTrafficPolicy: Local publishNotReadyAddresses: true persistence: enabled: true size: 1Gi statefulSet: volumeClaimTemplates: - name: logs accessModes: ["ReadWriteOnce"] storage: 1Gi livenessProbe: enabled: true readinessProbe: enabled: true startupProbe: enabled: true serviceMonitor: enabled: true capabilities: apiVersions: - monitoring.coreos.com/v1 # force-assume for offline helm template

workload.type: StatefulSet with no explicit statefulSet.serviceName gets a library-managed governing headless Service (<fullname>-headless) automatically — see Architecture for why. That headless Service carries a platform/service-role: headless label specifically so the default serviceMonitor selector can exclude it and scrape only the primary Service, avoiding doubled Prometheus targets for the same pods.

DaemonSet with sidecars, an init container, and dev mTLS

workload: type: DaemonSet image: repository: example/daemon tag: "1.0.0" tlsSelfSigned: enabled: true commonName: daemon.local dnsNames: [daemon.local] mtls: enabled: true clients: - name: worker - name: reader trustBundle: enabled: true initContainers: enabled: true containers: - name: init-wait image: docker.io/library/busybox:1.36.1 command: ["sh", "-c", "echo init"] sidecars: enabled: true containers: - name: metrics-proxy image: docker.io/library/busybox:1.36.1 command: ["sh", "-c", "sleep infinity"] securityContext: runAsUser: 65532 # overrides the library default; every other field still applies - name: log-shipper image: docker.io/library/busybox:1.36.1 command: ["sh", "-c", "sleep infinity"] # no override — still fully hardened daemonSet: nodeSelector: kubernetes.io/os: linux tolerations: - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule

The two sidecars deliberately demonstrate the hardening merge from two angles: metrics-proxy overrides one securityContext key and keeps every other library default, while log-shipper supplies no securityContext at all and still renders fully hardened — Pod Security Standards is evaluated per container, so neither can opt out by omission. See Security features → mTLS for what tlsSelfSigned.mtls mounts into each container.

Gateway API + generated credentials + admission webhook

A slice from the library’s full-coverage fixture, showing several tier-1 features composing in one chart:

image: repository: example/app tag: "1.2.3" ingress: enabled: true hostname: app.example.com gatewayApi: enabled: true hostnames: - app.example.com parentRefs: - name: shared-gateway namespace: gateway-system httpRoute: enabled: true hostnames: - app.example.com parentRefs: - name: shared-gateway namespace: gateway-system generatedSecrets: - name: admin keys: - key: password kind: password - key: auth kind: htpasswd username: admin passwordFrom: password webhooks: enabled: true validating: - name: validate.app.example.com path: /validate rules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["pods"] capabilities: apiVersions: - gateway.networking.k8s.io/v1 - monitoring.coreos.com/v1

ingress and gatewayApi aren’t mutually exclusive — both can target the same hostname if you’re running both an Ingress controller and a Gateway API implementation during a migration window. generatedSecrets and webhooks are unrelated features that happen to compose cleanly here: the generated admin credential has nothing to do with the webhook’s serving certificate, which the library generates and rotates independently. See Values reference for the full key list and Security features for the credential/certificate mechanics.

extraObjects: RBAC and a PriorityClass

For anything the opinionated tier-1 blocks don’t model:

allowClusterScopedExtras: true # PriorityClass below is cluster-scoped extraObjects: Role: - name: app-reader rules: - apiGroups: [""] resources: [configmaps, secrets] verbs: [get, list, watch] RoleBinding: - name: app-reader-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: app-reader subjects: - kind: ServiceAccount name: app-high # matches serviceAccount.name / chart fullname PriorityClass: - name: app-high # cluster-scoped Kinds skip the namespace automatically value: 1000000 globalDefault: false

allowClusterScopedExtras is required the moment any extraObjects entry names a cluster-scoped Kind — omit it and the render fails naming the offending Kind, rather than silently dropping the object. See Security model for why that gate exists.

If your only need is a namespaced Role bound to the chart’s own ServiceAccount, the first-class rbac block (see Values reference) is less to write than the extraObjects form above — it wires the RoleBinding subject to serviceAccount.name automatically, so it can’t drift.

extraObjects: opt-in templating with template: true

Every extraObjects entry renders verbatim by default, so literal {{ }} text (a PrometheusRule alert annotation like {{ $labels.pod }}, say) passes through untouched. Set template: true on an entry to opt that ONE entry into tpl expansion against the release context, so it can reference things like {{ include "platform.fullname" . }} or .Release.Namespace:

extraObjects: ConfigMap: - name: '{{ include "platform.fullname" . }}-scripts' template: true # opt-in: whole entry tpl-expanded data: ns: '{{ .Release.Namespace }}' PrometheusRule: - name: alerts # no flag — rendered verbatim spec: groups: - rules: - annotations: summary: 'Pod {{ $labels.pod }} down' # untouched

The Kind map key (ConfigMap, PrometheusRule above) is never templated — only entry contents are. template is itself a reserved control key: it is stripped before rendering and never appears in the rendered object. To emit a literal {{ inside a template: true entry instead of having it evaluated, escape it with {{ "{{" }}.

The NOTES.txt hostPath/privileged advisory sweep inspects extraObjects values BEFORE template expansion — a hostPath path or privileged flag that only appears after a template: true entry expands will not be flagged.

tpl: value prefix for annotation and env values

Annotations and envVars (map form) don’t have a control key to opt into — they’re flat strings, so the opt-in lives in the value itself. Prefix a string with tpl: and the remainder is tpl-expanded against the release context; everything else, prefixed or not, renders byte-identical:

podAnnotations: vault.example/role: 'tpl:{{ .Release.Name }}-app' # opts in — expanded summary: 'Pod {{ $labels.pod }} down' # no prefix — untouched envVars: NAMESPACE: 'tpl:{{ .Release.Namespace }}' PLAIN: 'literal-value' serviceAccount: annotations: vault.example/role: 'tpl:{{ .Release.Name }}-app'

One shared helper covers every annotation-emitting site in the library: commonAnnotations and every resource’s own annotations/podAnnotations field — the workload, the pod template, serviceAccount.annotations, and every other generated object (Service, Ingress, ConfigMap, Secret, RBAC, webhooks, TLS Secrets, generatedSecrets, Gateway API routes, CronJob, and the monitoring CRDs) — plus envVars map-form values. Merge/precedence still resolves on the raw strings first: a commonAnnotations sentinel that loses to a resource’s own literal annotation of the same key is never expanded, since it never renders. envVars in its slice (list-of-objects) form is always a raw passthrough — the tpl: prefix is never checked there. If a literal value must itself start with tpl:, escape it so the sentinel doesn’t match: tpl:{{ "tpl:..." }}.

This is the flat-string sibling of extraObjectstemplate: true above — structured entries (a whole extraObjects item, Kind and all) opt in with a control key, while flat string values opt in with a value prefix, since there’s no key to attach a boolean to.

Rendering any of these offline

None of the CRD-backed Kinds above (gatewayApi, serviceMonitor, webhooks uses only built-ins so it’s fine) render under a bare helm template — Helm’s client-side API discovery reports no CRDs at all. Force-assume the groups you need to see them locally or in CI:

helm dependency update . helm template my-app . \ --api-versions gateway.networking.k8s.io/v1/HTTPRoute \ --api-versions monitoring.coreos.com/v1/ServiceMonitor \ --kube-version 1.34

or set the equivalent capabilities.apiVersions list in values, shown in each example above — see the capability catalog for why the bare group/version form to --api-versions silently fails to satisfy the gate.

Last updated on