Mounting a ConfigMap to an Existing Volume in Kubernetes
In this quick fix, I’ll detail how I FINALLY solved an issue that’s been plaguing me for quite some time: Mounting a single file stored in a ConfigMap into an existing directory in a Docker container on Kubernetes.
Introduction
When I build applications to deploy with Docker, I expose configuration options through environment variables. This makes updating existing deployments simple — update the environment variable of a container and you can update the configuration.
However, many applications deployed with Docker were not originally designed to run in Docker. These applications will often rely on configuration files in a config directory to store and update the configuration of an application.
In this QuickFix, we’ll look at how to use a config map as a configuration file, and how to mount it into an existing config directory without clobbering the directory on the container.
The Problem
Kubernetes has a feature that allows you to add files to a config map and mount the ConfigMap as a directory into a container. The keys in the config map become the file names, and the values in the config map become the files.
As an example, let’s look at a ConfigMap named test that contains two keys, Readme.md
and test.txt
. The values of those keys are the contents we want in those files.
apiVersion: v1
data:
Readme.md: '# This is a readme, mounted from a config map'
test.txt: This is a text file that was mounted from a config map.
kind: ConfigMap
metadata:
name: test
namespace: default
We can mount these config maps into a container by creating a volume mount. In the example below, we’ve mounted the config map to the /config
path in the container.
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
namespace: default
spec:
template:
spec:
containers:
- image: ubuntu:xenial
volumeMounts:
- mountPath: /config
name: vol1
volumes:
- configMap:
name: test
name: vol1
This will create a new directory called /config
inside which will be our Readme.md
and test.txt
files:
root@test:/# ls
bin boot config dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr varroot@test:/# cd configroot@test:/config# ls
Readme.md test.txtroot@test:/config# cat Readme.md
# This is a readme, mounted from a config maproot@test:/config# cat test.txt
This is a text file that was mounted from a config map
This works well when the directory we want to mount into is empty or does not exist in the container. However, if the directory exists, the mount will overwrite that directory.
Many applications store their application configuration files in the same directory, and often times the developer will only want to override a small number of them.
For example, let’s say my application looks for database configuration in a file called database.yml
in the /config
subdirectory of my application. The working directory in the container is /usr/src/app
and that I’d like to mount a file called database.yml
from a ConfigMap into an existing config
directory within that working directory. The config
directory already contains a configuration file called default_config.yml
which I don’t want to change.
Before mounting, the directory looks like this:
config
└── default_config.yml
└── application.yml
What I’d like to happen after injecting the config maps using a volume mount is this:
config
└── default_config.yml
└── application.yml
However, since the default ConfigMap volume mount overwrites the directory, I end up with the following:
config
└── database.yml
Since my application relies on default_config.yml
and since the application also looks for the database.yml
in the same subdirectory, I need to mount the file from the ConfigMap as a volume without overwriting the entire /config
directory.
The Solution
We can use the subPath option when mounting our ConfigMap to tell Kubernetes not to overwrite the entire directory. The subPath option tells Kubernetes to mount the ConfigMap as a subpath within an existing directory, rather than as its own separate volume mount.
We’ll solve this by changing two items in our original file. First, we’ll update the mountPath to point to the specific file location that we want to place the contents of our file into. We’ll also specify that file as a subPath in our mount:
Previously:
volumeMounts:
- mountPath: /config
name: vol1
Now:
volumeMounts:
- mountPath: /usr/src/app/config/database.yml
name: vol1
subPath: database.yml
This will ensure that the database.yml is a file and not a directory.
Next, we’ll update our ConfigMap mount to point to the specific key (with the same name as the subPath) as the file, and use that file name as the path we wish to mount to:
Previously:
volumes:
- configMap:
name: test
name: vol1
Now:
volumes:
- configMap:
items:
- key: database.yml
path: database.yml
name: test
name: vol1
The final result looks like the following:
apiVersion: v1
data:
database.yml: 'credentials-go-here'
kind: ConfigMap
metadata:
name: config
namespace: default
---------------
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
namespace: default
template:
spec:
containers:
image: ubuntu:xenial
name: test
resources: {}
volumeMounts:
- mountPath: /usr/src/app/config/database.yml
name: vol1
subPath: database.yml
volumes:
- configMap:
items:
- key: database.yml
path: database.yml
name: config
name: vol1