08 · ConfigMaps & Secrets¶
Not run against a live cluster
Manifests and output below follow documented ConfigMap/Secret behavior; not executed against a live cluster in this session.
Why externalize configuration¶
Baking configuration (database URLs, feature flags, API keys) into a container image means rebuilding the image for every environment (dev/staging/prod) or every config change. Kubernetes gives you two objects to externalize this instead:
- ConfigMap — non-sensitive configuration data (URLs, feature flags, whole config files).
- Secret — the same mechanism, intended for sensitive data (passwords,
tokens, TLS certs), stored base64-encoded and treated specially by
Kubernetes (excluded from
kubectl describeoutput, encryptable at rest).
Both can be consumed by Pods the same two ways: as environment variables or as mounted files.
Creating a ConfigMap¶
Declaratively:
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
data:
APP_ENV: "production"
LOG_LEVEL: "info"
greeting.txt: |
Hello from a ConfigMap-mounted file!
This can be any multi-line text content.
Imperatively (handy for quick experiments):
kubectl create configmap web-config \
--from-literal=APP_ENV=production \
--from-literal=LOG_LEVEL=info \
--from-file=greeting.txt
Consuming a ConfigMap as environment variables¶
# deployment-with-config.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27-alpine
envFrom:
- configMapRef:
name: web-config
env:
- name: SINGLE_VALUE
valueFrom:
configMapKeyRef:
name: web-config
key: LOG_LEVEL
envFrom.configMapRef injects every key in the ConfigMap as an env var
(APP_ENV, LOG_LEVEL, and greeting.txt — note that non-identifier keys
like a filename with a dot are technically injected but awkward as env var
names, which is why file-like content is usually mounted as a volume
instead, below). env[].valueFrom.configMapKeyRef pulls in a single,
specifically named key.
kubectl exec deploy/web -- env | grep -E 'APP_ENV|LOG_LEVEL|SINGLE_VALUE'
# APP_ENV=production
# LOG_LEVEL=info
# SINGLE_VALUE=info
Consuming a ConfigMap as mounted files¶
spec:
containers:
- name: web
image: nginx:1.27-alpine
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: web-config
Every key in the ConfigMap becomes a file in /etc/config — APP_ENV,
LOG_LEVEL, and greeting.txt, each with the key's value as file content.
This is the natural fit for mounting a whole config file (e.g. an nginx
.conf, a .properties file) rather than exploding it into individual env
vars.
kubectl exec deploy/web -- cat /etc/config/greeting.txt
# Hello from a ConfigMap-mounted file!
# This can be any multi-line text content.
Creating a Secret¶
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # base64 of "admin"
password: c3VwZXJzZWNyZXQ= # base64 of "supersecret"
data values must be base64-encoded (not encrypted — this is important:
base64 is trivially reversible, so a Secret's confidentiality depends on
Kubernetes RBAC restricting who can read it, plus encryption-at-rest for
etcd, not on the encoding itself). Generate the values:
Or use stringData for plain-text convenience (Kubernetes base64-encodes it
for you on write):
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
username: admin
password: supersecret
Imperatively:
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=supersecret
Consuming a Secret¶
Identical shape to ConfigMaps, just secretKeyRef / secret instead:
spec:
containers:
- name: web
image: nginx:1.27-alpine
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumeMounts:
- name: db-creds-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: db-creds-volume
secret:
secretName: db-credentials
kubectl describe secret db-credentials deliberately does not print
values, only key names — a small guardrail against accidental exposure on
screen-shares/logs.
Important caveats¶
- Base64 is encoding, not encryption. Anyone who can
kubectl get secret -o yamlcan decode it. Real secret hygiene needs RBAC (Level 3) restricting read access, and often an external secrets manager (Vault, cloud KMS-backed secret stores) integrated via a CSI driver or operator — out of scope for this level, but worth knowing exists. - Env-var Secrets are visible via
kubectl exec ... -- envand can leak into crash dumps/logs more easily than mounted-file Secrets — many teams prefer mounting Secrets as files for anything sensitive. - Updating a ConfigMap/Secret does not automatically restart Pods that already mounted it as env vars (env vars are set once, at container start). Mounted files do eventually update in-place (kubelet syncs periodically), but your app still needs to notice and reload — a common pattern is a sidecar or app-level file-watcher for hot-reload.
How It Actually Works¶
The caveats above all trace back to two very different delivery mechanisms hiding behind what looks like one feature:
- Env vars are injected once, at container-create time, by the
kubelet — not refreshed. When the kubelet builds the CRI
CreateContainerRequestfor a Pod, it resolves everyvalueFrom.configMapKeyRef/secretKeyRefby making a synchronousGETto the API server at that moment and bakes the resolved values into the container's environment. This happens exactly once per container start, which is precisely why editing the source ConfigMap/Secret afterward has zero effect until the Pod is recreated — there's no live binding, only a one-time copy. - Mounted ConfigMaps/Secrets are a live, syncing volume, not a
copy. The kubelet mounts these as a
tmpfs-backed volume using its built-in configmap/secret volume plugin, which runs a periodic sync loop (governed by--sync-frequency, and capped by a TTL cache default of ~60 seconds viaConfigMapAndSecretChangeDetectionStrategy) that re-fetches the object from the API server and rewrites the files in place using an atomic symlink swap (write to a new temp directory, then swap the..datasymlink) so your application never observes a half-written file, only an old-complete or new-complete version. - Base64 in
Secret.datais a wire-format requirement, not a security boundary. The Kubernetes API schema requires allSecretstring values to be valid within JSON, sodatafields are base64-encoded purely for that reason; at rest, unless the cluster is configured with encryption-at-rest (anEncryptionConfigurationon the API server that encrypts values before etcd ever writes them to disk — off by default on most clusters), a Secret sits in etcd as base64-decoded-able plaintext, protected only by RBAC on who canget/listit.
Exercise¶
Create the web-config ConfigMap and db-credentials Secret above. Deploy
a single-replica Deployment that consumes LOG_LEVEL as an env var,
mounts the whole ConfigMap at /etc/config, and mounts the Secret at
/etc/secrets. exec into the Pod and verify: env | grep LOG_LEVEL,
cat /etc/config/greeting.txt, and cat /etc/secrets/username (you should
see the decoded plain-text value as file content, since Kubernetes decodes
Secrets automatically when mounting — only the API representation is
base64).