ConfigMaps & Secrets
Baking configuration into container images creates a new image for every environment change. Kubernetes separates config from code with two resources: ConfigMaps for non-sensitive configuration (database hostnames, feature flags, nginx configs) and Secrets for sensitive data (passwords, API keys, TLS certificates). Both can be injected into pods as environment variables or as files in the filesystem.
ConfigMaps
A ConfigMap stores key-value pairs. Values can be short strings (a port number, a log level) or entire file contents (an nginx config, a properties file). They're namespace-scoped and unencrypted.
Creating ConfigMaps
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
data:
# Simple key-value pairs
LOG_LEVEL: info
DATABASE_HOST: postgres.production.svc.cluster.local
DATABASE_PORT: "5432"
FEATURE_DARK_MODE: "true"
# File content stored as a value (key = filename)
nginx.conf: |
server {
listen 80;
location /health { return 200 "ok"; }
}
# From literal values
kubectl create configmap app-config \
--from-literal=LOG_LEVEL=info \
--from-literal=DATABASE_HOST=postgres.production
# From a file (key = filename, value = file contents)
kubectl create configmap nginx-config \
--from-file=nginx.conf
# From an env file (KEY=VALUE format, one per line)
kubectl create configmap app-config \
--from-env-file=config.env
Consuming ConfigMaps
Three ways to consume a ConfigMap in a pod:
spec:
containers:
- name: app
image: my-app:latest
# 1. Individual env vars
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
# 2. All keys as env vars at once (envFrom)
envFrom:
- configMapRef:
name: app-config # every key becomes an env var
# 3. As files in the filesystem (via volume)
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
readOnly: true
volumes:
- name: nginx-conf
configMap:
name: nginx-config # each key becomes a file
Secrets
Secrets have the same structure as ConfigMaps but are intended for sensitive values. Data is stored base64-encoded in etcd. The API returns base64 — you must decode to get the raw value.
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
data:
# Values must be base64-encoded
username: cG9zdGdyZXM= # echo -n "postgres" | base64
password: c3VwZXJzZWNyZXQ= # echo -n "supersecret" | base64
stringData:
# Alternative: plain text in stringData (Kubernetes base64-encodes on apply)
api-key: my-raw-api-key-here
# From literals (values stored as-is, no manual base64)
kubectl create secret generic db-credentials \
--from-literal=username=postgres \
--from-literal=password=supersecret
# TLS Secret from cert files
kubectl create secret tls my-tls \
--cert=tls.crt \
--key=tls.key
# Docker registry credentials
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=user \
--docker-password=pass
Secret Types
| Type | Use | Required keys |
|---|---|---|
Opaque | Arbitrary user-defined data (default) | Any |
kubernetes.io/tls | TLS certificate and key | tls.crt, tls.key |
kubernetes.io/dockerconfigjson | Container registry credentials | .dockerconfigjson |
kubernetes.io/service-account-token | ServiceAccount bearer token | Auto-generated by Kubernetes |
kubernetes.io/basic-auth | HTTP Basic Auth credentials | username, password |
kubernetes.io/ssh-auth | SSH private key | ssh-privatekey |
Consuming Secrets
The consumption API is identical to ConfigMaps — env vars, envFrom, or volumes. Volume mounts are preferred for credentials: they don't appear in kubectl describe pod output and can be updated without restarting the pod.
spec:
containers:
- name: app
image: my-app:latest
# Env var from a specific Secret key
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
# All Secret keys as env vars
envFrom:
- secretRef:
name: db-credentials
# As files (volume mount — preferred for creds)
volumeMounts:
- name: db-creds
mountPath: /run/secrets/db
readOnly: true
volumes:
- name: db-creds
secret:
secretName: db-credentials
defaultMode: 0400 # owner read-only
Why Secrets Aren't Secret
get secrets RBAC permission reads them. Scope RBAC tightly — list secrets alone returns all values./proc/<pid>/environ, crash dumps, container inspection. Prefer volume mounts for secrets.Live Update Behavior
Updates to ConfigMaps and Secrets propagate differently depending on how they're consumed:
| Consumption method | Updates automatically? | Delay |
|---|---|---|
| Volume mount (full directory) | Yes | Kubelet sync period (default ~60s) + propagation time |
Volume mount with subPath | No | Requires pod restart |
Environment variable (env) | No | Requires pod restart |
Environment variable (envFrom) | No | Requires pod restart |
For applications that watch the filesystem for config changes (nginx, Prometheus, most 12-factor apps), volume mounts give zero-downtime config updates — the app reloads when it detects file changes. Environment variables require a rolling restart.
External Secrets
For production, store secret values outside the cluster in a dedicated secrets manager and sync them into Kubernetes Secrets automatically. This avoids committing secrets to Git and centralizes rotation:
| Tool | Backend | How it works |
|---|---|---|
| External Secrets Operator (ESO) | AWS Secrets Manager, GCP Secret Manager, Vault, Azure Key Vault, 1Password | CRD-based sync — ExternalSecret polls the backend and creates/updates a Kubernetes Secret |
| Vault Agent Injector | HashiCorp Vault | Sidecar injected by a mutating webhook writes secrets to a shared volume at pod start |
| Secrets Store CSI Driver | AWS, GCP, Azure, Vault | Mounts secrets directly from the provider as a volume — no Kubernetes Secret created |
kubectl Commands
# List ConfigMaps
kubectl get configmap -n production
kubectl get cm -n production # shorthand
# View ConfigMap data
kubectl get cm app-config -n production -o yaml
# Edit a ConfigMap in-place
kubectl edit cm app-config -n production
# List Secrets (data is not shown in output)
kubectl get secret -n production
# Decode a Secret value
kubectl get secret db-credentials -n production \
-o jsonpath='{.data.password}' | base64 -d
# Compare the current ConfigMap to what's mounted in a running pod
kubectl exec deploy/app -n production -- cat /etc/config/app.properties
kubectl get cm app-config -n production -o jsonpath='{.data.app\.properties}'
# Delete and recreate a ConfigMap (to force a volume update)
kubectl delete cm app-config -n production
kubectl apply -f app-config.yaml