Introduction
Container images should be reusable across environments. Kubernetes separates non-sensitive settings into ConfigMaps and credentials into Secrets, then injects either object as environment variables or files.
You will configure one application with both mechanisms, inspect the result safely, update configuration, and observe why environment variables and mounted volumes refresh differently.
The goal is not only to create two API objects. You will follow each value all the way from Kubernetes storage into a running container, then change it and observe how the delivery mechanism affects refresh behavior.
Create Application Configuration
In this step, create a namespace and a ConfigMap from literal values. A ConfigMap stores string keys under data; it does not encrypt them.
Move to the lab workspace first. The namespace keeps every object for this example together and prevents similarly named configuration in other labs from being selected accidentally.
cd /home/labex/project/application-config
kubectl create namespace practice-config
kubectl create configmap app-settings -n practice-config --from-literal=APP_MODE=production --from-literal=LOG_LEVEL=info
kubectl get configmap app-settings -n practice-config -o yaml
Each --from-literal=KEY=VALUE adds one entry. The YAML output makes the stored keys visible and is appropriate for non-sensitive configuration.
Look under data: for APP_MODE and LOG_LEVEL. Kubernetes stores the desired values, but nothing consumes them yet; creating a ConfigMap alone does not alter a Pod.
The relevant part of the output is deliberately plain text:
data:
APP_MODE: production
LOG_LEVEL: info
kind: ConfigMap
Fields such as uid, resourceVersion, and creationTimestamp are generated by Kubernetes and will differ. For this step, read only data, because that is the configuration you supplied.
Create a Secret Safely
In this step, create a Secret without placing its value in a YAML file. Secret data is base64-encoded in the API, not encrypted merely because the object kind is Secret; RBAC and encryption at rest remain important.
generic means this is an ordinary key-value Secret rather than a specialized TLS or image-pull Secret. Quotes around the password prevent the shell from interpreting punctuation if the value later contains special characters.
kubectl create secret generic app-credentials -n practice-config --from-literal=username=appuser --from-literal=password='practice-pass'
kubectl get secret app-credentials -n practice-config
kubectl get secret app-credentials -n practice-config -o jsonpath='{.data.username}' | base64 --decode; echo
The final pipeline decodes only the username for demonstration. Avoid printing passwords in routine operational output.
The pipe character sends the encoded JSONPath result into base64 --decode. The semicolon starts a final echo, which adds a newline after the decoded value so the next shell prompt remains readable. Seeing appuser proves how encoding works; it also demonstrates why base64 must never be treated as a security boundary.
The middle command reports metadata without revealing either value:
NAME TYPE DATA AGE
app-credentials Opaque 2 ...
appuser
DATA=2 means that the Secret contains two keys; it is not a byte count. Opaque is the API type used for an ordinary key-value Secret.
Inject Configuration into a Pod
In this step, use ConfigMap values as environment variables and mount the Secret as read-only files.
Before creating the Pod, separate the two delivery paths in the manifest:
envFromturns every ConfigMap key into a container environment variable.volumesconnects the Secret to the Pod, whilevolumeMountschooses where that volume appears inside this particular container.
The Pod's loop does no application work; it simply keeps the container running so you can inspect both paths.
cd /home/labex/project/application-config
cat <<'EOF' > configured-app.yaml
apiVersion: v1
kind: Pod
metadata:
name: configured-app
namespace: practice-config
spec:
containers:
- name: app
image: busybox:1.36
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "while true; do sleep 30; done"]
envFrom:
- configMapRef:
name: app-settings
volumeMounts:
- name: credentials
mountPath: /etc/app-credentials
readOnly: true
volumes:
- name: credentials
secret:
secretName: app-credentials
EOF
Apply the manifest and wait for the Pod to become ready:
kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s
Now inspect each delivery path from inside the container. printenv reads process environment, while ls shows the files projected by the Secret volume:
kubectl exec configured-app -n practice-config -- printenv APP_MODE LOG_LEVEL
kubectl exec configured-app -n practice-config -- ls -l /etc/app-credentials
envFrom imports all ConfigMap keys. The Secret volume creates one file per key and is marked read-only inside the container.
Expected environment output is production followed by info. The directory should contain files named username and password; filenames correspond to Secret keys.
In the real container, the listing looks similar to this:
production
info
total 0
lrwxrwxrwx ... password -> ..data/password
lrwxrwxrwx ... username -> ..data/username
The entries are symbolic links managed by Kubernetes. This indirection lets kubelet switch the mounted data to a new version without rewriting a file while the application is reading it. Your application can still open the familiar paths /etc/app-credentials/username and /etc/app-credentials/password.
Update Mounted Secret Data
In this step, update the Secret and observe the mounted file. kubectl create ... --dry-run=client -o yaml | kubectl apply -f - generates desired YAML locally and pipes it directly to apply; - means standard input.
This pattern is useful for updating an imperatively generated object without deleting it. The left side prints a complete Secret manifest, and the right side sends that manifest to the API server.
kubectl create secret generic app-credentials -n practice-config --from-literal=username=serviceuser --from-literal=password='rotated-pass' --dry-run=client -o yaml | kubectl apply -f -
Because the Secret was originally created with kubectl create, the first update can include this warning before the success line:
Warning: resource secrets/app-credentials is missing the kubectl.kubernetes.io/last-applied-configuration annotation ...
secret/app-credentials configured
The warning explains that apply has no previous declarative snapshot to compare with. Kubernetes adds the annotation automatically during this update, so configured confirms success; you do not need to delete and recreate the Secret.
Kubelet updates projected Secret volume content asynchronously. Poll until the new username appears:
current_username=""
for attempt in $(seq 1 24); do
current_username=$(kubectl exec configured-app -n practice-config -- cat /etc/app-credentials/username)
if [ "$current_username" = "serviceuser" ]; then
break
fi
sleep 5
done
echo "$current_username"
test "$current_username" = "serviceuser"
The loop retries for up to two minutes because volume projection is eventually consistent. In the tested environment, the API object changed immediately but the mounted file needed longer than one minute to refresh.
$(...) is command substitution: the shell runs seq 1 24 and uses its numbers as loop values. Each iteration stores the current file content in current_username; break stops at the first match, and sleep 5 avoids continuously polling the API. The final test returns a failure if the value never changed, so the command cannot silently continue with stale data. Successful output is serviceuser.
Refresh Environment Configuration
In this step, update LOG_LEVEL and compare it with the running Pod. Environment variables are fixed when a container starts, so changing the ConfigMap does not rewrite the current process environment.
The merge patch changes only one entry under data. Immediately reading the running process demonstrates an important difference from a mounted volume:
kubectl patch configmap app-settings -n practice-config --type=merge -p '{"data":{"LOG_LEVEL":"debug"}}'
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL
The Pod still prints info. Recreate it from the saved manifest to consume the new value:
configmap/app-settings patched
info
This is expected evidence, not a failed patch: the ConfigMap contains debug, but the already-running process still has the environment created at startup.
cd /home/labex/project/application-config
kubectl delete pod configured-app -n practice-config --wait=true
kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL
The replacement Pod prints debug. This difference is why production workloads commonly trigger a rollout after environment-based configuration changes.
pod "configured-app" deleted
pod/configured-app created
pod/configured-app condition met
debug
The manifest itself still references the ConfigMap by name; no value was copied into configured-app.yaml. Recreating the Pod causes kubelet to resolve that reference again and inject the latest data.
Summary
You separated application settings and credentials from an image, consumed ConfigMap data as environment variables, mounted Secret data as files, rotated a projected Secret, and recreated a Pod to refresh environment-based configuration.


