Explore the complete learning track from Linux fundamentals to advanced GitOps and Terraform. Packed with practical terminal sessions and real-world architectures.
Lesson 7 of 12•30 min
ConfigMaps & Secrets: Decoupling Configurations
A fundamental principle of the 12-Factor App methodology: configuration must be stored in the environment, not the code.
If your database URL is hardcoded into your Docker image, you need to build a different image for every environment (dev, staging, production). That means you're shipping different code to different environments — which defeats the purpose of containerization.
Kubernetes provides two resources for configuration injection:
Resource
For
Encryption in etcd
ConfigMap
Non-sensitive config: ports, feature flags, service URLs
❌ Plaintext
Secret
Sensitive data: passwords, API keys, TLS certificates
✅ Base64 (encrypted at rest with envelope encryption)
Method 1: As Environment Variables (Individual Keys)
yaml
spec:
containers:
- name: api
image: my-api:v2
env:
- name: NODE_ENV
valueFrom:
configMapKeyRef:
name: api-config # ConfigMap name
key: NODE_ENV # Key within the ConfigMap
- name: PORT
valueFrom:
configMapKeyRef:
name: api-config
key: PORT
Method 2: All Keys as Environment Variables (envFrom)
yaml
spec:
containers:
- name: api
image: my-api:v2
# All keys in api-config become environment variables
envFrom:
- configMapRef:
name: api-config
# My app now has: NODE_ENV=production, PORT=3000, LOG_LEVEL=info, etc.
Method 3: As Files Mounted in a Volume
yaml
spec:
containers:
- name: api
image: my-api:v2
volumeMounts:
- name: config-volume
mountPath: /app/config # Mount point inside container
readOnly: true
volumes:
- name: config-volume
configMap:
name: api-config
# Only mount specific keys as files (optional — default: all keys)
items:
- key: config.json
path: config.json # → /app/config/config.json
- key: nginx.conf
path: nginx.conf # → /app/config/nginx.conf
Volume-Mounted ConfigMaps Auto-Update
When a ConfigMap changes, volume-mounted files are automatically updated inside running containers (within 60-120 seconds). Your app can watch the file for changes and reload hot.
Environment variable ConfigMaps (envFrom) do NOT auto-update — the pod must be restarted to pick up changes.
Secrets
Secrets store sensitive data. Kubernetes encrypts them at rest in etcd using envelope encryption (when configured) and only exposes them to pods that explicitly reference them.
# secret.yaml (declarative — but NEVER commit passwords in plaintext!)
# Use this template, fill values with base64 or use Sealed Secrets / External Secrets
apiVersion: v1
kind: Secret
metadata:
name: app-secret
namespace: production
type: Opaque # Generic secret
data:
# Values MUST be base64 encoded
# echo -n "mypassword" | base64
POSTGRES_PASSWORD: bXlwYXNzd29yZA==
API_KEY: c2VjcmV0LWtleS12YWx1ZQ==
stringData:
# Alternatively, use stringData — K8s base64-encodes it for you
DATABASE_URL: "postgres://appuser:password@postgres:5432/myapp"
Never Commit Secrets to Git
secret.yaml with real values must NEVER be committed to a Git repository — even a private one. The values are base64-encoded (not encrypted) and trivially decoded with base64 -d.
For production, use one of these approaches:
Sealed Secrets (Bitnami) — encrypt secrets with a cluster-specific key; safe to commit
External Secrets Operator — sync secrets from AWS Secrets Manager, Vault, or GCP Secret Manager
kubectl create secret imperatively in CI/CD (never stored in Git)
GitHub Actions Secrets → passed as env vars to kubectl create secret
Using Secrets in Pods
Method 1: As Individual Environment Variables
yaml
spec:
containers:
- name: api
image: my-api:v2
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secret # Secret name
key: DATABASE_URL # Key in the Secret
- name: API_KEY
valueFrom:
secretKeyRef:
name: app-secret
key: API_KEY
optional: false # Pod will fail to start if key doesn't exist
# Deploy the Secret first (Deployment fails to start without it)
kubectl create secret generic app-secret \
--from-literal=DATABASE_URL="postgres://appuser:mypassword@postgres.production.svc.cluster.local:5432/myapp" \
--from-literal=JWT_SECRET="$(openssl rand -base64 48)" \
-n production
# Then deploy the app
kubectl apply -f api-with-config.yaml
# Verify env vars are available inside the pod
kubectl exec -n production deployment/api -- env | grep -E "NODE_ENV|PORT|LOG_LEVEL|DATABASE"
# NODE_ENV=production
# PORT=3000
# LOG_LEVEL=info
# DATABASE_URL=postgres://appuser:mypassword@postgres.production:5432/myapp
Inspecting ConfigMaps and Secrets
bash
# View a ConfigMap's data (plaintext)
kubectl get configmap api-config -n production -o yaml
# Decode a Secret value (base64)
kubectl get secret app-secret -n production -o jsonpath='{.data.DATABASE_URL}' | base64 -d
# postgres://appuser:mypassword@postgres:5432/myapp
# View all data in a Secret (decoded)
kubectl get secret app-secret -n production -o json | \
jq -r '.data | to_entries[] | "\(.key): \(.value | @base64d)"'
# List all secrets in a namespace
kubectl get secrets -n production
# NAME TYPE DATA AGE
# app-secret Opaque 2 5m
# ghcr-pull-secret kubernetes.io/dockerconfigjson 1 10m
# my-tls-secret kubernetes.io/tls 2 3d
# default-token-xxx kubernetes.io/service-account-token 3 30d
Production Best Practices
1. Use External Secrets Operator for Real Production
Syncing secrets from a secrets manager means developers never see real production passwords:
yaml
# ExternalSecret: pulls from AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secret
namespace: production
spec:
refreshInterval: 1h # Resync from AWS SM every hour
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: app-secret # Creates a regular K8s Secret
creationPolicy: Owner
data:
- secretKey: DATABASE_URL # Key in K8s Secret
remoteRef:
key: prod/myapp/db # AWS Secrets Manager secret name
property: url # JSON property within the secret
2. Rotate Secrets Without Downtime
bash
# Update the secret value (new password after rotation)
kubectl create secret generic app-secret \
--from-literal=DATABASE_URL="postgres://appuser:NEWPASSWORD@postgres:5432/myapp" \
-n production \
--dry-run=client -o yaml | kubectl apply -f -
# Trigger rolling restart to pick up the new env var value
kubectl rollout restart deployment/api -n production
3. Namespace Isolation
Secrets are namespace-scoped. A pod in namespace staging cannot access secrets in namespace production:
bash
# Create the same secret in each namespace independently
kubectl create secret generic app-secret --from-literal=... -n staging
kubectl create secret generic app-secret --from-literal=... -n production
Summary
ConfigMaps and Secrets decouple configuration from container images:
ConfigMaps: store non-sensitive key-value pairs and config files — injected via envFrom, individual env[].valueFrom.configMapKeyRef, or volume mounts
Secrets: store sensitive data — same injection methods; values are base64-encoded in YAML
Volume-mounted ConfigMaps auto-update in running pods; env var ConfigMaps require a pod restart
NEVER commit secrets to Git — use Sealed Secrets, External Secrets Operator, or create them imperatively in CI/CD
In production, use External Secrets Operator to sync from AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault
In the next lesson, you will expose your application to the internet with an Ingress Controller — routing HTTP/HTTPS traffic by hostname and URL path.