ConfigMaps and Secrets
In Lesson 4, you built a Deployment that ran a specific container image with a specific environment. What happens when your API key expires and you need to update it? Or when you move from development to production and need different database URLs? Rebuilding your entire container image just to change a configuration value is wasteful and error-prone.
ConfigMaps and Secrets solve this by decoupling configuration from your container image. You store configuration in Kubernetes objects, inject them into your Pods, and change configuration without rebuilding anything. ConfigMaps handle non-sensitive data (URLs, feature flags). Secrets handle sensitive data (API keys, database credentials). Both use the same injection patterns—the difference is in how they're stored and accessed.
The Problem: Configuration Baked Into Images
Imagine your AI agent needs an API key to call an external service. Your Dockerfile hardcodes it:
FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
# Hardcoding config—terrible practice
ENV API_KEY="sk-prod-12345"
CMD ["python", "agent.py"]
Problems with this approach:
- Same image for all environments: Production, staging, and development all use the same image with the same API key. Security risk.
- Rebuilding for changes: Update an API key? Rebuild the entire image.
- Secrets in registries: Your API key is stored in the container image registry—visible to anyone with access.
- No separation of concerns: The developer who builds the image shouldn't know production credentials.
Kubernetes ConfigMaps and Secrets invert this model: the image contains no configuration. Kubernetes injects configuration at runtime.
ConfigMaps: Non-Sensitive Configuration
A ConfigMap is a Kubernetes object that stores key-value pairs or file content. It's not encrypted—use it for non-sensitive configuration like URLs, feature flags, and log levels.
Creating a ConfigMap with kubectl
The fastest way to create a ConfigMap is with the kubectl command:
kubectl create configmap app-config \
--from-literal=LOG_LEVEL=DEBUG \
--from-literal=DATABASE_URL=postgresql://localhost:5432/mydb \
--from-literal=FEATURE_FLAGS={"analytics":true,"beta":false}
Output:
configmap/app-config created
This creates a ConfigMap named app-config with three key-value pairs. Let's examine it:
kubectl get configmap app-config -o yaml
Output:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
spec: {}
data:
LOG_LEVEL: "DEBUG"
DATABASE_URL: "postgresql://localhost:5432/mydb"
FEATURE_FLAGS: '{"analytics":true,"beta":false}'
Notice: ConfigMaps are plain text. No encryption. They're for non-sensitive data only.
Creating a ConfigMap with YAML Manifest
For reproducibility, define ConfigMaps in YAML:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
data:
LOG_LEVEL: "DEBUG"
DATABASE_URL: "postgresql://localhost:5432/mydb"
FEATURE_FLAGS: '{"analytics":true,"beta":false}'
Apply it:
kubectl apply -f configmap.yaml
Output:
configmap/app-config created
The YAML approach is preferred in production because it's version-controlled and reproducible.
Secrets: Sensitive Data
Secrets work like ConfigMaps but are designed for sensitive data: API keys, database passwords, tokens. The key difference: Kubernetes stores Secrets separately from regular data, and some storage backends can encrypt them (though by default, Kubernetes base64-encodes Secrets, which is NOT encryption—see the security note below).
Creating a Secret with kubectl
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=super-secret-password
Output:
secret/db-credentials created
Let's examine it:
kubectl get secret db-credentials -o yaml
Output:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: default
type: Opaque
data:
password: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
username: YWRtaW4=
Notice the data field contains base64-encoded values. Let's decode one:
echo "c3VwZXItc2VjcmV0LXBhc3N3b3Jk" | base64 --decode
Output:
super-secret-password
Creating a Secret with YAML Manifest
For production, define Secrets in YAML with base64-encoded values:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: default
type: Opaque
data:
username: YWRtaW4= # base64 of "admin"
password: c3VwZXItc2VjcmV0LXBhc3N3b3Jk # base64 of "super-secret-password"
Or let Kubernetes do the encoding:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: default
type: Opaque
stringData:
username: admin
password: super-secret-password
When using stringData, Kubernetes automatically base64-encodes the values when storing them. Apply it:
kubectl apply -f secret.yaml
Output:
secret/db-credentials created
Injecting Configuration as Environment Variables
Once you have a ConfigMap or Secret, inject it into a Pod as environment variables.
Environment Variable Injection from ConfigMap
Create a Deployment that injects the app-config ConfigMap:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 2
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myregistry/agent:v1.0
envFrom:
- configMapRef:
name: app-config
# Individual environment variables from Secrets
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
Let's break this down:
envFrom.configMapRef: Injects ALL keys from theapp-configConfigMap as environment variables. The Pod seesLOG_LEVEL,DATABASE_URL, andFEATURE_FLAGS.env.valueFrom.secretKeyRef: Injects a single Secret key (passwordfromdb-credentials) as an environment variable namedDB_PASSWORD.
Apply this Deployment:
kubectl apply -f deployment.yaml
Output:
deployment.apps/api-server created
Verify the Pods received the configuration:
kubectl get pods -l app=api-server
Output:
NAME READY STATUS RESTARTS AGE
api-server-5d8c7f9b4-7xvzq 1/1 Running 0 15s
api-server-5d8c7f9b4-kq9mfl 1/1 Running 0 15s
Check the environment inside a Pod:
kubectl exec -it api-server-5d8c7f9b4-7xvzq -- env | grep LOG_LEVEL
Output:
LOG_LEVEL=DEBUG
The Pod sees the configuration value. Verify the database password is also present:
kubectl exec -it api-server-5d8c7f9b4-7xvzq -- env | grep DB_PASSWORD
Output:
DB_PASSWORD=super-secret-password
Mounting Configuration as Files
Sometimes your application expects configuration in files, not environment variables. Use volume mounts to inject ConfigMaps and Secrets as files.
Mounting a ConfigMap as a Volume
Create a ConfigMap with file-like data:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-files
data:
config.json: |
{
"database": "postgresql://localhost:5432/mydb",
"log_level": "DEBUG",
"features": {
"analytics": true,
"beta": false
}
}
settings.env: |
TIMEOUT=30
RETRIES=3
Now mount this ConfigMap as files in a Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 1
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myregistry/agent:v1.0
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: app-config-files
Apply this Deployment:
kubectl apply -f deployment-with-volumes.yaml
Output:
deployment.apps/api-server created
Verify the files exist inside the Pod:
kubectl exec -it api-server-abc123-xyz -- ls -la /etc/config/
Output:
total 8
drwxr-xr-x 3 root root 120 Dec 22 10:30 .
drwxr-xr-x 1 root root 4096 Dec 22 10:30 ..
-rw-r--r-- 1 root root 156 Dec 22 10:30 config.json
-rw-r--r-- 1 root root 31 Dec 22 10:30 settings.env
Read the configuration file:
kubectl exec -it api-server-abc123-xyz -- cat /etc/config/config.json
Output:
{
"database": "postgresql://localhost:5432/mydb",
"log_level": "DEBUG",
"features": {
"analytics": true,
"beta": false
}
}
Mounting a Secret as a Volume
Mounting Secrets as files works identically to ConfigMaps:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 1
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myregistry/agent:v1.0
volumeMounts:
- name: secrets-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secrets-volume
secret:
secretName: db-credentials
Apply:
kubectl apply -f deployment-with-secrets.yaml
Output:
deployment.apps/api-server created
Verify the secret files exist:
kubectl exec -it api-server-def456-uvw -- cat /etc/secrets/password
Output:
super-secret-password
Important: Base64 Encoding Is NOT Encryption
This is critical for security understanding: Kubernetes Secrets are base64-encoded, but base64 is encoding, not encryption. Anyone with access to the Secret object can decode it immediately.
kubectl get secret db-credentials -o yaml
data:
password: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
This password is trivially decodable:
echo "c3VwZXItc2VjcmV0LXBhc3N3b3Jk" | base64 --decode
Output:
super-secret-password
What this means for your deployments:
- Don't treat base64 as security: Secrets are NOT secure by default in Kubernetes.
- Use RBAC to control access: Restrict who can
kubectl get secret. - Enable encryption at rest: Many Kubernetes distributions (AWS EKS, Google GKE, Azure AKS) support encrypting Secrets in the etcd database. Enable this in production.
- Use external secret systems: For highly sensitive credentials, consider external secret managers (HashiCorp Vault, AWS Secrets Manager) that integrate with Kubernetes.
For this course, understand that Secrets prevent credentials from being accidentally logged or displayed, but they're not cryptographically secure against someone with cluster access.
Practice Exercises
Exercise 1: Create a ConfigMap and Inject It
Create a ConfigMap named agent-config with these keys:
API_ENDPOINT=https://api.example.com
TIMEOUT_SECONDS=30
DEBUG_MODE=false
Then create a Pod that injects this ConfigMap as environment variables. Verify the Pod receives the configuration.
Solution:
kubectl create configmap agent-config \
--from-literal=API_ENDPOINT=https://api.example.com \
--from-literal=TIMEOUT_SECONDS=30 \
--from-literal=DEBUG_MODE=false
Create a Pod manifest:
apiVersion: v1
kind: Pod
metadata:
name: agent-pod
spec:
containers:
- name: agent
image: alpine
command: ["sh", "-c", "env | grep API_ENDPOINT && sleep 3600"]
envFrom:
- configMapRef:
name: agent-config
restartPolicy: Never
Apply and verify:
kubectl apply -f agent-pod.yaml
kubectl exec -it agent-pod -- env | grep API_ENDPOINT
Output:
API_ENDPOINT=https://api.example.com
Exercise 2: Create a Secret and Mount as Files
Create a Secret named db-secrets with:
username=postgres
password=prod-db-password
host=db.example.com
Mount this Secret into a Pod at /etc/db-secrets/ and verify you can read the files.
Solution:
kubectl create secret generic db-secrets \
--from-literal=username=postgres \
--from-literal=password=prod-db-password \
--from-literal=host=db.example.com
Create a Pod manifest:
apiVersion: v1
kind: Pod
metadata:
name: db-client
spec:
containers:
- name: client
image: alpine
command: ["sh", "-c", "cat /etc/db-secrets/password && sleep 3600"]
volumeMounts:
- name: db-secrets
mountPath: /etc/db-secrets
readOnly: true
volumes:
- name: db-secrets
secret:
secretName: db-secrets
restartPolicy: Never
Apply and verify:
kubectl apply -f db-client.yaml
kubectl logs db-client
Output:
prod-db-password
Exercise 3: Update a ConfigMap and Redeploy
Create a Deployment that uses a ConfigMap. Update the ConfigMap and observe whether the Pods automatically receive the new configuration.
Solution:
Create initial ConfigMap:
kubectl create configmap app-settings --from-literal=ENVIRONMENT=development
Create Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: env-printer
spec:
replicas: 1
selector:
matchLabels:
app: env-printer
template:
metadata:
labels:
app: env-printer
spec:
containers:
- name: printer
image: alpine
command: ["sh", "-c", "echo $ENVIRONMENT && sleep 3600"]
env:
- name: ENVIRONMENT
valueFrom:
configMapKeyRef:
name: app-settings
key: ENVIRONMENT
restartPolicy: Never
Apply:
kubectl apply -f deployment.yaml
kubectl logs -l app=env-printer
Output:
development
Now update the ConfigMap:
kubectl patch configmap app-settings -p '{"data":{"ENVIRONMENT":"production"}}'
Important discovery: The existing Pod still sees the old value. Kubernetes doesn't automatically reload ConfigMap changes into running Pods. To apply the new configuration, you must restart the Deployment:
kubectl rollout restart deployment/env-printer
kubectl logs -l app=env-printer
Output:
production
This teaches an important lesson: ConfigMaps aren't "live"—they're read when the Pod starts. To update configuration, you redeploy the Pods.
Try With AI
In this section, you'll collaborate with Claude to generate ConfigMaps and Secrets for your Part 6 agent.
Part 1: Planning Your Configuration
Your Part 6 FastAPI agent needs external configuration. You'll ask Claude to help you design which values belong in ConfigMaps and which in Secrets.
Ask Claude:
I'm deploying a FastAPI agent to Kubernetes. The agent needs:
- OpenAI API key (sensitive)
- PostgreSQL database URL with credentials (sensitive)
- Agent name for logging (non-sensitive)
- Request timeout in seconds (non-sensitive)
- Feature flags in JSON format (non-sensitive)
- TLS certificate path (non-sensitive, but file-based)
Which of these should go in ConfigMaps vs Secrets? Why? And provide the Kubernetes YAML manifests for both.
Part 2: Critical Evaluation
Review Claude's response. Ask yourself:
- Did Claude correctly classify sensitive vs non-sensitive data?
- Did Claude create separate ConfigMap and Secret objects?
- Does the YAML follow Kubernetes conventions (metadata, kind, apiVersion)?
- Are the data keys clearly named?
Part 3: Refinement
Based on your evaluation, tell Claude:
I like your classification. However, I also need:
- Agent version (for the logs)
- Maximum concurrent connections (for rate limiting)
Can you update both manifests to include these? Also, in the Deployment template,
show me how to inject the ConfigMap using envFrom and the Secret using individual
valueFrom references.
Part 4: Validation
Claude generates an updated Deployment. Review it:
- Does the ConfigMap injection use
envFrom.configMapRef? - Does the Secret injection use
env[].valueFrom.secretKeyRef? - Are all the configuration keys properly referenced?
Part 5: Implementation Check
Ask Claude:
Now show me the complete Deployment YAML that injects both the ConfigMap
and Secret, includes proper pod labels and replica count of 3, and includes
a liveness probe on the /health endpoint. Also add a security note about
why we're using Secrets here (not for cryptographic security, but for
access control and to prevent accidental logging).
This exercises your ability to prompt Claude for iterative refinement of configuration manifests—a skill you'll use frequently when deploying to Kubernetes.