Certified Kubernetes Administrator (CKA) #12 ConfigMap and Secret in Depth
By #11 we had made a full loop through the workload objects. Now we turn to the two objects that inject configuration and secret values into those workloads — ConfigMap and Secret — and look at them closely from an operator’s point of view.
The app-developer-level introduction was covered in one cycle in K8s Basics #6. This post builds on that and focuses on the parts you actually touch in the exam and in operations: making them quickly from an empty terminal, the behavioral differences by injection method, the fact that base64 is not encryption, and the effect of immutable.
Telling the two objects apart #
ConfigMap and Secret share the same purpose: pulling configuration values out of the workload definition. Instead of hardcoding a port or a domain into the manifest, you put them in a separate object so you can run the same image with different settings across environments.
| Aspect | ConfigMap | Secret |
|---|---|---|
| Use | Non-secret configuration values | Passwords, tokens, keys, certificates |
| Stored form | Plaintext | base64-encoded |
| etcd storage | Plaintext | Plaintext by default (only encoded) |
| Type | Single | Opaque, kubernetes.io/tls, and many more |
The last two rows of the table are where operators most often get it wrong. The name “Secret” makes it easy to assume the data is encrypted, but by default a Secret is not encrypted. We’ll cover this separately later.
Creating a ConfigMap: three sources #
In the exam you frequently have to make a ConfigMap quickly from an empty terminal. There are three sources.
–from-literal: keys and values directly #
# List key=value pairs directly
k create configmap app-config \
--from-literal=APP_COLOR=blue \
--from-literal=APP_MODE=prodThis is the fastest when you have only two or three keys. Tacking on --dry-run=client -o yaml to check the YAML first is a safe habit.
–from-file: from a file #
# A whole file (key is the filename, value is the content)
k create configmap nginx-config --from-file=nginx.conf
# Specify the key name directly
k create configmap nginx-config --from-file=conf=nginx.conf
# An entire directory (each file inside becomes a key)
k create configmap all-config --from-file=./config-dir/Use this when you need to carry an entire config file as-is. If you don’t specify a key, the filename becomes the key.
–from-env-file: from an env-format file #
# Each KEY=VALUE line becomes a separate key
k create configmap app-config --from-env-file=app.propertiesWith --from-file one file becomes one key, but with --from-env-file each line in the file becomes a separate key. This difference is what decides the result when you inject the whole thing with envFrom.
Defining it directly in YAML #
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_COLOR: blue
APP_MODE: prod
nginx.conf: |
server {
listen 80;
}A | block lets you carry a multi-line config file as-is.
Creating a Secret: be conscious of the type #
A Secret is made almost the same way as a ConfigMap, but a type is added. Knowing these three is enough for the exam and for operations.
generic (Opaque) #
The most common general-purpose type. It uses the same source options as ConfigMap.
k create secret generic db-secret \
--from-literal=DB_USER=admin \
--from-literal=DB_PASS=s3cr3tdocker-registry #
The credentials used to pull images from a private registry. A Pod’s imagePullSecrets references this type.
k create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=admin \
--docker-password=s3cr3t \
--docker-email=ops@example.comtls #
Holds a TLS certificate and key. Referenced by Ingress TLS termination (#19).
k create secret tls web-tls \
--cert=tls.crt \
--key=tls.keyWhen defining in YAML: data and stringData #
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
data:
DB_USER: YWRtaW4= # base64-encoded value
stringData:
DB_PASS: s3cr3t # write plaintext and the apiserver encodes itdata requires a base64-encoded value, while stringData takes plaintext that the apiserver encodes on storage. When you encode by hand, use the following.
echo -n 's3cr3t' | base64 # encode
echo 'czNjcjN0' | base64 -d # decodeLeaving out -n on echo encodes the trailing newline too, which corrupts the value. That one character costs people exam points more often than you’d expect.
base64 is not encryption #
There is one thing an operator must engrave into their mind when handling Secrets: base64 is encoding, not encryption. Anyone can immediately turn it back into plaintext with base64 -d. In other words, whoever can view the Secret YAML or gets hold of an etcd dump reads the secret values as they are.
# Viewing a Secret value in plaintext needs no special privileges
k get secret db-secret -o jsonpath='{.data.DB_PASS}' | base64 -dSo what makes a Secret better than a ConfigMap is the following.
- RBAC separation. You can scope read access on Secrets separately from ConfigMaps to narrow access to secret values (#9).
- Reduced exposure surface. Some components are designed to print ConfigMaps but mask Secrets in logs or environment dumps.
- Target of etcd encryption. You can pick out Secrets alone and turn on encryption at storage time.
A paragraph on encryption at rest #
In a default cluster, Secrets are stored in etcd in base64 form — that is, effectively in plaintext. To prevent this, you must attach an EncryptionConfiguration to the apiserver via --encryption-provider-config to turn on encryption at rest. Then even if the etcd disk is stolen, the secret values aren’t immediately readable. This configuration, key rotation, and the KMS provider are control plane security topics, so we’ll cover them separately in #24 and in the follow-up CKS track. Here, it’s enough to lock in the fact that a Secret is not encrypted by default, and there is a separate setting to turn that on.
Injection 1: as environment variables #
The main subject is how you put the values you made into a container. The first way is environment variables.
env valueFrom: one key at a time #
spec:
containers:
- name: app
image: nginx
env:
- name: APP_COLOR
valueFrom:
configMapKeyRef:
name: app-config
key: APP_COLOR
- name: DB_PASS
valueFrom:
secretKeyRef:
name: db-secret
key: DB_PASSUse this when you want to pick a specific key and assign a separate variable name for use inside the container. ConfigMap uses configMapKeyRef, Secret uses secretKeyRef.
envFrom: the whole thing #
spec:
containers:
- name: app
image: nginx
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: db-secretThis injects all of the object’s keys as environment variables at once. Here the key name becomes the environment variable name verbatim, so it matters that you set the keys to the names you want them to have as environment variables. This is why a ConfigMap made with --from-env-file earlier meshes well with envFrom.
If you want to prefix the key names, use prefix.
envFrom:
- configMapRef:
name: app-config
prefix: CONF_ # becomes CONF_APP_COLOR, etc.Injection 2: as a volume #
The second way is a volume mount. Use it when you need to place the config file itself onto the container’s file system.
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: config-vol
mountPath: /etc/app
volumes:
- name: config-vol
configMap:
name: app-configIn this case, each key becomes a file under /etc/app/. The APP_COLOR key becomes the file /etc/app/APP_COLOR, with the value as its contents. Secret works the same way with a secret: volume.
subPath: just one file #
Mounting a volume plainly hides the existing contents of the mountPath directory. When you want to slot in just one file while keeping the directory intact, use subPath.
volumeMounts:
- name: config-vol
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
volumes:
- name: config-vol
configMap:
name: nginx-configThis keeps the /etc/nginx/ directory intact and overlays only the nginx.conf file with the ConfigMap’s value. But subPath has an important trap.
The auto-refresh difference between env and volume #
This is the crux you must know in operations. When you later change the contents of a ConfigMap or Secret, how it propagates to the container differs by injection method.
| Injection method | When the source changes |
|---|---|
| env / envFrom | Not propagated automatically. Pod restart required |
| volume mount | The file is auto-refreshed after a short delay |
| volume + subPath | Not auto-refreshed |
Environment variables are injected once when the container starts and don’t change after that. So to change a setting injected via env, you have to bring the Pod up again (#10’s kubectl rollout restart is used here).
A volume mount is synced periodically by the kubelet, so the file contents change after a short delay. That said, the app has to re-read the file for it to actually take effect, so an app that doesn’t watch the file will still need a restart in the end. And a file mounted via subPath is excluded from this auto-refresh, so if you change a setting often and want zero-downtime propagation, it’s right to avoid subPath.
immutable: performance and safety together #
For settings that don’t change often, you can declare ConfigMap and Secret as immutable.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_MODE: prod
immutable: trueSetting immutable: true has two effects.
- Safety. It prevents the accident of changing a production setting by mistake. Once immutable, you can’t modify
data; to change it, you delete the object and recreate it. - Performance. The kubelet no longer needs to watch for changes, so apiserver load drops on large clusters. The more objects you have, the bigger the effect.
Once a ConfigMap or Secret is made immutable, the natural pattern is to create a new object under a new name and roll out the workload to reference it.
Exam points #
- Get the three sources into your hands. A few keys means
--from-literal, a whole file means--from-file, an env-format file means--from-env-file. - Pick the Secret type to match the question. General-purpose is
generic, registry credentials aredocker-registry, certificates aretls. - Don’t drop
-nfromechowhen base64-encoding. Missing it costs points more often than expected. - Distinguish the injection method from the wording of the question. “As an environment variable” means
env/envFrom, “as a file” means a volume mount, “overlay just one file” means subPath. - When a setting doesn’t take effect after you change it, recall that env injection requires a Pod restart.
- The flow of pulling the YAML first with dry-run and hand-editing is faster and more accurate than writing an empty manifest from scratch.
Wrap-up #
What this post locked in:
- ConfigMap and Secret separate configuration from the workload. This is the foundation for running the same image differently per environment.
- The creation sources are the three
--from-literal,--from-file, and--from-env-file, and with an env-format file each line becomes a separate key. - For Secret types, remember
generic,docker-registry, andtls. base64 is encoding, not encryption, and the default etcd storage is effectively plaintext. To encrypt, you separately turn on encryption at rest (#24,CKS). - Injection splits into
env valueFrom(one key),envFrom(the whole thing), volume mount (files), and subPath (one file). - env is not auto-refreshed and needs a restart, a volume mount is auto-refreshed, and subPath is excluded from refresh.
immutable: truegets you both accident prevention and performance on large clusters.
Next — Scheduling 1 #
Having filled in the insides of the workload all the way to configuration injection, we now move on to scheduling, which decides which node to seat that workload on.
In #13 Scheduling 1, we’ll work through it manifest by manifest — from the basics of label-based placement with nodeSelector, to expressing more flexible node selection rules with nodeAffinity, to topology control that gathers or spreads Pods with podAffinity/podAntiAffinity — following the order in which the scheduler narrows down the nodes.