Hardcoding config into your container image is the original sin of containerised applications. Here’s how Kubernetes gives you a better way — and how to avoid the traps that catch most engineers.
Kubernetes Zero to Hero · Episode 05
In Episode 04, we connected pods to the world with Services. Your applications can now find each other and receive external traffic. But they still have a problem most engineers don’t think about until it bites them in production.
Where does your app’s configuration live?
Database hostnames. API keys. Feature flags. Log levels. Port numbers. Environment-specific URLs. Every application has configuration, and that configuration changes between environments — dev, staging, production. If it’s baked into your container image, you need a different image for every environment. That’s not scalable, and it’s a security disaster waiting to happen.
Kubernetes solves this with two objects: ConfigMaps for non-sensitive configuration, and Secrets for sensitive data. This episode covers both — how to create them, how to inject them into pods, and the security practices that separate careful operators from careless ones.
The Problem With Hardcoded Config
Let’s be concrete about what we’re trying to avoid.
# Don't do this
FROM node:18
ENV DATABASE_URL=postgres://prod-db.internal:5432/myapp
ENV API_KEY=sk-live-abc123supersecret
ENV LOG_LEVEL=error
COPY . .
CMD ["node", "server.js"]
This image is married to production. You can’t use it in staging without rebuilding. Your API key is now baked into a layer of your image — visible to anyone who can pull it. When the key rotates, you rebuild and redeploy.
The twelve-factor app methodology got this right in 2012: configuration that varies between deployments should be stored in the environment, not the code. Kubernetes makes this native.
ConfigMaps: Externalising Non-Sensitive Config
A ConfigMap is a Kubernetes object that stores non-sensitive configuration data as key-value pairs. Your pods consume it at runtime — not at build time.
Creating a ConfigMap
Method 1: From a YAML manifest (most common in production)
# app-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
data:
# Simple key-value pairs
LOG_LEVEL: "info"
APP_PORT: "8080"
ENVIRONMENT: "production"
# Multi-line values — great for config files
app.properties: |
server.port=8080
logging.level=info
feature.dark-mode=true
cache.ttl=3600 nginx.conf: |
server {
listen 80;
location / {
proxy_pass http://backend-service:8080;
}
}
Method 2: From the command line
# From literal values
$ kubectl create configmap app-config \
--from-literal=LOG_LEVEL=info \
--from-literal=APP_PORT=8080
# From a file (key = filename, value = file contents)
$ kubectl create configmap nginx-config \
--from-file=nginx.conf# From an entire directory
$ kubectl create configmap app-configs \
--from-file=./config-dir/
$ kubectl apply -f app-config.yaml
$ kubectl get configmap app-config
NAME DATA AGE
app-config 5 3s# Inspect the contents
$ kubectl describe configmap app-config
$ kubectl get configmap app-config -o yaml
Injecting ConfigMaps into Pods
There are three ways to consume a ConfigMap in a pod. Each has its place.
Method 1: Environment Variables (individual keys)
# pod-with-env.yaml
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: my-app:1.0
env:
- name: LOG_LEVEL # env var name inside the container
valueFrom:
configMapKeyRef:
name: app-config # ConfigMap name
key: LOG_LEVEL # key inside the ConfigMap
- name: APP_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PORT
Method 2: All keys as Environment Variables (bulk inject)
spec:
containers:
- name: app
image: my-app:1.0
envFrom:
- configMapRef:
name: app-config # injects ALL keys as env vars at once
⚠️
envFromis convenient but dangerous at scale. If someone adds a key to the ConfigMap that conflicts with a system environment variable, your container behaviour changes silently. Prefer explicitenvmappings for production workloads where predictability matters.
Method 3: Mounted as a File (best for config files)
spec:
containers:
- name: nginx
image: nginx:1.25
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/conf.d # directory inside container
volumes:
- name: config-volume
configMap:
name: app-config
items:
- key: nginx.conf # which key to mount
path: default.conf # filename inside mountPath
# Verify the file is mounted correctly
$ kubectl exec -it <pod-name> -- cat /etc/nginx/conf.d/default.conf
💡 Hot reloading: Files mounted from ConfigMaps are updated automatically when the ConfigMap changes — typically within 60 seconds. Environment variables are NOT updated on running pods — you need a pod restart. Design your apps accordingly.
Secrets: Sensitive Data Deserves Different Treatment
A Secret is Kubernetes’ mechanism for storing sensitive data — passwords, tokens, TLS certificates, SSH keys. It works similarly to a ConfigMap but with important differences:
- Stored separately from ConfigMaps in etcd
- Not shown in
kubectl describe podoutput (values are masked) - Can be configured to only be distributed to nodes that need them
- Base64 encoded by default (not encrypted — we’ll address this)
Creating Secrets
Method 1: From the command line (avoids storing secrets in YAML files)
# Generic secret from literal values
$ kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=s3cr3tP@ssw0rd
# From files (great for TLS certs, SSH keys)
$ kubectl create secret generic tls-certs \
--from-file=tls.crt=./server.crt \
--from-file=tls.key=./server.key# Built-in type for TLS
$ kubectl create secret tls my-tls-secret \
--cert=./server.crt \
--key=./server.key
Method 2: From a YAML manifest
# db-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
# Values MUST be base64 encoded
username: YWRtaW4= # echo -n 'admin' | base64
password: czNjcjN0UEBzc3cwcmQ= # echo -n 's3cr3tP@ssw0rd' | base64
# Encode values yourself
$ echo -n 'admin' | base64
YWRtaW4=
$ echo -n 's3cr3tP@ssw0rd' | base64
czNjcjN0UEBzc3cwcmQ=
⚠️ Base64 is encoding, not encryption. Anyone with access to the Secret object can decode the value instantly:
echo 'YWRtaW4=' | base64 --decode. Do not commit Secret YAML files containing real credentials to version control — ever. We'll discuss proper secret management approaches at the end of this article.
Injecting Secrets into Pods
The injection methods mirror ConfigMaps exactly:
# secret-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-secrets
spec:
containers:
- name: app
image: my-app:1.0
# Method 1: Individual env vars from Secret
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password # Method 2: Bulk inject all Secret keys as env vars
envFrom:
- secretRef:
name: db-credentials # Method 3: Mount as files
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true # always readOnly for secrets volumes:
- name: secret-volume
secret:
secretName: db-credentials
defaultMode: 0400 # restrictive file permissions (owner read-only)
$ kubectl apply -f secret-pod.yaml
# Secrets are masked in describe output
$ kubectl describe pod app-with-secrets
# Environment:
# DB_USERNAME: <set to the key 'username' in secret 'db-credentials'>
# DB_PASSWORD: <set to the key 'password' in secret 'db-credentials'># But you CAN read the value if you have permission
$ kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 --decode
s3cr3tP@ssw0rd
Secret Types
Kubernetes has several built-in Secret types for specific use cases:
Type Use Case Opaque Default. Arbitrary user-defined data. kubernetes.io/tls TLS certificate and key. Used by Ingress. kubernetes.io/dockerconfigjson Docker registry credentials for pulling private images. kubernetes.io/service-account-token Auto-created for ServiceAccounts. kubernetes.io/ssh-auth SSH private key. kubernetes.io/basic-auth Username and password credentials.
Private Registry Pull Secrets
A common real-world need: pulling images from a private container registry.
# Create a registry pull secret
$ kubectl create secret docker-registry registry-creds \
--docker-server=registry.example.com \
--docker-username=myuser \
--docker-password=mypassword \
--docker-email=me@example.com
# Reference it in your pod spec
spec:
imagePullSecrets:
- name: registry-creds # Kubernetes uses this to authenticate when pulling images
containers:
- name: app
image: registry.example.com/myapp:1.0
A Complete Real-World Example
Let’s wire it all together — a web application with a database backend, using ConfigMaps for app config and Secrets for credentials:
# Full stack: ConfigMap + Secret + Deployment
---
apiVersion: v1
kind: ConfigMap
metadata:
name: webapp-config
data:
DB_HOST: "postgres-service"
DB_PORT: "5432"
DB_NAME: "myapp"
LOG_LEVEL: "info"
MAX_CONNECTIONS: "10"
---
apiVersion: v1
kind: Secret
metadata:
name: webapp-secrets
type: Opaque
data:
DB_PASSWORD: cGFzc3dvcmQxMjM= # password123
JWT_SECRET: c3VwZXJzZWNyZXRrZXk= # supersecretkey
API_KEY: YWJjMTIzZGVmNDU2= # abc123def456
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: my-webapp:2.1
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: webapp-config # inject all config as env vars
env:
- name: DB_PASSWORD # inject secrets individually
valueFrom:
secretKeyRef:
name: webapp-secrets
key: DB_PASSWORD
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: webapp-secrets
key: JWT_SECRET
- name: API_KEY
valueFrom:
secretKeyRef:
name: webapp-secrets
key: API_KEY
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
The Hard Truth About Kubernetes Secrets
Here’s what most tutorials don’t tell you: Kubernetes Secrets are not truly secret by default.
The problems:
- etcd stores Secrets in base64 — not encrypted at rest unless you explicitly configure encryption. Anyone with direct etcd access reads your secrets in plaintext.
- YAML manifests with Secrets shouldn’t live in Git. But everyone needs to track their infrastructure as code. This is a genuine tension.
- RBAC controls who can read Secrets, but the default permissions are often too permissive in clusters set up quickly.
For most production workloads, the pragmatic path is:
- Enable etcd encryption at rest (cluster-level setting — ask your cloud provider or set it yourself)
- Use External Secrets Operator to sync secrets from AWS Secrets Manager, GCP Secret Manager, or Azure Key Vault into Kubernetes Secrets automatically
- Lock down RBAC so only the pods that need a Secret can access it (covered in Episode 08)
- Never commit Secret YAML with real values to version control — use Sealed Secrets or a
.gitignoreguard
# Enable encryption at rest check (kubeadm clusters)
$ kubectl get apiserver -o yaml | grep encryption
# List who can read secrets in your cluster (important audit)
$ kubectl auth can-i get secrets --all-namespaces --as=system:serviceaccount:default:default
Immutable ConfigMaps and Secrets
For performance and safety, mark ConfigMaps and Secrets as immutable once deployed. This prevents accidental changes and reduces API Server load in large clusters.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v2
immutable: true # cannot be modified after creation — must create new version
data:
LOG_LEVEL: "info"
# Attempting to edit an immutable ConfigMap fails immediately
$ kubectl edit configmap app-config-v2
# error: configmaps "app-config-v2" is forbidden: field is immutable
💡 Versioning pattern: Name your immutable ConfigMaps with a version suffix —
app-config-v1,app-config-v2. When config changes, create a new ConfigMap and update the Deployment to reference it. The rolling update restarts pods with the new config. Clean, auditable, rollback-friendly.
Debugging Config Issues
# Verify a ConfigMap was created correctly
$ kubectl get configmap app-config -o yaml
# Check what env vars are actually set inside a running pod
$ kubectl exec -it <pod-name> -- env | sort# Check a specific env var
$ kubectl exec -it <pod-name> -- printenv LOG_LEVEL# Verify a mounted config file
$ kubectl exec -it <pod-name> -- cat /etc/config/app.properties# Check if a Secret key exists (without revealing the value)
$ kubectl get secret db-credentials -o jsonpath='{.data}' | python3 -m json.tool
Hands-On Lab
# 1. Create a ConfigMap from literals
$ kubectl create configmap my-config \
--from-literal=APP_ENV=staging \
--from-literal=LOG_LEVEL=debug
# 2. Create a Secret
$ kubectl create secret generic my-secret \
--from-literal=DB_PASSWORD=supersecret# 3. Deploy a pod that uses both
$ kubectl apply -f secret-pod.yaml# 4. Verify the env vars are set inside the pod
$ kubectl exec -it app-with-secrets -- printenv | grep -E 'APP_ENV|LOG_LEVEL|DB_'# 5. Update the ConfigMap and watch the file mount update (if using volume mount)
$ kubectl edit configmap my-config # change LOG_LEVEL to info
$ sleep 60
$ kubectl exec -it app-with-secrets -- cat /etc/config/LOG_LEVEL# 6. Try to read a Secret value via kubectl
$ kubectl get secret my-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 --decode# 7. Mark a ConfigMap as immutable and try to edit it
$ kubectl patch configmap my-config -p '{"immutable": true}'
$ kubectl edit configmap my-config # should fail# 8. Clean up
$ kubectl delete configmap my-config
$ kubectl delete secret my-secret
$ kubectl delete pod app-with-secrets
What’s Next
In Episode 06, we tackle Storage — PersistentVolumes, PersistentVolumeClaims, and StatefulSets. Containers are ephemeral by design, which means any data written inside a container is lost when it restarts. If your application needs to persist data — databases, file uploads, logs — you need Kubernetes storage primitives. We’ll cover how to request storage, how the storage lifecycle works, and how StatefulSets handle the special needs of stateful workloads.
Episode Topic
EP 01 Introduction to Kubernetes
EP 02 Kubernetes Architecture & Components
EP 03 Pods, Deployments & ReplicaSets
EP 04 Services & Networking
EP 05 ← You are here ConfigMaps, Secrets & Environment Config
EP 06 Storage: PersistentVolumes & StatefulSets
EP 07 Ingress Controllers & Load Balancing
EP 08 RBAC & Security Fundamentals
EP 09 Monitoring with Prometheus & Grafana
EP 10 Production Hardening & Helm Charts
Before the Next Article
Make sure you understand the difference between these two questions:
- When should I use a ConfigMap vs a Secret?
- When should I use an environment variable vs a volume mount?
If you can answer both — with specific examples — you’ve fully absorbed this episode.
Bonus challenge: look at an application you’re currently running. Find every place config or credentials are hardcoded, and sketch out how you’d externalise them with ConfigMaps and Secrets.
Configuration management is boring until the day you ship a container with a hardcoded production credential into a public image registry. That day is very not boring. You now know how to make sure it never happens. See you in Episode 06.