Kubernetes ConfigMaps and Secrets
How to externalize configuration and credentials in Kubernetes — ConfigMap YAML, Secret YAML, mounting as env vars vs files, and patterns for reloading config without rebuilding images.
What you'll learn
- ✓Why configuration should live outside container images
- ✓How to write ConfigMap and Secret YAML and apply it to a cluster
- ✓The difference between mounting as environment variables vs files
- ✓Why base64 encoding in Secrets is not encryption — and how to harden it
- ✓Patterns for reloading config without rebuilding or redeploying images
Prerequisites
- •A working mental model of Pods and Deployments — see Pods, Deployments, and Services
- •kubectl access to a cluster (minikube, kind, or a managed cluster works)
Application code rarely runs the same in development, staging, and production. Database hostnames, log levels, feature flags, and API keys all change. Baking those values into a container image is a fast way to make your images either useless across environments or dangerous to share. Kubernetes solves this with two objects: ConfigMaps for non-sensitive configuration and Secrets for credentials and tokens.
Why externalize config
The classic rule from the Twelve-Factor App is: store config in the environment. The image you ship is the same artifact you ran in CI; the only thing that changes between environments is the configuration you inject at startup.
In Kubernetes that means:
- The container image contains code and default behaviour only
- ConfigMaps and Secrets hold the values that differ per environment
- Pods reference those objects, and the kubelet injects them at runtime
The payoff is a single image that runs everywhere, simpler rollbacks (the image does not change just because a value did), and a clear audit trail of who changed what.
ConfigMap YAML
A ConfigMap is a key-value store living in your cluster. Keys are strings; values are strings (or short binary blobs). Here is a minimal one:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
data:
LOG_LEVEL: "info"
FEATURE_NEW_CHECKOUT: "true"
WELCOME_MESSAGE: "Hello from staging"
Apply it like any other Kubernetes object:
kubectl apply -f app-config.yaml
# configmap/app-config created
kubectl get configmap app-config -o yaml
# output: the same YAML with managed fields added
You can also build one imperatively from a file or from literals:
# From literal key/value pairs
kubectl create configmap app-config \
--from-literal=LOG_LEVEL=info \
--from-literal=FEATURE_NEW_CHECKOUT=true
# From an existing properties or env file
kubectl create configmap app-config --from-env-file=./.env.staging
The result is the same kind of object — useful when you want to keep a real .env file in version control templates.
Secret YAML
Secrets look almost identical, with one twist: the values in data are base64-encoded.
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: default
type: Opaque
data:
DB_PASSWORD: c3VwZXItc2VjcmV0
API_TOKEN: dG9rZW4tMTIzNDU=
To produce those values:
echo -n "super-secret" | base64
# c3VwZXItc2VjcmV0
If you prefer plain text in your YAML, use stringData and Kubernetes will encode it for you:
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
DB_PASSWORD: "super-secret"
API_TOKEN: "token-12345"
base64 is not encryption. Anyone who can
kubectl get secret -o yamlcan decode it instantly. Secrets keep credentials out of container images and out of pod specs — they do not keep them out of the etcd database where Kubernetes stores them. For real protection, enable etcd encryption at rest and lock down RBAC so only the right service accounts can read Secrets.
A more production-grade approach is to integrate with an external secret manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) using the External Secrets Operator. The cluster still ends up with a Secret object, but the source of truth lives somewhere with proper audit logging and rotation.
Mounting as environment variables
The most common pattern is exposing config to a container as environment variables.
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: ghcr.io/acme/web:1.4.2
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secrets
ports:
- containerPort: 8080
envFrom imports every key in the referenced object as an environment variable. Now inside the container, process.env.LOG_LEVEL and process.env.DB_PASSWORD are populated automatically.
If you only want specific keys, or want to rename them, use the longer env form:
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: DB_PASSWORD
The trade-off: environment variables are simple but immutable after the container starts. Updating the ConfigMap will not change the running container’s env — you need to roll the Deployment.
Mounting as files
Mounting as files is the right answer when the application expects a configuration file (NGINX, Postgres, application TOML files), or when you want updates to propagate without restarting the pod.
spec:
containers:
- name: web
image: ghcr.io/acme/web:1.4.2
volumeMounts:
- name: config-volume
mountPath: /etc/app
readOnly: true
- name: secret-volume
mountPath: /etc/app/secrets
readOnly: true
volumes:
- name: config-volume
configMap:
name: app-config
- name: secret-volume
secret:
secretName: app-secrets
Each key in the ConfigMap or Secret becomes a file in the mounted directory:
kubectl exec deploy/web -- ls /etc/app
# FEATURE_NEW_CHECKOUT LOG_LEVEL WELCOME_MESSAGE
kubectl exec deploy/web -- cat /etc/app/LOG_LEVEL
# info
The kubelet updates these files in place when the ConfigMap or Secret changes (typically within a minute, depending on the kubelet sync period). Your application can watch the file and reload — no pod restart required.
Try it yourself. Apply the ConfigMap and Deployment above. kubectl exec into a pod and print $LOG_LEVEL. Edit the ConfigMap with kubectl edit configmap app-config, change LOG_LEVEL to debug, and re-exec — the env var is unchanged. Then mount the same ConfigMap as files, edit it again, and watch /etc/app/LOG_LEVEL update without restarting the pod.
Reloading patterns
Three patterns cover almost every case:
1. Roll the Deployment. The simplest, most reliable approach. Annotate the pod template with a checksum of the ConfigMap so every config change triggers a rollout.
spec:
template:
metadata:
annotations:
checksum/config: "{{ sha256 (toYaml .Values.config) }}"
Helm and Kustomize both make this easy. When the ConfigMap changes, the annotation changes, the pod template hash changes, and Kubernetes rolls new pods.
2. Watch the file. If the app can reload its own config (NGINX reload, an application that watches /etc/app/config.yaml with fsnotify), mount the ConfigMap as files and let the app pick up changes on its own.
3. Use a controller. Projects like Reloader watch ConfigMaps and Secrets for changes and trigger rolling restarts of dependent Deployments automatically. Useful when you cannot easily change the application or its templating.
Namespaces and scope
ConfigMaps and Secrets are namespaced. A Pod can only mount objects from its own namespace. This is mostly a feature — it stops the dev namespace from accidentally pulling prod credentials — but it means you cannot share one Secret across namespaces without a tool that replicates it.
If you need the same Secret in five namespaces, either generate it five times from your secret manager or use a tool like Kubernetes Reflector to replicate it.
Size and shape limits
A ConfigMap or Secret cannot exceed 1 MiB total (an etcd limit). That is plenty for most config files but not enough for, say, a large machine-learning model. If you find yourself bumping into the limit, you are probably trying to ship the wrong thing through ConfigMap — use a Volume or pull from object storage at startup instead.
A complete worked example
Here is a full Deployment using both a ConfigMap and a Secret, with one file mount and one env-var import.
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
data:
app.properties: |
log.level=info
feature.checkout=true
---
apiVersion: v1
kind: Secret
metadata:
name: web-secrets
type: Opaque
stringData:
DATABASE_URL: "postgres://app:pw@db.internal:5432/app"
---
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: ghcr.io/acme/web:1.4.2
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: web-secrets
key: DATABASE_URL
volumeMounts:
- name: config
mountPath: /etc/app
readOnly: true
volumes:
- name: config
configMap:
name: web-config
Apply it with kubectl apply -f web.yaml. The application sees DATABASE_URL in its environment and reads app.properties from /etc/app/app.properties.
Try it yourself. Use the worked example above. Roll the Deployment with kubectl rollout restart deploy/web and watch the new pods come up. Then change a value in the ConfigMap, re-apply, and notice — without that annotation pattern — pods are not restarted. Add the checksum annotation trick and observe the difference on the next change.
Recap
You now know:
- ConfigMaps hold non-sensitive key/value data; Secrets hold credentials and are base64-encoded (not encrypted)
- You can inject either as environment variables (immutable after start) or as files (updated in place)
- Real protection for Secrets means etcd encryption at rest, tight RBAC, and ideally an external secret manager
- Use the checksum annotation or a tool like Reloader to ensure config changes actually trigger pod rollouts
- ConfigMaps and Secrets are namespaced and capped at 1 MiB
These two objects sit underneath almost every real workload in Kubernetes. Once they are second nature, the rest of the platform’s templating and GitOps stories click into place.
Next steps
With config externalized, you are ready to expose the workload to the outside world. The next post in the series covers Ingress and how external traffic finds its way to your Pods.
Useful adjacent reading:
- Kubernetes Ingress: Routing External Traffic — for getting users to your services
- Pods, Deployments, and Services — for the workload primitives below this layer
- What Is Kubernetes? — for the conceptual foundation
Questions or feedback? Email codeloomdevv@gmail.com.