Skip to content

Module 9 — ConfigMaps & Secrets

Overview

ConfigMaps and Secrets decouple configuration from container images. ConfigMaps hold non-sensitive data, Secrets hold sensitive data (base64-encoded, not encrypted by default). The CKA exam tests creation, consumption (env vars vs volumes), and immutability. This module covers all three topics in depth.


1. ConfigMaps

1.1 What Is a ConfigMap?

A ConfigMap stores key-value pairs of non-sensitive configuration data. Pods consume them as environment variables or mounted files.

1
2
3
4
5
6
7
┌──────────────────┐          ┌──────────────┐
│   ConfigMap      │          │     Pod      │
│                  │          │              │
│  DB_HOST=mysql   │──env──▶  │  $DB_HOST    │
│  DB_PORT=3306    │──env──▶  │  $DB_PORT    │
│  app.conf=...    │──vol──▶  │  /etc/config/│
└──────────────────┘          └──────────────┘

1.2 Creating ConfigMaps

Imperative — From Literals

1
2
3
4
kubectl create configmap app-config \
  --from-literal=DB_HOST=mysql \
  --from-literal=DB_PORT=3306 \
  --from-literal=LOG_LEVEL=info

Imperative — From a File

# Create a config file
cat <<EOF > app.properties
database.host=mysql
database.port=3306
database.name=myapp
EOF

# Create ConfigMap from file (key = filename, value = file contents)
kubectl create configmap app-config --from-file=app.properties

# Custom key name
kubectl create configmap app-config --from-file=config.txt=app.properties

# From an entire directory (each file becomes a key)
kubectl create configmap app-config --from-file=./config-dir/

Imperative — From env File

1
2
3
4
5
6
7
# env file format (KEY=VALUE per line)
cat <<EOF > app.env
DB_HOST=mysql
DB_PORT=3306
EOF

kubectl create configmap app-config --from-env-file=app.env

Declarative YAML

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: default
data:
  DB_HOST: mysql
  DB_PORT: "3306"           # values are always strings
  LOG_LEVEL: info
  app.conf: |               # multi-line value (file content)
    server.port=8080
    server.host=0.0.0.0
    logging.level=INFO

Generate YAML

1
2
3
4
kubectl create configmap app-config \
  --from-literal=DB_HOST=mysql \
  --from-literal=DB_PORT=3306 \
  --dry-run=client -o yaml > configmap.yaml

1.3 Inspecting ConfigMaps

1
2
3
4
5
6
7
# List
kubectl get configmaps
kubectl get cm                    # shortname

# View contents
kubectl describe cm app-config
kubectl get cm app-config -o yaml

2. Secrets

2.1 What Is a Secret?

A Secret stores sensitive data — passwords, tokens, certificates, SSH keys. Values are base64-encoded (NOT encrypted) by default.

2.2 Secret Types

Type Usage
Opaque (default) Arbitrary key-value data
kubernetes.io/tls TLS certificate + private key
kubernetes.io/dockerconfigjson Docker registry credentials
kubernetes.io/service-account-token ServiceAccount token (legacy)
kubernetes.io/basic-auth Username + password
kubernetes.io/ssh-auth SSH private key

2.3 Creating Secrets

Imperative — Generic (Opaque)

1
2
3
kubectl create secret generic db-secret \
  --from-literal=DB_USER=admin \
  --from-literal=DB_PASSWORD=s3cret123

Imperative — From Files

1
2
3
4
5
6
7
8
9
# Create secret from certificate files
kubectl create secret generic tls-data \
  --from-file=cert.pem=/path/to/cert.pem \
  --from-file=key.pem=/path/to/key.pem

# TLS type (validates cert + key)
kubectl create secret tls my-tls \
  --cert=/path/to/tls.crt \
  --key=/path/to/tls.key

Imperative — Docker Registry

1
2
3
4
5
kubectl create secret docker-registry regcred \
  --docker-server=https://index.docker.io/v1/ \
  --docker-username=<user> \
  --docker-password=<password> \
  --docker-email=<email>

Declarative YAML

Values must be base64-encoded:

1
2
3
# Encode values
echo -n 'admin' | base64        # YWRtaW4=
echo -n 's3cret123' | base64    # czNjcmV0MTIz
1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  DB_USER: YWRtaW4=              # base64 of "admin"
  DB_PASSWORD: czNjcmV0MTIz      # base64 of "s3cret123"

Or use stringData to avoid manual encoding:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
stringData:                       # plain text — Kubernetes encodes it
  DB_USER: admin
  DB_PASSWORD: s3cret123

CKA Tip: Use stringData in YAML to avoid base64 encoding mistakes. Use --from-literal imperatively for speed.

2.4 Inspecting Secrets

# List
kubectl get secrets

# View (values are hidden)
kubectl describe secret db-secret

# View base64-encoded values
kubectl get secret db-secret -o yaml

# Decode a specific value
kubectl get secret db-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d

2.5 Security Considerations

Aspect Reality
base64 encoding NOT encryption — anyone with API access can decode
etcd storage Secrets stored in plain text in etcd by default
Encryption at rest Must be explicitly configured (EncryptionConfiguration)
RBAC Restrict get/list on Secrets to authorized users only
Namespace isolation Secrets are namespaced — pods can only access Secrets in their namespace

CKA Tip: The exam won't ask you to configure encryption at rest, but know that base64 ≠ encryption.


3. Consuming ConfigMaps and Secrets — Environment Variables

3.1 Single Key as Environment Variable

apiVersion: v1
kind: Pod
metadata:
  name: env-pod
spec:
  containers:
  - name: app
    image: nginx
    env:
    - name: DATABASE_HOST              # env var name in the container
      valueFrom:
        configMapKeyRef:
          name: app-config             # ConfigMap name
          key: DB_HOST                 # key in the ConfigMap
    - name: DATABASE_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-secret              # Secret name
          key: DB_PASSWORD             # key in the Secret

3.2 All Keys as Environment Variables

apiVersion: v1
kind: Pod
metadata:
  name: envfrom-pod
spec:
  containers:
  - name: app
    image: nginx
    envFrom:
    - configMapRef:
        name: app-config               # all keys become env vars
    - secretRef:
        name: db-secret                # all keys become env vars

With optional prefix:

1
2
3
4
    envFrom:
    - configMapRef:
        name: app-config
      prefix: APP_                     # DB_HOST → APP_DB_HOST

3.3 Verify Environment Variables

1
2
3
4
5
kubectl exec env-pod -- env | grep DATABASE
# DATABASE_HOST=mysql
# DATABASE_PASSWORD=s3cret123

kubectl exec envfrom-pod -- env | sort

4. Consuming ConfigMaps and Secrets — Volume Mounts

4.1 Mount Entire ConfigMap as a Directory

Each key becomes a file in the mounted directory:

apiVersion: v1
kind: Pod
metadata:
  name: vol-pod
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: config-volume
      mountPath: /etc/config           # directory where files appear
  volumes:
  - name: config-volume
    configMap:
      name: app-config

Result inside the container:

kubectl exec vol-pod -- ls /etc/config
# DB_HOST
# DB_PORT
# LOG_LEVEL
# app.conf

kubectl exec vol-pod -- cat /etc/config/DB_HOST
# mysql

kubectl exec vol-pod -- cat /etc/config/app.conf
# server.port=8080
# server.host=0.0.0.0
# logging.level=INFO

4.2 Mount Entire Secret as a Directory

spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: secret-volume
      mountPath: /etc/secrets
      readOnly: true                   # best practice for secrets
  volumes:
  - name: secret-volume
    secret:
      secretName: db-secret

Values are automatically decoded from base64 when mounted as files:

kubectl exec vol-pod -- cat /etc/secrets/DB_PASSWORD
# s3cret123    (plain text, not base64)

4.3 Mount Specific Keys Only

1
2
3
4
5
6
7
volumes:
- name: config-volume
  configMap:
    name: app-config
    items:
    - key: app.conf                    # only this key
      path: application.conf           # mounted as this filename
kubectl exec vol-pod -- ls /etc/config
# application.conf                     (only this file)

4.4 Mount a Single Key as a File (Without Overwriting the Directory)

Using subPath prevents the volume mount from replacing the entire directory:

spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: config-volume
      mountPath: /etc/nginx/nginx.conf   # single file path
      subPath: nginx.conf                # key from ConfigMap
  volumes:
  - name: config-volume
    configMap:
      name: nginx-config

Important: subPath mounts do NOT receive automatic updates when the ConfigMap changes.

4.5 File Permissions

1
2
3
4
5
volumes:
- name: secret-volume
  secret:
    secretName: db-secret
    defaultMode: 0400                  # read-only for owner

5. Environment Variables vs Volume Mounts

Aspect Environment Variables Volume Mounts
Format Key-value pairs as env vars Keys as files in a directory
Auto-update ❌ No — requires pod restart ✅ Yes — kubelet syncs periodically (~60s)
subPath update N/A ❌ No — subPath mounts don't auto-update
Binary data ❌ Not suitable ✅ Supports binary (certs, keystores)
Visibility Visible in kubectl describe pod Not visible in describe
Security Env vars can leak in logs/crash dumps Files are slightly more secure
Use case Simple key-value config Config files, certificates, multi-line data

5.1 When to Use Which

Scenario Recommendation
Database connection string Environment variable
API key or password Volume mount (more secure) or env var
Full config file (nginx.conf, app.properties) Volume mount
TLS certificates Volume mount
Feature flags (simple on/off) Environment variable
Config that must auto-update without restart Volume mount (not subPath)

5.2 Auto-Update Behavior

When a ConfigMap or Secret is updated:

1
2
3
4
# Update a ConfigMap
kubectl edit cm app-config
# OR
kubectl create cm app-config --from-literal=DB_HOST=new-mysql --dry-run=client -o yaml | kubectl apply -f -
Mount type Auto-updates? Delay
Volume mount (full) ✅ Yes ~60 seconds (kubelet sync period)
Volume mount with subPath ❌ No Never — requires pod restart
Environment variable ❌ No Never — requires pod restart

6. Immutable ConfigMaps and Secrets

6.1 Why Immutable?

Benefit Detail
Performance Kubelet stops watching for changes — reduces API server load
Safety Prevents accidental changes to critical configuration
Scale Significant in large clusters with thousands of ConfigMaps/Secrets

6.2 Creating Immutable ConfigMaps

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-v1
data:
  DB_HOST: mysql
  DB_PORT: "3306"
immutable: true                # ← cannot be changed after creation

6.3 Creating Immutable Secrets

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Secret
metadata:
  name: db-secret-v1
type: Opaque
immutable: true                # ← cannot be changed after creation
stringData:
  DB_USER: admin
  DB_PASSWORD: s3cret123

6.4 What Happens When You Try to Modify

1
2
3
4
5
kubectl edit cm app-config-v1
# error: configmaps "app-config-v1" is immutable

kubectl patch cm app-config-v1 -p '{"data":{"DB_HOST":"new-mysql"}}'
# error: the object "app-config-v1" is invalid: data: Forbidden: field is immutable when `immutable` is set

6.5 Updating Immutable ConfigMaps/Secrets

You can't modify them — you must:

  1. Create a new ConfigMap/Secret with a new name (e.g., app-config-v2)
  2. Update the pod/deployment to reference the new name
  3. Delete the old ConfigMap/Secret
# 1. Create new version
kubectl create configmap app-config-v2 \
  --from-literal=DB_HOST=new-mysql \
  --from-literal=DB_PORT=3306

# Make it immutable
kubectl patch cm app-config-v2 -p '{"immutable": true}'

# 2. Update the deployment to use v2
kubectl set env deployment/myapp --from=configmap/app-config-v2

# 3. Delete old version
kubectl delete cm app-config-v1

6.6 Making an Existing ConfigMap Immutable

1
2
3
4
5
6
# You CAN set immutable from false/unset to true
kubectl patch cm app-config -p '{"immutable": true}'

# But you CANNOT set it back to false
kubectl patch cm app-config -p '{"immutable": false}'
# error: field is immutable

CKA Tip: immutable: true is a one-way operation. Once set, you cannot unset it or modify the data. You must delete and recreate.


7. Projected Volumes — Combining Multiple Sources

You can mount ConfigMaps, Secrets, and other sources into a single directory:

spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: all-config
      mountPath: /etc/config
  volumes:
  - name: all-config
    projected:
      sources:
      - configMap:
          name: app-config
          items:
          - key: app.conf
            path: app.conf
      - secret:
          name: db-secret
          items:
          - key: DB_PASSWORD
            path: db-password
      - downwardAPI:
          items:
          - path: pod-name
            fieldRef:
              fieldPath: metadata.name

Result:

1
2
3
4
kubectl exec pod -- ls /etc/config
# app.conf
# db-password
# pod-name

8. Practice Exercises

Exercise 1 — Create and Consume a ConfigMap

# 1. Create a ConfigMap
kubectl create configmap web-config \
  --from-literal=APP_COLOR=blue \
  --from-literal=APP_MODE=production

# 2. Create a pod using env vars
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: web-pod
spec:
  containers:
  - name: web
    image: nginx
    envFrom:
    - configMapRef:
        name: web-config
EOF

# 3. Verify
kubectl exec web-pod -- env | grep APP
# APP_COLOR=blue
# APP_MODE=production

# 4. Clean up
kubectl delete pod web-pod
kubectl delete cm web-config

Exercise 2 — Mount a ConfigMap as a File

# 1. Create a ConfigMap with a config file
kubectl create configmap nginx-config --from-literal=nginx.conf="server { listen 8080; }"

# 2. Mount it as a volume
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: nginx-custom
spec:
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
    - name: config
      mountPath: /etc/nginx/conf.d
  volumes:
  - name: config
    configMap:
      name: nginx-config
EOF

# 3. Verify the file exists
kubectl exec nginx-custom -- cat /etc/nginx/conf.d/nginx.conf

# 4. Clean up
kubectl delete pod nginx-custom
kubectl delete cm nginx-config

Exercise 3 — Secrets as Environment Variables and Volumes

# 1. Create a Secret
kubectl create secret generic app-secret \
  --from-literal=API_KEY=abc123 \
  --from-literal=DB_PASS=supersecret

# 2. Create a pod using both env and volume
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: secret-pod
spec:
  containers:
  - name: app
    image: busybox
    command: ["sh", "-c", "echo env=\$API_KEY; cat /etc/secrets/DB_PASS; sleep 3600"]
    env:
    - name: API_KEY
      valueFrom:
        secretKeyRef:
          name: app-secret
          key: API_KEY
    volumeMounts:
    - name: secret-vol
      mountPath: /etc/secrets
      readOnly: true
  volumes:
  - name: secret-vol
    secret:
      secretName: app-secret
EOF

# 3. Check logs
kubectl logs secret-pod
# env=abc123
# supersecret

# 4. Verify volume mount (decoded from base64)
kubectl exec secret-pod -- cat /etc/secrets/DB_PASS
# supersecret

# 5. Clean up
kubectl delete pod secret-pod
kubectl delete secret app-secret

Exercise 4 — Auto-Update Test

# 1. Create a ConfigMap and mount it
kubectl create configmap update-test --from-literal=message=hello

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: update-pod
spec:
  containers:
  - name: app
    image: busybox
    command: ["sh", "-c", "while true; do cat /etc/config/message; sleep 5; done"]
    volumeMounts:
    - name: config
      mountPath: /etc/config
  volumes:
  - name: config
    configMap:
      name: update-test
EOF

# 2. Check current value
kubectl exec update-pod -- cat /etc/config/message
# hello

# 3. Update the ConfigMap
kubectl patch cm update-test -p '{"data":{"message":"world"}}'

# 4. Wait ~60 seconds and check again
sleep 70
kubectl exec update-pod -- cat /etc/config/message
# world    ← auto-updated!

# 5. Clean up
kubectl delete pod update-pod
kubectl delete cm update-test

Exercise 5 — Immutable ConfigMap

# 1. Create an immutable ConfigMap
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: locked-config
data:
  KEY: value1
immutable: true
EOF

# 2. Try to modify it
kubectl patch cm locked-config -p '{"data":{"KEY":"value2"}}'
# Error — immutable

# 3. Verify you must delete and recreate
kubectl delete cm locked-config
kubectl create configmap locked-config --from-literal=KEY=value2

# 4. Clean up
kubectl delete cm locked-config

Exercise 6 — Decode a Secret

# 1. Create a Secret
kubectl create secret generic exam-secret \
  --from-literal=username=admin \
  --from-literal=password=exam-pass-123

# 2. View the base64-encoded values
kubectl get secret exam-secret -o yaml

# 3. Decode each value
kubectl get secret exam-secret -o jsonpath='{.data.username}' | base64 -d
# admin

kubectl get secret exam-secret -o jsonpath='{.data.password}' | base64 -d
# exam-pass-123

# 4. Clean up
kubectl delete secret exam-secret

9. Key Takeaways for the CKA Exam

Point Detail
Imperative creation is fastest kubectl create configmap --from-literal / kubectl create secret generic --from-literal
stringData avoids base64 mistakes Use it in Secret YAML instead of manually encoding data
envFrom for all keys at once configMapRef / secretRef — injects every key as an env var
Volume mounts auto-update ~60s delay, except subPath mounts which never update
Env vars never auto-update Requires pod restart to pick up changes
Secrets are base64, not encrypted Know the difference — RBAC is the primary access control
readOnly: true for secret volumes Best practice — prevents container from modifying mounted secrets
Immutable is one-way Once immutable: true is set, you cannot modify or unset it
Decode secrets with base64 -d kubectl get secret <name> -o jsonpath='{.data.<key>}' \| base64 -d
items for selective mounting Mount only specific keys with custom filenames
subPath for single file Doesn't overwrite the directory, but loses auto-update

Previous: 08-scheduling.md — Scheduling

Next: 10-services.md — Services