Post

Kubernetes Learning Path: ConfigMaps and Secrets

Kubernetes Learning Path: ConfigMaps and Secrets

Kubernetes Learning Path: ConfigMaps and Secrets

In the previous post, we deployed our first application to Kubernetes. But here’s the thing—real applications need configuration. Database URLs, API keys, feature flags, and other settings that change between environments.

You definitely don’t want to hardcode these values into your container images. That would mean rebuilding your entire image just to change a database URL or update an API key. That’s where ConfigMaps and Secrets come in.

What Are ConfigMaps and Secrets?

Think of ConfigMaps and Secrets like configuration files that live inside Kubernetes. Instead of baking configuration into your container images, you store it separately and inject it into your pods at runtime.

ConfigMaps are for non-sensitive configuration data—things like application settings, feature flags, or database names.

Secrets are for sensitive data—like passwords, API keys, or certificates. They’re similar to ConfigMaps but have some extra protection (though they’re still just base64-encoded, not encrypted by default).

Let’s see how to use them with some practical examples.

Working with ConfigMaps

Create a ConfigMap from Literal Values

The simplest way to create a ConfigMap is from literal values on the command line:

1
2
3
kubectl create configmap app-config \
  --from-literal=APP_ENV=production \
  --from-literal=LOG_LEVEL=info

Output:

1
configmap/app-config created

Check what you just created:

1
kubectl get configmap app-config -o yaml

Output:

1
2
3
4
5
6
7
8
apiVersion: v1
data:
  APP_ENV: production
  LOG_LEVEL: info
kind: ConfigMap
metadata:
  name: app-config
  namespace: default

See how your key-value pairs are stored? Now let’s create one from a file.

Create a ConfigMap from a File

First, create a simple config file:

1
2
3
4
5
cat > app.properties << EOF
database.name=myapp
database.pool.size=20
cache.enabled=true
EOF

Now create a ConfigMap from this file:

1
kubectl create configmap app-config-file --from-file=app.properties

Output:

1
configmap/app-config-file created

View it:

1
kubectl describe configmap app-config-file

Output:

1
2
3
4
5
6
7
8
9
10
11
12
Name:         app-config-file
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
app.properties:
----
database.name=myapp
database.pool.size=20
cache.enabled=true

The entire file content is stored as a single key called app.properties.

Use ConfigMap as Environment Variables

Now let’s use our ConfigMap in a pod. Create a file called pod-with-configmap.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
  name: config-demo-pod
spec:
  containers:
  - name: demo
    image: alpine:latest
    command: ["sh", "-c", "echo APP_ENV=$APP_ENV && echo LOG_LEVEL=$LOG_LEVEL && sleep 3600"]
    env:
    - name: APP_ENV  # Environment variable name inside the container
      valueFrom:
        configMapKeyRef:
          name: app-config  # References the ConfigMap we created earlier
          key: APP_ENV      # Pulls the value of "APP_ENV" key from that ConfigMap
    - name: LOG_LEVEL
      valueFrom:
        configMapKeyRef:
          name: app-config  # Same ConfigMap as above
          key: LOG_LEVEL    # Pulls the value of "LOG_LEVEL" key

Apply it:

1
kubectl apply -f pod-with-configmap.yaml

Output:

1
pod/config-demo-pod created

Check the logs to see if the environment variables were injected:

1
kubectl logs config-demo-pod

Output:

1
2
APP_ENV=production
LOG_LEVEL=info

Great! The values from your ConfigMap are now available as environment variables inside the container.

Use ConfigMap as a Volume

Sometimes you want configuration files, not just environment variables. You can mount a ConfigMap as a volume. Create pod-with-config-volume.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
  name: config-volume-pod
spec:
  containers:
  - name: demo
    image: alpine:latest
    command: ["sh", "-c", "cat /config/app.properties && sleep 3600"]
    volumeMounts:
    - name: config-volume    # References the volume defined below
      mountPath: /config     # Where to mount the files inside the container
  volumes:
  - name: config-volume      # Volume name (must match volumeMounts above)
    configMap:
      name: app-config-file  # References the ConfigMap created from app.properties file

Apply it:

1
kubectl apply -f pod-with-config-volume.yaml

Output:

1
pod/config-volume-pod created

Check the logs:

1
kubectl logs config-volume-pod

Output:

1
2
3
database.name=myapp
database.pool.size=20
cache.enabled=true

The file from your ConfigMap is now mounted inside the container at /config/app.properties. This is handy when your app expects actual config files instead of environment variables.

Working with Secrets

Secrets work almost the same way as ConfigMaps, but they’re meant for sensitive data.

Create a Secret from Literal Values

1
2
3
kubectl create secret generic db-secret \
  --from-literal=username=admin \
  --from-literal=password=super-secret-password

Output:

1
secret/db-secret created

View it:

1
kubectl get secret db-secret -o yaml

Output:

1
2
3
4
5
6
7
8
9
apiVersion: v1
data:
  password: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
  username: YWRtaW4=
kind: Secret
metadata:
  name: db-secret
  namespace: default
type: Opaque

Notice the values are base64-encoded. That’s not encryption—it’s just encoding. Anyone with access to your cluster can decode them:

1
echo "YWRtaW4=" | base64 -d

Output:

1
admin

So Secrets aren’t super secure by default, but they’re better than ConfigMaps because Kubernetes handles them differently—they’re not written to disk on nodes unless needed, and you can enable encryption at rest if you want.

Create a Secret from a File

Create a file with sensitive data:

1
echo "my-super-secret-api-key" > api-key.txt

Create a Secret from it:

1
kubectl create secret generic api-secret --from-file=api-key.txt

Output:

1
secret/api-secret created

Use Secret as Environment Variables

Create pod-with-secret.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
  name: secret-demo-pod
spec:
  containers:
  - name: demo
    image: alpine:latest
    command: ["sh", "-c", "echo DB_USER=$DB_USER && echo DB_PASS=$DB_PASS && sleep 3600"]
    env:
    - name: DB_USER       # Environment variable name inside the container
      valueFrom:
        secretKeyRef:
          name: db-secret  # References the Secret we created earlier
          key: username    # Pulls the value of "username" key from that Secret
    - name: DB_PASS
      valueFrom:
        secretKeyRef:
          name: db-secret  # Same Secret as above
          key: password    # Pulls the value of "password" key

Apply it:

1
kubectl apply -f pod-with-secret.yaml

Output:

1
pod/secret-demo-pod created

Check the logs:

1
kubectl logs secret-demo-pod

Output:

1
2
DB_USER=admin
DB_PASS=super-secret-password

The secret values are automatically decoded and injected as plain text into your environment variables.

Use Secret as a Volume

Just like ConfigMaps, you can mount Secrets as volumes. Create pod-with-secret-volume.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
  name: secret-volume-pod
spec:
  containers:
  - name: demo
    image: alpine:latest
    command: ["sh", "-c", "cat /secrets/api-key.txt && sleep 3600"]
    volumeMounts:
    - name: secret-volume    # References the volume defined below
      mountPath: /secrets    # Where to mount the secret files inside the container
      readOnly: true         # Good security practice for secrets
  volumes:
  - name: secret-volume      # Volume name (must match volumeMounts above)
    secret:
      secretName: api-secret # References the Secret created from api-key.txt file

Apply it:

1
kubectl apply -f pod-with-secret-volume.yaml

Output:

1
pod/secret-volume-pod created

Check the logs:

1
kubectl logs secret-volume-pod

Output:

1
my-super-secret-api-key

The secret file is mounted and readable inside the container. Notice we set readOnly: true—that’s a good practice for security.

Practical Example: nginx with Custom Config

Let’s put it all together. We’ll deploy nginx with a custom config file from a ConfigMap, and also inject both ConfigMap and Secret values as environment variables. This shows how you can use multiple ConfigMaps and Secrets in a single deployment.

First, create a simple custom nginx config file:

1
2
3
4
5
6
7
8
9
cat > default.conf << EOF
server {
    listen 80;
    location / {
        return 200 'Hello from Kubernetes!\nThis config came from a ConfigMap.\n';
        add_header Content-Type text/plain;
    }
}
EOF

Create a ConfigMap from it:

1
kubectl create configmap nginx-config --from-file=default.conf

Output:

1
configmap/nginx-config created

Now create a deployment that uses both ConfigMap and Secret. Create nginx-deployment-with-config.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-with-config
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        env:
        # Environment variable from ConfigMap
        - name: APP_ENV                      # Variable name in the container
          valueFrom:
            configMapKeyRef:
              name: app-config               # References ConfigMap created with --from-literal
              key: APP_ENV                   # Gets the "APP_ENV" key value
        # Environment variable from Secret
        - name: API_KEY                      # Variable name in the container
          valueFrom:
            secretKeyRef:
              name: api-secret               # References Secret created from api-key.txt
              key: api-key.txt               # Gets the file content as the value
        volumeMounts:
        - name: nginx-config                 # Must match volume name below
          mountPath: /etc/nginx/conf.d/default.conf  # Exact file path in container
          subPath: default.conf              # Mounts only this file, not entire directory
      volumes:
      - name: nginx-config                   # Volume name referenced above
        configMap:
          name: nginx-config                 # References ConfigMap created from default.conf

Notice the subPath: default.conf—this mounts just the single file instead of the entire directory, which prevents nginx from having issues with its default config.

Apply it:

1
kubectl apply -f nginx-deployment-with-config.yaml

Output:

1
deployment.apps/nginx-with-config created

Wait for the pod to be ready:

1
kubectl get pods -l app=nginx

Output:

1
2
NAME                                READY   STATUS    RESTARTS   AGE
nginx-with-config-7d8c9f5b4-x9k2p   1/1     Running   0          15s

Test it with port-forwarding:

1
kubectl port-forward deployment/nginx-with-config 8080:80

In another terminal:

1
curl http://localhost:8080

Output:

1
2
Hello from Kubernetes!
This config came from a ConfigMap.

Perfect! Now let’s verify the environment variables are available inside the container:

1
kubectl exec deployment/nginx-with-config -- sh -c 'echo "APP_ENV=$APP_ENV" && echo "API_KEY=$API_KEY"'

Output:

1
2
APP_ENV=production
API_KEY=my-super-secret-api-key

Your nginx is now running with configuration from a ConfigMap and has access to both the ConfigMap and Secret values through environment variables.

Quick Experiments

Update a ConfigMap

You can update a ConfigMap and see how it affects running pods. Note that environment variables won’t update automatically—you need to restart the pod. But volume-mounted configs can update automatically (though it might take a minute).

1
kubectl edit configmap app-config

This opens an editor where you can change the values. Change APP_ENV from production to staging and save.

For the changes to take effect in pods using environment variables, you need to restart them:

1
kubectl rollout restart deployment/nginx-with-config

Output:

1
deployment.apps/nginx-with-config restarted

View Secret Values

You already saw that secrets are just base64-encoded. Let’s decode one:

1
kubectl get secret db-secret -o jsonpath='{.data.password}' | base64 -d

Output:

1
super-secret-password

This is why you still need to protect access to your Kubernetes cluster—secrets aren’t truly encrypted without additional setup.

Clean Up

Delete all the resources we created:

1
2
3
4
5
kubectl delete pod config-demo-pod config-volume-pod secret-demo-pod secret-volume-pod
kubectl delete deployment nginx-with-config
kubectl delete configmap app-config app-config-file nginx-config
kubectl delete secret db-secret api-secret
rm -f app.properties api-key.txt default.conf

Output:

1
2
3
4
5
6
7
8
9
10
pod "config-demo-pod" deleted
pod "config-volume-pod" deleted
pod "secret-demo-pod" deleted
pod "secret-volume-pod" deleted
deployment.apps "nginx-with-config" deleted
configmap "app-config" deleted
configmap "app-config-file" deleted
configmap "nginx-config" deleted
secret "db-secret" deleted
secret "api-secret" deleted

What You Learned

✅ How to create ConfigMaps from literal values and files
✅ How to inject ConfigMaps as environment variables and volumes
✅ How to create and use Secrets for sensitive data
✅ How to mount Secrets in pods
✅ The difference between ConfigMaps and Secrets

That’s it! You can now keep configuration separate from your container images, which makes your apps way more flexible when moving between environments.

What’s Next?

In Part 3 of this series, we’ll explore Understanding Namespaces in Kubernetes. You’ll learn how to:

  • Organize resources using namespaces
  • Deploy apps to different environments
  • Work with namespace-specific configurations

Stay tuned!


Series Navigation

Part 1: Deploy Your First App
Part 2: ConfigMaps and Secrets ← You just finished this!
Part 3: Understanding Namespaces (Coming soon)
Part 4: Persistent Storage (Coming soon)
Part 5: Ingress and Load Balancing (Coming soon)


Found a mistake or have questions? Feel free to open an issue here.

Thanks for reading! Feel free to share this post with others.