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: TCPEverything 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 templateworkload.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: NoScheduleThe 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/v1ingress 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: falseallowClusterScopedExtras 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' # untouchedThe 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 extraObjects’ template: 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.34or 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.