Chapter 6
ConfigMaps & Secrets — Managing App Configuration
Before you read, guessHow do ConfigMaps and Secrets differ in their handling of sensitive data?
Take ten seconds and guess — even a wrong guess makes the answer stick. Tap to see where the chapter lands, or just read on.
ConfigMaps hold config that's safe to be public. Secrets hold sensitive config — but by default they're only base64-encoded, not encrypted. That's a different format, not a lock.
Somewhere in your app's code right now there's probably a line like const dbHost = "prod-db.internal". It made sense the day you wrote it. It's been quietly causing pain ever since. Let's fix that the Kubernetes way.
The problem: your image shouldn't know where it's running
In Post 4 you built a Deployment and rolled out an update. The whole point of that exercise: you build one container image — the packaged app plus everything it needs to run — and promote that exact same image through dev, staging, and prod, unchanged. That promise breaks the moment you hard-code an environment-specific value into the image: a database URL, a feature flag, an API key, a log level. Now your "prod image" and "staging image" are actually two different files, you're rebuilding just to flip one flag, and nobody can be sure what passed tests in staging is really what's running in prod.
The fix: pull configuration out of the image, hand it to the app when it starts. Kubernetes gives you two objects for this. A ConfigMap holds config that's fine for anyone to see. A Secret holds config you need to keep private.
ConfigMap: config as its own object
A ConfigMap is a key-value store — a simple list of names paired with values, like APP_ENV: production — that lives as its own object, separate from your Pod. You create it once. Any Pod that needs those values just references the ConfigMap by name. Need to point your app at a different environment? Edit the ConfigMap. No image rebuild, no code change.
It's for values that are fine to be public: an environment name, a log level, a feature-flag toggle, the address of another service. If someone reads it over your shoulder, nothing bad happens.
Secret: same idea, for the stuff that matters if it leaks
A Secret works the same way — key-value pairs, referenced by name — but it's for values where exposure is a real problem: passwords, API tokens, TLS certificates. Kubernetes stores Secrets as a separate object type, and kubectl hides the values by default when you print one.
Here's the part that trips people up, so I'll say it plainly: by default, a Secret's values are base64-encoded, not encrypted. Those are not the same thing. Encoding just rewrites data in a different format — anyone can reverse it, no key needed. Encryption scrambles data so only someone holding the right key can read it back. Base64 is encoding, not encryption: one command turns it back into the original text, no password required. All it does is stop a value from showing up as plaintext when you casually run kubectl get. It does nothing against someone with real access to the cluster, or to etcd — the database Kubernetes stores all its objects in.
So what actually protects a Secret? Three layers. First, RBAC — the rules deciding which users and service accounts may run commands like kubectl get secret — locked down so only whoever truly needs a value can read it. A Secret nobody can read is safe no matter how it's encoded. Second, encryption at rest turned on for etcd, so the raw data isn't sitting on disk as plaintext. Third, in production, a dedicated secrets tool: something like Sealed Secrets, for storing encrypted secrets safely in Git, or GCP Secret Manager, which you'll meet later in this series. Kubernetes Secrets are the right shape for wiring values into Pods — the storage behind them just isn't real protection on its own. Don't let "it's a Secret" fool you into thinking "it's encrypted."
Two ways to get values into a Pod
Whether it's a ConfigMap or a Secret, a Pod can consume it in one of two ways:
As environment variables. The values get injected into the container's environment when it starts — the same kind of environment variables your app already reads. Simple, and it's what most apps expect. The catch: they're set once, at process start. Update the ConfigMap or Secret afterward, and a running container has no idea — it holds the old values in memory until something restarts it.
As a mounted volume. A volume, in Kubernetes terms, is just storage attached to a container's filesystem — we'll cover it properly in the next post. Here, each key shows up as a file inside the container, with the file's contents equal to the value. The advantage: Kubernetes updates these files live when the ConfigMap or Secret changes — no restart needed. Genuinely useful for something like a rotated TLS certificate. The catch mirrors the first one: the file updating doesn't mean your app notices. Unless your app is actively watching that file and reloading it, it's still running on whatever it read at startup.
Neither approach reloads your app for you. The real difference is where the update lands, and how fast.
Hands-on: create, inspect, decode
Create a ConfigMap the quick, imperative way, setting keys and values directly with --from-literal:
$ kubectl create configmap app-config \
--from-literal=APP_ENV=production \
--from-literal=LOG_LEVEL=info
configmap/app-config created
Look at what got stored:
$ kubectl get configmap app-config -o yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
data:
APP_ENV: production
LOG_LEVEL: info
Plain text, sitting right there in the object. Exactly what you'd expect for a recipe card taped to the wall. Now let's create a Secret:
$ kubectl create secret generic db-secret \
--from-literal=DB_PASSWORD=supersecret
secret/db-secret created
$ kubectl get secret db-secret -o yaml
apiVersion: v1
kind: Secret
metadata:
name: db-secret
namespace: default
type: Opaque
data:
DB_PASSWORD: c3VwZXJzZWNyZXQ=
That string, c3VwZXJzZWNyZXQ=, looks like ciphertext if you don't know better. It isn't — it's base64. Prove it to yourself:
$ kubectl get secret db-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
supersecret
One pipe, and you're back to plaintext. No key, no password. That's the "drawer isn't really locked" point, right on your screen — the value was never turned into ciphertext, just rewritten in a different format.
Wiring both into a Pod
Here's a Pod spec that does both: pulls every key from the ConfigMap in via envFrom, and pulls one key out of the Secret via valueFrom.secretKeyRef:
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: myapp:1.0
envFrom:
- configMapRef:
name: app-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: DB_PASSWORD
envFrom dumps every key from the ConfigMap into the environment, keeping the same key names — fast when you want the whole set at once. valueFrom.secretKeyRef does the opposite: it pulls one named key out of a Secret and lets you rename it as it lands in the container, which is exactly the precision you want for something sensitive.
envFrom. "Inject this one key as an environment variable named X" means env plus valueFrom.secretKeyRef. Mix the two up and you fail the task. The next trap sits right beside it: editing a ConfigMap does not restart Pods already using it as env vars. The Pod keeps old values until its containers get recreated — on a Deployment, run kubectl rollout restart deployment <name> (the command from Post 4) after updating the config. Don't just edit the ConfigMap and assume the change is live.Key Takeaways
- Keep configuration out of your container image, so the same image can move through dev, staging, and prod unchanged.
- ConfigMaps hold config that's safe to be public. Secrets hold sensitive config — but by default they're only base64-encoded, not encrypted. That's a different format, not a lock.
- Real protection for a Secret comes from RBAC, encryption at rest on etcd, and, in production, a dedicated tool like Sealed Secrets or GCP Secret Manager.
- A Pod can consume a ConfigMap or Secret as env vars (simple, but frozen at container start) or as a mounted volume (updates live, but your app still has to notice).
envFrominjects an entire ConfigMap;valueFrom.secretKeyRefinjects one specific Secret key — know the syntax difference cold for the exam.- Updating a ConfigMap does not restart Pods using it as env vars — run
kubectl rollout restart deploymentto pick up the new values.