Annotations
Every resource the chart renders accepts user annotations: the global annotations: value lands on every object's metadata, a per-resource annotations merges on top of it, and pod templates have their own channel through podAnnotations. Unlike labels, annotations carry no identity: nothing selects on them, nothing is immutable, and their values are free-form strings. They are the chart's integration surface, where external-dns, cert-manager, Velero, Prometheus, and IRSA read their configuration.
| Scope | Object metadata | Pod template |
|---|---|---|
| Global | annotations | podAnnotations |
| Per resource | deployments.<name>.annotations, same on services, ingresses, jobs, cronJobs, persistence, serviceAccount, and deployments.<name>.autoscaling (HPA) | deployments.<name>.podAnnotations, same on jobs and cronJobs |
Object or pod
The two surfaces are different objects in Kubernetes, and a Deployment's metadata annotations do not propagate to its Pods. Tools that act on running pods, such as Velero backup hooks, Prometheus scrape config, and service mesh injection, read the Pod, so their annotations go in podAnnotations. Tools that act on the resource itself, such as external-dns on a Service or cert-manager on an Ingress, read the object metadata, so theirs go in annotations. See Velero for a worked example of the pod channel.
annotations: # on every resource's metadata
meta.example.com/owner: platform@example.com
podAnnotations: # on every pod template
backup.velero.io/backup-volumes: data
services:
app:
annotations: # this Service only, merged on top
external-dns.alpha.kubernetes.io/hostname: app.example.com
deployments:
app:
podAnnotations: # this Deployment's pods only
prometheus.io/port: "8080"podAnnotations covers the pod templates of Deployments, CronJobs, and Jobs alike.
Default annotations
Both channels follow the chart's merge order: global, then per-resource, then the chart's own. The chart sets a handful of annotations by default, each a behavior switch some controller reads. Most of them render after your annotations and win, so your values cannot clobber them:
| Annotation | Where | Purpose |
|---|---|---|
checksum/variables, checksum/secrets, checksum/sealedSecrets, checksum/files | Deployment pod templates | Roll pods when config data changes. Not rendered on Job and CronJob pods, which are created fresh per run. |
helm.sh/hook, helm.sh/hook-weight, helm.sh/hook-delete-policy | Jobs with a hook: key | Helm lifecycle hooks. Configured through jobs.<name>.hook, never by writing the annotation yourself. |
helm.sh/resource-policy: keep | PVCs (unless keep: false) | Survive helm uninstall. |
helm.sh/hook: test | The connection test Pod | Marks it a helm test hook. |
The one you can override is sealedsecrets.bitnami.com/cluster-wide on SealedSecrets: the global annotations win over it per key, so it is the knob for switching scope:
annotations:
sealedsecrets.bitnami.com/cluster-wide: "false"
sealedsecrets.bitnami.com/namespace-wide: "true"Resources without a values block
The generated singletons, meaning the variables ConfigMap, the secrets Secret, the mounted-files ConfigMap and Secret, and both SealedSecrets, have flat data maps as values, so they carry no per-resource annotations key. They receive the global channel only, as does the Helm test Pod. extraManifests dict mode injects the global annotations into each manifest, with any metadata you provide winning per key; extraManifests list mode is verbatim and receives nothing.