Configure Kubernetes Apps with ConfigMaps and Secrets

KubernetesBeginner
Practice Now

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:

  • envFrom turns every ConfigMap key into a container environment variable.
  • volumes connects the Secret to the Pod, while volumeMounts chooses 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.