ConfigMap and Secret: Pulling Configuration and Secrets Out of the Image
· tech
📑 Contents
- Why: one image has to run in every environment
- ConfigMap vs Secret: the difference is “secret or not”
- Two ways to inject: environment variables vs mounted files
- In YAML: create the config, then inject it into the Pod
- Reflections
- Separating config from image is the foundation of “build once, run anywhere”
- A Secret is only base64 — the name gives you a false sense of safety
- Good config management makes “changing a setting” not the same as “changing the program”
Over the previous posts you’ve learned to deploy an app and expose it as a service. But a real app is still missing one piece: configuration — database addresses, feature flags, plus passwords, API keys, certificates. There’s an iron rule for these: they must not be hard-coded into the image or the code. K8s externalises configuration with ConfigMaps (non-secret settings) and Secrets (secrets), injecting them into the Pod only at runtime. This post covers why, how the two differ, and how injection works.
Why: one image has to run in every environment
The core reason to separate config from image is one sentence: the image must be “immutable and reusable across environments”. The same my-app:1.0 should run untouched in Development, Staging and Production — the only difference being the configuration injected:
ConfigMap vs Secret: the difference is “secret or not”
The two are used almost identically; both store key-value pairs (or whole config files). The difference is whether what’s inside is secret:
- ConfigMap: non-secret, ordinary settings — database hostname, log level, feature flags, a whole
application.yaml. - Secret: secrets — database passwords, API keys, TLS certificates, tokens.
But here’s the most misunderstood, and most dangerous, trap: a Secret is only base64-encoded by default; it is not encrypted. base64 is “a different representation”, not “a lock” — anyone who can read the Secret can recover the plaintext with one command. So to make Secrets actually secure you need three more things: turn on etcd encryption at rest (so they’re really encrypted at the storage layer), restrict who can read them strictly with RBAC, and in Production connect an external secret manager (Vault, the cloud’s KMS). Treat a Secret as “configuration with access control”, not as “an encrypted safe”, and the false sense of security in the name won’t fool you.
Two ways to inject: environment variables vs mounted files
Config is ready; how does it get into the Pod? Two ways, each with its own fit:
In YAML: create the config, then inject it into the Pod
Turning the two diagrams into actual declarations. First create the ConfigMap (plaintext) and the Secret (note data takes base64 values, or use stringData to write plaintext and let K8s encode it):
apiVersion: v1
kind: ConfigMap
metadata: { name: web-config }
data:
LOG_LEVEL: "info" # non-secret: plaintext as is
application.yaml: | # a whole config file works too
server:
timeout: 30s
---
apiVersion: v1
kind: Secret
metadata: { name: web-secret }
type: Opaque
stringData:
DB_PASSWORD: "s3cr3t" # stringData: write plaintext, K8s base64-encodes on save (still not encrypted)
Then, in the Deployment’s Pod template, demonstrate both injection methods — env pulls into environment variables, volumeMounts mounts as files:
spec:
containers:
- name: web
image: myrepo/web:1.0
env:
- name: LOG_LEVEL # ① env var: pull one key from the ConfigMap
valueFrom: { configMapKeyRef: { name: web-config, key: LOG_LEVEL } }
- name: DB_PASSWORD # same for a secret, with secretKeyRef
valueFrom: { secretKeyRef: { name: web-secret, key: DB_PASSWORD } }
volumeMounts:
- { name: cfg, mountPath: /etc/web } # ② mounted as files: the whole application.yaml appears in this directory
volumes:
- name: cfg
configMap: { name: web-config }
The two contrasts are the point of the second diagram: env/...KeyRef is environment-variable injection (fixed at start; a change needs a Pod restart); volumeMounts + volumes.configMap is mount as files (files refresh after the ConfigMap is updated, but the app has to re-read them). To pour a whole ConfigMap/Secret into environment variables in one go, there’s also envFrom, which saves a lot of lines.
Reflections
Separating config from image is the foundation of “build once, run anywhere”
When I was learning Docker/K8s I did the stupid thing of writing the DB address, even credentials, straight into the image — the result was rebuilding an image every time I changed environments, one for dev, one for prod, a total mess, and I nearly pushed a password to git. What ConfigMap/Secret taught me is a clean boundary: the image owns “code and dependencies”; config owns “where this runs and with what parameters”; keep them apart. The value of that boundary is making “the same artefact runs in every environment” real — the image you validated in Staging goes to Production without a single bit changed, only a different config. It’s the same coin as the “reproducible, portable artefact” of hermetic builds, two faces: one guards the purity of the build artefact, the other the injection of runtime config.
A Secret is only base64 — the name gives you a false sense of safety
The name “Secret” is dangerous because it sounds so safe that people unconsciously assume “put it in a Secret and it’s locked”. But by default it’s only base64; anyone with access can recover the plaintext in a second. The lesson goes beyond K8s: never let a thing’s “name” do your security judgment for you. Now whenever I see a feature claiming “encrypted”, “secure”, “protected”, I ask one more question: “what does it actually do, and whom does it stop” — is it real encryption or just encoding? Does it stop outsiders, or also insiders with permissions? The name is marketing; the actual threat model is engineering. Knowing specifically what a security mechanism blocks and what it doesn’t matters far more than remembering what it’s called.
Good config management makes “changing a setting” not the same as “changing the program”
I increasingly think you can tell how mature a system is by “how painful it is to change one setting”. In an immature system, changing a parameter means touching code, rebuilding, redeploying — every change a big production — so people avoid changing anything and hard-code the settings. In a mature system, config is external and injected — flip a feature flag, adjust a threshold, no code touched, maybe not even a restart (if mounted as files). ConfigMap/Secret make config a first-class citizen, fully separating “adjusting behaviour” from “rewriting logic”, and that separation is itself a form of maintainability. Separating “what changes” (config) from “what mostly doesn’t” (code), so the former can be adjusted cheaply — this isn’t just K8s wisdom; it’s a thread running through every good architecture I’ve seen.