Skip to main content

ConfigMaps and Secrets — Manage Configuration Without Rebuilding Images

· 6 min read
Goel Academy
DevOps & Cloud Learning Hub

Your app needs a database URL in dev, a different one in staging, and yet another in production. You could bake each URL into a separate Docker image, but then you would need three images for the same code. ConfigMaps and Secrets let you inject configuration at deploy time, so one image works everywhere. Here is how to use them properly.

ConfigMaps — Externalizing Configuration

A ConfigMap holds key-value pairs or entire configuration files. It decouples configuration from your container image so you can change settings without rebuilding.

Creating ConfigMaps

From literal values:

kubectl create configmap app-config \
--from-literal=DATABASE_HOST=postgres.default.svc \
--from-literal=DATABASE_PORT=5432 \
--from-literal=LOG_LEVEL=info \
--from-literal=CACHE_TTL=3600

From a file:

# Create a config file
cat > nginx.conf << 'EOF'
server {
listen 80;
server_name _;
location / {
proxy_pass http://api-service:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
EOF

# Create ConfigMap from the file
kubectl create configmap nginx-config --from-file=nginx.conf

# From an entire directory
kubectl create configmap app-configs --from-file=./config-dir/

From YAML:

apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
# Simple key-value pairs
DATABASE_HOST: "postgres.default.svc"
DATABASE_PORT: "5432"
LOG_LEVEL: "info"

# Multi-line config file
app.properties: |
server.port=8080
spring.datasource.url=jdbc:postgresql://postgres:5432/mydb
spring.jpa.hibernate.ddl-auto=validate
management.endpoints.web.exposure.include=health,metrics

Using ConfigMaps as Environment Variables

apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapi:1.0
# Option 1: Individual keys
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DATABASE_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: DATABASE_PORT

# Option 2: All keys at once
envFrom:
- configMapRef:
name: app-config
# optional: true # Don't fail if ConfigMap is missing

Mounting ConfigMaps as Volumes

This is the preferred approach for config files. Each key becomes a file in the mounted directory.

apiVersion: v1
kind: Pod
metadata:
name: app-with-config
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/conf.d/
readOnly: true
- name: app-properties
mountPath: /app/config/app.properties
subPath: app.properties # Mount single file, not the whole dir
volumes:
- name: config-volume
configMap:
name: nginx-config
- name: app-properties
configMap:
name: app-config
items: # Select specific keys
- key: app.properties
path: app.properties

The advantage of volume mounts: when you update a ConfigMap, the mounted files update automatically (within 30-60 seconds). Environment variables do not update — they require a pod restart.

Secrets — Sensitive Data

Secrets work like ConfigMaps but are designed for sensitive data: passwords, API keys, TLS certificates.

Secret Types

TypeUse CaseAuto-Validation
OpaqueGeneral purpose (passwords, tokens)None
kubernetes.io/dockerconfigjsonDocker registry credentialsMust contain .dockerconfigjson key
kubernetes.io/tlsTLS certificatesMust contain tls.crt and tls.key
kubernetes.io/basic-authHTTP basic authenticationMust contain username and password
kubernetes.io/ssh-authSSH authenticationMust contain ssh-privatekey
kubernetes.io/service-account-tokenService account tokensAuto-generated

Creating Secrets

# Opaque secret from literals (kubectl base64-encodes automatically)
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='S3cur3P@ss!'

# TLS secret from cert files
kubectl create secret tls app-tls \
--cert=tls.crt \
--key=tls.key

# Docker registry secret
kubectl create secret docker-registry registry-cred \
--docker-server=ghcr.io \
--docker-username=myuser \
--docker-password=mytoken

From YAML (values must be base64-encoded):

apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # echo -n 'admin' | base64
password: UzNjdXIzUEBzcyE= # echo -n 'S3cur3P@ss!' | base64
# Base64 encode/decode
echo -n 'my-secret-value' | base64
# bXktc2VjcmV0LXZhbHVl

echo 'bXktc2VjcmV0LXZhbHVl' | base64 -d
# my-secret-value

Important: base64 is encoding, not encryption. Anyone with cluster access can decode your secrets. For real encryption, use external secret managers.

Using Secrets in Pods

apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapi:1.0
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumeMounts:
- name: tls-certs
mountPath: /etc/tls
readOnly: true

# Pull from private registry using secret
imagePullSecrets:
- name: registry-cred

volumes:
- name: tls-certs
secret:
secretName: app-tls
defaultMode: 0400 # Read-only for owner

Immutable ConfigMaps and Secrets

Starting with Kubernetes 1.21, you can make ConfigMaps and Secrets immutable. This prevents accidental changes and improves cluster performance (the API server does not need to watch them).

apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v2
data:
DATABASE_HOST: "postgres.prod.svc"
LOG_LEVEL: "warn"
immutable: true
apiVersion: v1
kind: Secret
metadata:
name: api-key-v3
type: Opaque
data:
key: c3VwZXItc2VjcmV0LWtleQ==
immutable: true

To update an immutable ConfigMap, you create a new one with a different name (version it!) and update your Deployment to reference it. This triggers a rolling update, which is actually safer than hot-reloading config.

External Secrets Operators

For production environments, store secrets in a dedicated vault and sync them into Kubernetes:

# Using External Secrets Operator with AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: db-credentials # Kubernetes Secret name
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: prod/database
property: username
- secretKey: password
remoteRef:
key: prod/database
property: password

This way, secrets are managed in AWS Secrets Manager (or HashiCorp Vault, Azure Key Vault, GCP Secret Manager) and automatically synced into your cluster.

Real-World Configuration Pattern

Here is a pattern that works well for most teams — use ConfigMaps for non-sensitive config and Secrets for credentials, with clear naming and versioning:

# Create environment-specific configs
kubectl create configmap api-config-prod \
--from-literal=ENV=production \
--from-literal=LOG_LEVEL=warn \
--from-literal=CACHE_TTL=7200 \
-n production

kubectl create configmap api-config-staging \
--from-literal=ENV=staging \
--from-literal=LOG_LEVEL=debug \
--from-literal=CACHE_TTL=60 \
-n staging

# Verify what was created
kubectl get configmap api-config-prod -n production -o yaml
kubectl get secret db-credentials -n production -o jsonpath='{.data.password}' | base64 -d

The golden rule: never put secrets in ConfigMaps, never put non-sensitive data in Secrets. Keep them separate for clarity and security auditing.


Next up: Kubernetes Storage — persistent volumes, storage classes, and CSI drivers for data that needs to survive pod restarts.