CoursesKustomizeConfigMap & Secret generators

ConfigMap & Secret generators

Generate config with hashes.

Intermediate12 min · lesson 3 of 12

A ConfigMap is one of the simplest objects in Kubernetes (the system that runs your applications, packaged into containers, across a fleet of machines): a bag of plain, non-secret settings stored as key/value pairs. It looks harmless to hand-write. You type the YAML (the plain-text format Kubernetes reads), commit it to Git (the version-control system that records every change to your files), and apply it to the cluster. A week later you change one value, apply again, and nothing happens. The running pods (a pod is one or more containers that run together as a single unit) keep serving the old value. This is the quiet bug that eats an afternoon: the config is right in Git and right in the cluster, but the process inside the container never picked up the change. Kustomize (a tool that assembles and patches Kubernetes YAML without templates) has a generator feature that closes this gap by design, and understanding why it works is the whole lesson.

Generating Instead Of Hand-Writing

Instead of writing a ConfigMap or a Secret (its sensitive-value cousin, meant for passwords, tokens, and keys) by hand, you point Kustomize at source data you already have and let it build the finished object. Three kinds of source cover almost everything: a few inline values typed straight into the config, a real file on disk that you want mounted word for word, and an environment file of KEY=VALUE lines. You declare the source in a file named kustomization.yaml, and the generator turns it into a finished resource at build time.

app.properties
pool.size=8
timeout.ms=3000
db.env
DB_USER=app
DB_PASSWORD=supersecret
kustomization.yaml
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
files:
- app.properties
secretGenerator:
- name: db-creds
envs:
- db.env
terminal
$ kustomize build .
output
apiVersion: v1
data:
LOG_LEVEL: info
app.properties: |
pool.size=8
timeout.ms=3000
kind: ConfigMap
metadata:
name: app-config-6ct58987ht
---
apiVersion: v1
data:
DB_PASSWORD: c3VwZXJzZWNyZXQ=
DB_USER: YXBw
kind: Secret
metadata:
name: db-creds-4d2f9bk7h8
type: Opaque

Two things jump out of that output. The literal value and the file both landed inside the ConfigMap's data. And the names are not the ones you asked for. You wrote app-config and db-creds, but the tool emitted app-config-6ct58987ht and db-creds-4d2f9bk7h8. That trailing gibberish is the whole point of the feature.

The Fingerprint In The Name

A hash is a fingerprint of some data. Feed the same bytes in and you always get the same short string out. Change a single byte and the string comes out completely different, with no resemblance to the one before it. Kustomize computes a hash of the ConfigMap's contents and staples it onto the name as a suffix. So the name is no longer a label you picked. It is a fingerprint of what sits inside. Two ConfigMaps with identical data get the same suffix. Change one character of one value and the suffix changes.

Here is why that fixes the stale-config bug. A Deployment (the Kubernetes object that keeps a set of identical pods running and rolls out updates to them) only replaces its pods when the pod template, the blueprint it stamps each new pod from, actually changes. Think of a building superintendent who sends a repair crew only when the blueprint on file changes. Swap the furniture inside apartment 4B and nobody gets dispatched, because the blueprint still reads the same. Renumber the unit to 4B-v2 and the blueprint now differs, so the crew shows up. Mounting a ConfigMap by a fixed name and editing its contents is swapping the furniture. Baking the fingerprint into the name is renumbering the unit. Kustomize rewrites every reference to point at the hashed name, so when the data changes, the name changes, the pod template changes, and Kubernetes rolls the pods on its own.

Watch A Change Ripple Through

Add a Deployment that reads the ConfigMap two ways, once as environment variables and once as a mounted file, and list it under resources in the same kustomization.yaml.

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
envFrom:
- configMapRef:
name: app-config
volumeMounts:
- name: cfg
mountPath: /etc/app
volumes:
- name: cfg
configMap:
name: app-config
terminal
$ kustomize build . | grep -n app-config
output
9: name: app-config-6ct58987ht
37: name: app-config-6ct58987ht
45: name: app-config-6ct58987ht

You wrote app-config three times in your source: once in the generator, once under envFrom (the field that loads a ConfigMap's keys in as environment variables), and once under volumes. All three came out rewritten to the same hashed name. Now change one value and rebuild.

terminal
$ sed -i 's/LOG_LEVEL=info/LOG_LEVEL=debug/' kustomization.yaml
$ kustomize build . | grep -n app-config
output
9: name: app-config-9hf2t5k8db
37: name: app-config-9hf2t5k8db
45: name: app-config-9hf2t5k8db

One edited value, and every reference moved to a brand-new name in lockstep. When you apply this, the Deployment's pod template now names app-config-9hf2t5k8db where it used to name app-config-6ct58987ht. That is a real change to the template, so Kubernetes runs a rolling update (replacing the pods a few at a time so the app never fully goes down), and the new pods come up reading LOG_LEVEL=debug. No manual restart. No kubectl rollout restart. No forgetting.

How one edited value forces a rollout
1Edit a value
one line in kustomization.yaml or a source file
2Kustomize hashes the data
a fingerprint that changes if any byte changes
3New object name
app-config-<newhash> replaces the old suffix
4References rewritten
envFrom and volumes all repoint to the new name
5Pod template differs
the Deployment now names a resource it didn't before
6Rolling update
Kubernetes replaces pods; new config is live

Secrets Are Encoded, Not Locked

Look again at the Secret in that first build. DB_PASSWORD came out as c3VwZXJzZWNyZXQ=, which feels protected until you decode it. base64 (a reversible text encoding that repacks bytes into a small set of safe characters) is not encryption. It hides nothing from anyone willing to run one command.

terminal
$ echo 'c3VwZXJzZWNyZXQ=' | base64 -d
output
supersecret

A secretGenerator does exactly one transformation. It base64-encodes your values and wraps them in a Secret object. It does not encrypt anything. The encoded blob in the built YAML is trivially reversible, and worse, the source it read from (the db.env file, or literals typed into kustomization.yaml) sits in your repository in clear text the moment you commit it. For a defender, that is the first thing to grep for in a code review: real passwords, tokens, or keys living in an env file next to a kustomization.yaml.

A secretGenerator is not a vault
base64 is encoding, not encryption. Anyone who can read the manifest or run kubectl get secret -o yaml recovers the plain value in seconds. Never commit real secret sources to Git. Encrypt them with SOPS (Secrets OPerationS, a tool that encrypts the values inside a file while leaving the keys readable), or fetch them at deploy time with an external secrets operator, and keep the plain material out of version control entirely.

Tuning The Generators

You can steer the generators with a generatorOptions block. The two knobs you will reach for most are stamping consistent labels and annotations onto everything generated, and turning the hash suffix off. Labels matter more than they look, because they are how you find and clean up these objects later.

kustomization.yaml
generatorOptions:
disableNameSuffixHash: false
labels:
managed-by: kustomize
annotations:
origin: generated

Setting disableNameSuffixHash to true gives you a stable, predictable name, plain app-config with no suffix, which you sometimes need when another tool or a fixed reference outside Kustomize expects a known name. The cost is steep. You have handed back the exact safety you came for. With no changing suffix, editing the data no longer changes the pod template, and your pods go right back to quietly running stale config until something else restarts them. In an overlay (a folder that layers environment-specific changes on top of a shared base configuration) you can also give a generator a behavior of merge or replace, so you patch environment-specific values on top of a base ConfigMap instead of redefining the whole thing.

The Old Copies Pile Up

Every hash change mints a new object, and Kustomize never deletes the previous one for you. Apply your changes a few times over a month and the cluster quietly fills with orphaned generations that nothing mounts anymore.

terminal
$ kubectl get configmap -l managed-by=kustomize
output
NAME DATA AGE
app-config-6ct58987ht 2 6d
app-config-9hf2t5k8db 2 12m

The six-day-old copy is dead weight. The live Deployment points at the twelve-minute-old one, and nothing references the older version. A handful of stale ConfigMaps is harmless clutter. The same pattern with Secrets is not: a password you rotated stays readable inside the old Secret for as long as that object survives, which is real attack surface sitting in plain sight. Neither kustomize build nor a plain kubectl apply -k removes these for you. Prune them with kubectl apply -k . --prune scoped to a label selector, or let a GitOps tool (one that continuously syncs your cluster to match a Git repository), such as Argo CD or Flux, reap them on each sync.

Quick check
01You set disableNameSuffixHash: true, change LOG_LEVEL from info to debug, run kubectl apply -k, and kubectl exec deploy/web -- printenv LOG_LEVEL still prints info. What is going on?
Incorrect — The apply does update the ConfigMap, and you can read debug straight out of it. What never moved is the pod spec, so the running containers kept the environment they were started with.
Correct — Kubernetes replaces pods when the Deployment spec differs from what it applied last time. A constant name keeps that spec byte for byte the same, so nothing rolls and printenv keeps saying info.
Incorrect — A behavior of merge or replace decides how an overlay layers onto a base generator. It has no say in whether an edited literal reaches the built output, because that always happens.
Incorrect — Names resolve exactly, never by age. The Deployment asks for app-config and gets app-config, while any leftover hashed copies just sit there taking up space.
02Your build emits DB_PASSWORD: c3VwZXJzZWNyZXQ= inside the Secret, and a teammate says the password is protected now. Which statement holds up?
Incorrect — kubectl get secret -o yaml hands you that blob, and turning it back into text takes one command. A storage format is not a protection mechanism.
Incorrect — A digest cannot be reversed, but this is not one. Run echo 'c3VwZXJzZWNyZXQ=' | base64 -d and supersecret comes straight back at you.
Correct — The generator only repacks bytes into a safer character set. Both the built manifest and the db.env file it read from give the password up to anyone with repository access.
Incorrect — The suffix exists to move pod templates when data changes. It has no bearing on who can read the value, and the full name is printed in every build anyway.
03kubectl get configmap -l managed-by=kustomize lists app-config-6ct58987ht at 6d and app-config-9hf2t5k8db at 12m, and the live Deployment mounts the 12m one. What is the older object?
Correct — Each new hash mints a fresh object and leaves the previous one untouched. For a ConfigMap that is clutter, but for a Secret it keeps serving a credential you thought you had retired.
Incorrect — There is no queue here. The 12-minute-old object is what the live pods read, and the 6-day-old one is a past build that nothing references any more.
Incorrect — envFrom resolves one exact name, so an extra object cannot confuse it. Deleting the Deployment would take your pods down and leave the leftover sitting exactly where it was.
Incorrect — Neither kustomize build nor kubectl apply -k removes old generations. The label helps you find them, but you still prune with apply --prune scoped to a selector, or let Argo CD or Flux reap them.

Prove The Change Landed

Do not trust that a config change reached the process just because kustomize build printed a new hash. Check it against the running app. First confirm the rollout finished, then read the value straight out of a live pod.

terminal
$ kubectl rollout status deploy/web
$ kubectl exec deploy/web -- printenv LOG_LEVEL
output
deployment "web" successfully rolled out
debug

If printenv still says info after an apply, your pods did not roll, and the first thing to check is whether disableNameSuffixHash got switched on somewhere in your generatorOptions. That one flag is the difference between config that ships and config that quietly rots on old pods.

Try this

Run kustomize build . 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: a secretGenerator is not a vault. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related