Kubernetes ConfigMaps

Lesson 10 of 39 13 min read Updated

Every application needs configuration: database hostnames, log levels, feature flags, service URLs. If you bake these values into your container image, you need a different image for development, staging and production. That breaks one of the core promises of containers: build once, run anywhere.

A ConfigMap is a Kubernetes object that holds non-sensitive configuration as key-value pairs. Think of it as the settings sheet you hand to an app when it starts. The app (the image) stays the same, and each environment hands it a different sheet.

Kubernetes combines the ConfigMap with your Pod right before the container starts. The container can see the data as environment variables, as files in a small read-only folder, or as command-line arguments. Your code and your runtime configuration stay cleanly apart.

What you will learn

  1. What a ConfigMap is and why it beats environment variables scattered across manifests
  2. How to create, inspect, edit and delete ConfigMaps with kubectl
  3. The three ways a Pod consumes a ConfigMap: environment variables, files and command-line arguments
  4. How to feed a .NET app its settings and an appsettings.json file from ConfigMaps
  5. What happens to running Pods when you change a ConfigMap

What a ConfigMap is

A ConfigMap is a dictionary of string keys and string values, stored in the cluster as a Kubernetes object. You can picture it in two ways:

  1. As a small filesystem: each key becomes a file name and each value becomes the file content.
  2. As a set of environment variables: each key-value pair becomes a variable inside the container.

ConfigMaps are for non-sensitive data: log levels, service URLs, feature toggles, connection strings without passwords. A ConfigMap can hold at most 1 MiB, so it is meant for settings, not for large files.

A ConfigMap lives in a namespace. Pods in the same namespace can reference it. Pods in another namespace cannot.

Never put passwords in a ConfigMap. Anyone who can read the ConfigMap sees its values in plain text. Passwords, API keys and certificates belong in a Secret, which is the next lesson.

Why not put environment variables straight into the Pod spec?

You could, but it gets painful fast:

  1. Configuration is scattered across many Pod and Deployment manifests instead of living in one place.
  2. Changing one value means editing every manifest that uses it.
  3. Environment variables cannot give your app a whole configuration file, such as appsettings.json, at a known path.

ConfigMaps fix all three. You define the configuration once, reference it from as many Pods as you like and update it in one place.

Three ways to consume a ConfigMap

MethodHow it worksBest for
Environment variablesEach key-value pair becomes an environment variable inside the container.Simple values: log levels, ports, feature flags.
Volume (files)The ConfigMap is mounted as a folder. Each key becomes a file and its value becomes the file content.Structured config files: appsettings.json, nginx.conf.
Command-line argumentsEnvironment variables that come from the ConfigMap are referenced in the container's command or args with $(VAR_NAME).CLI tools that take flags or positional arguments.
You can mix all three in one Pod. The hands-on example uses environment variables and a mounted file in the same Deployment, then shows command-line arguments in a small Pod of its own.

Working with ConfigMaps from the command line

The fastest way to create a ConfigMap is kubectl create configmap. You can feed it literal values, files or whole directories.

From literal values

Use --from-literal to pass key-value pairs directly:

kubectl create configmap app-settings \
--from-literal=LOG_LEVEL=Information \
--from-literal=MAX_RETRIES=5

This creates a ConfigMap named app-settings with two keys: LOG_LEVEL and MAX_RETRIES.

From a file

Suppose you have a file called my-config.txt:

# This is a sample config file that I might use to configure an application
parameter1 = value1
parameter2 = value2

Load it with --from-file. You can combine --from-file and --from-literal in one command:

kubectl create configmap my-config \
--from-file=my-config.txt \
--from-literal=extra-param=extra-value \
--from-literal=another-param=another-value

The file name (my-config.txt) becomes the key and the whole file content becomes the value. To choose a different key, write --from-file=<key>=<path>, for example --from-file=settings.txt=my-config.txt.

From a directory

Point --from-file at a folder to load every file in it:

kubectl create configmap dir-config --from-file=./config-dir/

Each regular file in the folder becomes one key. Subfolders are skipped.

Generate a manifest instead

If you prefer to keep configuration in Git, let kubectl write the YAML for you without creating anything in the cluster:

kubectl create configmap app-settings \
--from-literal=LOG_LEVEL=Information \
--dry-run=client -o yaml > app-settings.yaml

View a ConfigMap

Print the full YAML, including the data section:

kubectl get configmap my-config -o yaml

Expected output (metadata such as uid and creationTimestamp trimmed):

apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
namespace: default
data:
another-param: another-value
extra-param: extra-value
my-config.txt: |
# This is a sample config file that I might use to configure an application
parameter1 = value1
parameter2 = value2

The file content is stored word for word under the key my-config.txt. The | in YAML keeps the newlines of a multi-line value.

For a quick summary, or to list ConfigMaps:

# Human-readable summary of one ConfigMap
kubectl describe configmap my-config

# List ConfigMaps in the current namespace, another namespace, or all of them
kubectl get configmaps
kubectl get configmaps -n <namespace>
kubectl get configmaps --all-namespaces

Edit a ConfigMap

kubectl edit configmap my-config

This opens the ConfigMap in your default editor. Save and close to apply the change. Mounted files pick up the change on their own, but environment variables do not. Step 9 below shows what that means in practice.

Delete a ConfigMap

kubectl delete configmap my-config
Deleting a ConfigMap can stop Pods from starting. A new Pod that references a missing ConfigMap waits in CreateContainerConfigError or ContainerCreating until you recreate it. If a reference is truly optional, add optional: true to it.

Hands-on: configure a .NET Notification API

You will build a realistic setup: a .NET Notification API that needs SMTP settings, a log level and rate-limit settings. You will create two ConfigMaps and a Deployment that reads them through environment variables and a mounted file.

The Deployment uses Microsoft's public sample image mcr.microsoft.com/dotnet/samples:aspnetapp. It is a small ASP.NET Core app that listens on port 8080 and answers health checks on /healthz. It stands in for your own Notification API image, so every command below works on a local cluster such as kind, minikube or Docker Desktop.

Step 1 — Create the key-value ConfigMap

This ConfigMap holds simple settings for SMTP, retries, logging and CORS. Save it as notification-api-config.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
name: notification-api-config
data:
SMTP_HOST: "mail.internal.local"
SMTP_PORT: "587"
RETRY_COUNT: "3"
LOG_LEVEL: "Information"
ALLOWED_ORIGINS: "https://portal.example.com,https://admin.example.com"

Apply it:

kubectl apply -f notification-api-config.yaml

Expected output:

configmap/notification-api-config created

Every value in a ConfigMap is a string. That is why "587" and "3" are quoted: without quotes YAML reads them as numbers and the API server rejects the manifest. Your application parses the strings into the right types.

Step 2 — Create the file-based ConfigMap

This ConfigMap holds a whole appsettings.Production.json file, for apps that expect a structured configuration file at a known path. Save it as notification-api-appsettings.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
name: notification-api-appsettings
data:
appsettings.Production.json: |
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"SmtpSettings": {
"Host": "mail.internal.local",
"Port": 587,
"UseTls": true
},
"RateLimiting": {
"PermitLimit": 100,
"WindowSeconds": 60
}
}

Apply it:

kubectl apply -f notification-api-appsettings.yaml

When you mount this ConfigMap as a volume, the key appsettings.Production.json becomes the file name and the JSON after the | becomes its content. Your .NET app reads it like any other appsettings file.

Step 3 — Create the Deployment that uses both ConfigMaps

This Deployment uses both environment-variable methods and a volume mount in one resource. Save it as notification-api-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: notification-api
labels:
app: notification-api
spec:
replicas: 2
selector:
matchLabels:
app: notification-api
template:
metadata:
labels:
app: notification-api
spec:
containers:
- name: notification-api
image: mcr.microsoft.com/dotnet/samples:aspnetapp
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: SmtpSettings__Host
valueFrom:
configMapKeyRef:
name: notification-api-config
key: SMTP_HOST
- name: SmtpSettings__Port
valueFrom:
configMapKeyRef:
name: notification-api-config
key: SMTP_PORT
- name: Logging__LogLevel__Default
valueFrom:
configMapKeyRef:
name: notification-api-config
key: LOG_LEVEL
envFrom:
- configMapRef:
name: notification-api-config
volumeMounts:
- name: appsettings-volume
mountPath: /app/config
readOnly: true
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
volumes:
- name: appsettings-volume
configMap:
name: notification-api-appsettings

Apply it and wait until both replicas are ready:

kubectl apply -f notification-api-deployment.yaml
kubectl rollout status deployment/notification-api

Expected output:

deployment "notification-api" successfully rolled out

The next three steps take this Deployment apart, one ConfigMap method at a time.

Step 4 — Pick individual keys with valueFrom

The env list picks specific keys from a ConfigMap and gives each one the environment variable name you choose:

env:
- name: SmtpSettings__Host # The variable name your app sees
valueFrom:
configMapKeyRef:
name: notification-api-config # Which ConfigMap
key: SMTP_HOST # Which key inside it

Use this when the ConfigMap key names do not match what your app expects. In .NET, a double underscore (__) separates configuration sections, so SmtpSettings__Host becomes SmtpSettings:Host in IConfiguration.

Step 5 — Load every key with envFrom

The envFrom block injects every key in the ConfigMap as an environment variable with the same name:

envFrom:
- configMapRef:
name: notification-api-config

The container now has SMTP_HOST, SMTP_PORT, RETRY_COUNT, LOG_LEVEL and ALLOWED_ORIGINS. This is the quickest option when you control the key names. If a name appears in both env and envFrom, the value from env wins.

Step 6 — Read the mounted configuration file

The volume mount projects the second ConfigMap as files inside the container:

volumeMounts:
- name: appsettings-volume
mountPath: /app/config # Folder where the files appear
readOnly: true

volumes:
- name: appsettings-volume
configMap:
name: notification-api-appsettings

Each key becomes one file, so the container sees /app/config/appsettings.Production.json. A ConfigMap with three keys would produce three files. Check it from a running Pod:

kubectl exec deploy/notification-api -- ls /app/config/

Expected output:

appsettings.Production.json

Print the file content:

kubectl exec deploy/notification-api -- cat /app/config/appsettings.Production.json

ASP.NET Core loads appsettings.Production.json from the app folder (/app), not from /app/config. In your own app, add the mounted file to configuration in Program.cs:

builder.Configuration.AddJsonFile("config/appsettings.Production.json", optional: true, reloadOnChange: true);
Do not mount the volume over your app folder. A volume mount hides everything that was already in the target folder. Mounting at /app would hide your app's DLLs and the container would fail to start. Mount into a subfolder such as /app/config. A subPath mount can place a single file instead, but that file never receives ConfigMap updates.

Step 7 — Verify the environment variables

Confirm that the values were injected. The pipe runs on your machine, so only printenv runs inside the container:

kubectl exec deploy/notification-api -- printenv | grep -E "SMTP|LOG_|RETRY|ALLOWED|SmtpSettings|Logging"

Expected output (the order may differ):

SmtpSettings__Host=mail.internal.local
SmtpSettings__Port=587
Logging__LogLevel__Default=Information
SMTP_HOST=mail.internal.local
SMTP_PORT=587
RETRY_COUNT=3
LOG_LEVEL=Information
ALLOWED_ORIGINS=https://portal.example.com,https://admin.example.com
That is the payoff. The image knows nothing about SMTP hosts or log levels. Point the same Deployment at a different ConfigMap and you have staging or production, with no rebuild.

Step 8 — Pass ConfigMap values as command-line arguments

The third method puts ConfigMap values into the container's startup command. Kubernetes replaces each $(VAR_NAME) in command or args with the value of that environment variable. Save this standalone Pod as config-cmd-demo.yaml:

apiVersion: v1
kind: Pod
metadata:
name: config-cmd-demo
spec:
containers:
- name: demo
image: busybox:1.37
command: ["sh", "-c", "echo Host=$(SMTP_HOST) Port=$(SMTP_PORT) && sleep 3600"]
env:
- name: SMTP_HOST
valueFrom:
configMapKeyRef:
name: notification-api-config
key: SMTP_HOST
- name: SMTP_PORT
valueFrom:
configMapKeyRef:
name: notification-api-config
key: SMTP_PORT
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "100m"
memory: "64Mi"
restartPolicy: Never

Apply it and read the log:

kubectl apply -f config-cmd-demo.yaml
kubectl logs config-cmd-demo

Kubernetes expands the variables before the shell runs, so the command becomes sh -c "echo Host=mail.internal.local Port=587 && sleep 3600". Expected output:

Host=mail.internal.local Port=587

This is handy for tools that read their configuration from command-line flags rather than from environment variables.

Step 9 — Update a ConfigMap

Change the log level from Information to Debug by editing the ConfigMap in place:

kubectl edit configmap notification-api-config

Or change LOG_LEVEL in notification-api-config.yaml and apply the file again:

kubectl apply -f notification-api-config.yaml

What running Pods see after the update depends on how they consume the ConfigMap:

Consumed asUpdated in running Pods?What to do
Mounted filesYes, automatically, usually within a minute or two (kubelet sync period plus cache delay)Make sure your app re-reads the file. In .NET, reloadOnChange: true asks for that.
Files mounted with subPathNoRestart the Pods, or mount the whole folder instead.
Environment variables (env, envFrom)No, they are set once when the container startsRestart the Pods.
Command-line argumentsNo, they are expanded once at startRestart the Pods.

To restart a Deployment's Pods without downtime, roll them:

kubectl rollout restart deployment/notification-api

Step 10 — Clean up

Remove everything you created in this lesson:

kubectl delete deployment notification-api
kubectl delete pod config-cmd-demo
kubectl delete configmap notification-api-config notification-api-appsettings
kubectl delete configmap app-settings my-config dir-config --ignore-not-found

If the manifests are in the same folder, you can also delete by file:

kubectl delete -f notification-api-deployment.yaml
kubectl delete -f config-cmd-demo.yaml
kubectl delete -f notification-api-config.yaml
kubectl delete -f notification-api-appsettings.yaml

Common mistakes

SymptomLikely causeFix
Pod stuck in CreateContainerConfigErrorThe ConfigMap or the key does not exist, or lives in another namespaceCheck the names with kubectl get configmap -o yaml and create it in the Pod's namespace
kubectl apply rejects the ConfigMapAn unquoted number or boolean under dataQuote every value: "587", "true"
App still uses the old valueThe value comes from an environment variablekubectl rollout restart deployment/<name>
Mounted file never changesIt is mounted with subPathMount the folder, or restart the Pods after each change
App files disappeared from the containerThe volume was mounted over the app folderMount into a subfolder such as /app/config

Lock settings with immutable ConfigMaps

If a ConfigMap should never change after you create it, add immutable: true at the top level, next to data. Kubernetes then rejects any edit, which protects you from accidental changes and lowers the load on the API server in large clusters. To change an immutable ConfigMap, delete and recreate it, then restart the Pods that use it.

apiVersion: v1
kind: ConfigMap
metadata:
name: notification-api-config-v2
immutable: true
data:
LOG_LEVEL: "Warning"

Summary

ConfigMaps separate configuration from container images, so you can follow the build once, run anywhere principle. The same image runs everywhere; only the ConfigMap changes.

  1. A ConfigMap stores non-sensitive configuration as string key-value pairs inside a namespace, up to 1 MiB.
  2. Create one imperatively with kubectl create configmap (from literals, files or folders) or declaratively from a YAML manifest.
  3. Consume it as environment variables (configMapKeyRef for single keys, envFrom for all keys), as files in a mounted volume, or as command-line arguments with $(VAR_NAME).
  4. Mounted files update automatically after a short delay. Environment variables and arguments need a Pod restart: kubectl rollout restart.
  5. ConfigMaps are for non-sensitive data only. Passwords, API keys and certificates go into Secrets.

Next, you will meet Secrets, which use the same patterns for the data you must keep private.