This is the third post in our Kubernetes fundamentals series, building on Pods and Deployments and networking.
Baking configuration values directly into a container image means rebuilding the image every time a value needs to change — the same problem we covered for Terraform variables and Ansible variables earlier in this blog. Kubernetes' answer is two related objects: ConfigMaps for ordinary configuration, and Secrets for anything sensitive — though the actual difference between them is smaller than most people assume.
ConfigMaps: externalizing ordinary configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
FEATURE_FLAG_NEW_UI: "true"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: my-app:1.0
envFrom:
- configMapRef:
name: app-configenvFrom with configMapRef injects every key in the ConfigMap as an environment variable inside the container — LOG_LEVEL, MAX_CONNECTIONS, and FEATURE_FLAG_NEW_UI are all available to the application exactly as if they'd been set with a plain environment: block, without rebuilding the image to change any of them.
Secrets: structurally almost identical, meaningfully different in intent
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
DB_PASSWORD: UzNjdXIzLUFjdHVhbC1QYXNzd29yZA== containers:
- name: web
image: my-app:1.0
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secretsThe value under data is base64-encoded, not encrypted. Base64 is an encoding, not a cipher — anyone with kubectl get access to that Secret can trivially recover the original value:
kubectl get secret app-secrets -o jsonpath='{.data.DB_PASSWORD}' | base64 -dS3cur3-Actual-Password
This is the single most common misunderstanding people carry about Kubernetes Secrets: the name implies protection the object doesn't actually provide by default. What a Secret does give you, that a ConfigMap doesn't, is: Kubernetes avoids logging its value in most standard output, and — critically, but only if you've configured it — encryption at rest in etcd (the cluster's backing datastore), which is not automatically enabled on every Kubernetes installation and needs to be verified, not assumed.
kubectl get secret app-secrets -o yamlIf that command, run by anyone with reasonable cluster access, immediately reveals a recoverable password — as it does by default — the real fix is usually reaching for a proper secrets-management integration rather than treating a raw Kubernetes Secret as sufficient on its own:
- External Secrets Operator, syncing values from AWS Secrets Manager, HashiCorp Vault, or a similar external store into Kubernetes Secrets automatically, so the actual secret value never has to be manually base64-encoded and committed anywhere.
- Sealed Secrets, which lets you commit an encrypted version of a Secret to git safely — decryptable only by a controller running inside the specific target cluster.
Either approach addresses the actual gap: encrypting the value before it's stored as a Kubernetes object, rather than trusting the object's own default protections.
Mounting configuration as files instead of environment variables
Not every application reads configuration from environment variables — some expect a config file at a specific path. Both ConfigMaps and Secrets support this via volume mounts:
containers:
- name: web
image: my-app:1.0
volumeMounts:
- name: config-volume
mountPath: /etc/app/config
volumes:
- name: config-volume
configMap:
name: app-configEach key in the ConfigMap becomes a separate file inside /etc/app/config (a file named LOG_LEVEL containing the text info, for example) — the right approach for an application expecting a real config file on disk, rather than environment variables it has to read individually.
A detail worth knowing: updates don't always restart anything
Update a ConfigMap that's mounted as a volume, and the file inside the running Pod does eventually update automatically (subject to a caching delay, typically under a minute) — but the application itself doesn't automatically reload it unless it's specifically written to watch that file for changes. An application reading environment variables from envFrom gets none of this: those values are set once, at container start, and a ConfigMap update has no effect at all on an already-running container using that mechanism — the Pod needs to be recreated (a rolling restart of the Deployment) to pick up the new values.
What to actually remember from this post
- ConfigMaps and Secrets are structurally almost identical — both inject key-value data as environment variables or mounted files.
- A Secret's
datais base64-encoded, not encrypted — anyone with reasonablekubectlaccess can trivially decode it, unless encryption-at-rest and access controls are deliberately configured. - For genuinely sensitive values, integrate a real secrets manager (External Secrets Operator, Sealed Secrets) rather than relying on a raw Secret object's default behavior.
- Environment-variable-based config requires a Pod restart to pick up changes — mounted-file config updates eventually, but the application still needs to actually watch for that change to benefit from it.
Next in the series: Kubernetes Storage: Volumes, PVCs, and StorageClasses, where we cover how an application keeps data that needs to outlive any individual Pod.
