Transformers: images, names, labels
Cross-cutting edits declaratively.
A patch is a scalpel. It reaches into one field of one resource and changes exactly that, which is what the patches lesson was about. A transformer is a stencil. You lay it over the whole build and it presses the same edit onto everything at once: retag every image, put a prefix on every name, stamp a label on every object. Kustomize ships a handful of these built in, switched on as plain top-level keys in kustomization.yaml (the little file that tells Kustomize what to assemble). The point is that a fleet-wide change lives in one readable, reviewable place instead of scattering into a dozen near-identical patches you then have to keep in sync by hand.
The base everything sits on
Every example below edits the same starting point: a base (the complete, valid set of manifests every environment starts from) with one Deployment (the Kubernetes object that runs and keeps your pods alive) and one ConfigMap (a small store of non-secret settings). YAML, by the way, is the plain-text format Kubernetes reads its objects from. The Deployment pulls its settings from the ConfigMap by name. Hold onto that link, because it is what makes reference rewriting easy to see later.
apiVersion: apps/v1kind: Deploymentmetadata:name: webspec:replicas: 2selector:matchLabels:app: webtemplate:metadata:labels:app: webspec:containers:- name: nginximage: nginx:1.25.3envFrom:- configMapRef:name: app-config
apiVersion: v1kind: ConfigMapmetadata:name: app-configdata:LOG_LEVEL: info
Retag every image from one line
The images transformer rewrites container image references wherever they show up: Deployments, StatefulSets, CronJobs, init containers, all of them, matched by the original image name written in the base. This is how a production overlay pins a specific tag or swaps to a hardened internal registry (the server that stores your container images) without ever touching the base. Each entry matches by name, then sets a newName (a different repo or registry), a newTag, or a digest. Because the base never hard-codes an environment's tag, your pipeline can bump one line in the prod overlay and leave dev and staging exactly where they were.
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- ../../baseimages:- name: nginx # match any container running 'nginx'newName: registry.example.com/prod/nginxnewTag: "1.27.3"
kubectl kustomize overlays/prod
apiVersion: v1data:LOG_LEVEL: infokind: ConfigMapmetadata:name: app-config---apiVersion: apps/v1kind: Deploymentmetadata:name: webspec:replicas: 2selector:matchLabels:app: webtemplate:metadata:labels:app: webspec:containers:- envFrom:- configMapRef:name: app-configimage: registry.example.com/prod/nginx:1.27.3name: nginx
One thing about that output before we go on: kustomize prints every object's fields in alphabetical order, so data lands above kind, and envFrom sits above image and name. That reordering is cosmetic, and the base file you wrote never changes. Now the matching itself. It is exact on the image name, not fuzzy. An entry for nginx rewrites every container whose image is nginx at any tag, and ignores everything else, so one overlay can retag a dozen services at once by listing a dozen entries. If a name in your base matches no entry, that image passes through unchanged, which is usually what you want, but it also means a typo in the entry name silently does nothing. Render the output and read it before you trust it.
A tag like 1.27.3 is a sticky note on a box. Someone with write access to the registry can peel it off and stick it on a different box, and your next rollout would happily run whatever the tag now points at. That is a real supply-chain move: compromise the registry or the build, repoint a mutable tag, wait for a redeploy. A digest closes that door. It is a SHA-256 fingerprint (a hash computed from the image's exact bytes), so the name resolves to one specific image and nothing else. Pin production by digest and a swapped tag will not match.
images:- name: nginxnewName: registry.example.com/prod/nginxdigest: sha256:5b8c...c2e # pin by content, not a movable tag
kubectl kustomize overlays/prod | grep image:
image: registry.example.com/prod/nginx@sha256:5b8c...c2e
Setting both a newTag and a digest is allowed. Kustomize does not complain; it emits nginx:1.27.3@sha256:... where the digest governs which image actually runs and the tag rides along as a human-readable label for whoever reads the manifest. Do not read that tag as a guarantee, though. It is a comment on the box, while the digest is the lock. If they ever disagree, the digest is what runs.
Rename the whole set, keep the wiring intact
namePrefix and nameSuffix rename every resource in the build, and here is the part people miss, they rewrite the references between those resources too. So a Deployment that names a ConfigMap, or a Service that fronts a workload, still points at the right thing after the rename. The namespace transformer sets metadata.namespace (which named partition of the cluster an object lives in) on every object in the build and repairs namespaced references the same way. Put together, these let two copies of one base live in a single cluster, a prod- copy and a staging- copy side by side, without a single hand-edited name. Generators (which build ConfigMaps and Secrets for you, covered earlier) get the prefix as well, and their content-hash suffix stacks on top of your nameSuffix, so the name still changes whenever the data does.
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- ../../basenamePrefix: prod-nameSuffix: -v2namespace: production
kubectl kustomize overlays/prod
apiVersion: v1data:LOG_LEVEL: infokind: ConfigMapmetadata:name: prod-app-config-v2namespace: production---apiVersion: apps/v1kind: Deploymentmetadata:name: prod-web-v2namespace: productionspec:replicas: 2selector:matchLabels:app: webtemplate:metadata:labels:app: webspec:containers:- envFrom:- configMapRef:name: prod-app-config-v2image: nginx:1.25.3name: nginx
Read the output closely. The Deployment became prod-web-v2, the ConfigMap became prod-app-config-v2, both landed in the production namespace, and the Deployment's configMapRef was rewritten to the new ConfigMap name on its own. You changed three lines, and Kustomize kept the wiring correct. Notice what it did not touch: the selector (the label query that tells a Deployment which pods are its own) and the pod labels still say app: web. Names and labels are separate concerns, and namePrefix stays in its lane.
One caveat on namespace. It moves the objects and fixes references inside this build, but it will not chase a name you hard-coded as a string somewhere it cannot see, like a service URL written into a ConfigMap value. Kustomize rewrites structure it understands, not free text. If your app talks to app-config.default.svc by a literal string, that string is yours to keep in sync.
Stamp labels and annotations across the fleet
labels and commonAnnotations write metadata onto every object in the build. This is the declarative way to satisfy a rule like every resource carries an owner and a part-of tag for the service graph. The modern labels field takes a list of pairs plus an includeSelectors flag; leave it false (the default) and it touches metadata only. The replicas transformer overrides the pod count for a named workload, which keeps a hard number out of the base so each overlay scales on its own. commonAnnotations behaves a little differently: it also copies onto the pod template, so restarts and rollouts carry the annotation down to the pods themselves.
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- ../../baselabels:- pairs:app.kubernetes.io/part-of: checkoutenv: prodincludeSelectors: false # default: metadata only, never selectorscommonAnnotations:team: paymentsreplicas:- name: web # target the workload by namecount: 5
kubectl kustomize overlays/prod # trimmed to the Deployment
apiVersion: apps/v1kind: Deploymentmetadata:annotations:team: paymentslabels:app.kubernetes.io/part-of: checkoutenv: prodname: webspec:replicas: 5selector:matchLabels:app: webtemplate:metadata:annotations:team: paymentslabels:app: webspec:containers:- envFrom:- configMapRef:name: app-configimage: nginx:1.25.3name: nginx
The Deployment now carries the two labels and the team annotation on its own metadata, the annotation reached the pod template, and replicas is 5. And notice: spec.selector.matchLabels is still app: web, and so are the pod template labels. That untouched selector is the whole point of includeSelectors: false. If you do want the labels on pods without touching selectors, that is what the separate includeTemplates flag is for. Keep those two flags distinct in your head, because selectors are the ones you can never safely change on a live object.
Before you apply any of this, preview it. kubectl diff -k overlays/prod renders the overlay and diffs it against what is live, so you see the exact change first. If that diff shows spec.selector shifting on a Deployment that already exists, stop, because the apply will fail with 'field is immutable' and you will be reaching for a delete and recreate on a running service. The same habit catches an image that lost its digest or a namespace that moved by accident. Look before you apply.
Make the digest rule something a machine enforces, not something you remember. Add one line to your continuous integration pipeline (the automated build-and-ship system): kubectl kustomize overlays/prod | grep -E 'image:', and fail the job if any line is missing @sha256:. From that point on an unpinned image cannot reach production without someone deliberately turning the check off.
Try this
Run kubectl kustomize overlays/prod on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: commonLabels writes into immutable selectors. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.