Balance innovation and agility with security and compliance
risks using a 3-step process across all cloud infrastructure.
Step up business agility without compromising
security or compliance
Everything you need to become a Kubernetes expert.
Always for free!
Everything you need to know about Magalix
culture and much more
In one of our articles, we discussed the environment variable pattern; how to define them, where to use them and their potential drawbacks. We briefly touched the configMap and Secrets resources as a means of injecting external configuration settings to the Pod (and its running containers). In this article, we have a deeper demonstration of the configMap and Secret usage patterns and best practices.
There are several considerations that you must take into account when deciding to use environment variables for all your configuration needs:
Let’s have a practical lab. In this lab, we launch a Python Flask application that uses an external configuration file to define which message the API should respond with. The image expects to find a file called main.py in a directory called app. The main.py file looks as follows:
from flask import Flask app = Flask(__name__) @app.route("/") def hello(): app.config.from_pyfile('/config/config.cfg') return app.config['MSG'] if __name__ == "__main__": # Only for debugging while developing app.run(host='0.0.0.0', debug=True, port=80)
Line 3 loads a configuration file called /config.cfg and makes use of it on line 6, where it returns the message to the client. The configuration file, config.cfg contents are as follows:
MSG="Welcome to Flask!"
Finally, the Dockerfile looks as follows:
FROM tiangolo/uwsgi-nginx-flask:python3.7 COPY ./app /app COPY config.cfg /config/config.cfg
Let’s build the image, run the container and verify that it works as expected:
$ docker build -t magalixcorp/myflask . Sending build context to Docker daemon 5.632kB Step 1/3 : FROM tiangolo/uwsgi-nginx-flask:python3.7 ---> 53353fb7df32 Step 2/3 : COPY ./app /app ---> 398f39d565cb Step 3/3 : COPY config.cfg /config/config.cfg ---> ebf4ac67f220 Successfully built ebf4ac67f220 Successfully tagged magalixcorp/myflask:latest $ docker run -p 8080:80 -d magalixcorp/myflask 9f4d904231ead30b38d0f858f0af816a22bf9878c9342d0712ecf90b6b0fc440 $ curl localhost:8080 Welcome to Flask!
Now, we need to run this application on Kubernetes. First, we need to create a configMap to hold our configuration file:
kind: ConfigMap apiVersion: v1 metadata: name: appconfig data: config.cfg: | MSG="Welcome to Flask!"
Then, comes our Pod definition:
kind: Pod apiVersion: v1 metadata: name: app spec: containers: - name: app image: magalixcorp/myflask volumeMounts: - name: config-vol mountPath: /config volumes: - name: config-vol configMap: name: appconfig
As you can see, we are mounting the /config directory to a volume that is backed up by our configMap. Each item in the configMap data part is transformed into a file on the volume. The key becomes the file name and the value is the file contents.
Now, let’s apply the definitions and test our configuration:
$ kubectl apply -f configmap.yml configmap/appconfig configured $ kubectl apply -f pod.yml pod/app created $ kubectl exec -it app -- bash root@app:/app# curl localhost Welcome to Flask!root@app:/app#
The container has a volume mounted under /config containing the config.cfg file. The Python script is designed so that it reads the configuration file each time an HTTP request arrives. Let’s change the message that the API outputs by changing the configMap:
kind: ConfigMap apiVersion: v1 metadata: name: appconfig data: config.cfg: | MSG="Welcome to Kubernetes!"
And apply this modified definition:
$ kubectl apply -f configmap.yml configmap/appconfig configured
You may have to wait for up to one minute until the changes are reflected in the mounted volume. You can check that we can receive the updated message by logging to the Pod and sending the HTTP request:
$ kubectl exec -it app -- bash root@app:/app# curl localhost Welcome to Kubernetes!root@app:/app#
Of course in a real environment, you should create a Service interfacing with this Pod and send the HTTP request to it. We needed to keep the example as concise as possible.
In the preceding lab, we demonstrated how we can use a ConfigMap to load “hot” configuration from a mounted volume. The advantages of this approach can be summarized as:
However, mounting ConfigMaps as volumes is not a silver bullet to all your configuration needs. There are times when an environment variable is more suitable for your scenario. For example, setting the log verbosity level (debug, info, notice, warn, etc.) in a mounted file is not the best practice. Fortunately, ConfigMaps allow you to mix environment variables with mounted volumes. This is demonstrated in the following lab.
In this lab, we show you how we can use the same ConfigMap for exposing configuration data to the containers both as mounted-volume files and environment variables. Let’s create a ConfigMap definition that mounts a hypothetical application configuration file called application.ini and also an environment variable for the log verbosity level called LOG_LEVEL:
kind: ConfigMap apiVersion: v1 metadata: name: appconfig data: LOG_LEVEL: info application.ini: | foo=bar var=abc var2=xyz
The Pod that makes use of this ConfigMap may look like this:
kind: Pod apiVersion: v1 metadata: name: app spec: containers: - name: app image: bash command: ["bash","-c","sleep 10000"] env: - name: LOG_LEVEL valueFrom: configMapKeyRef: name: appconfig key: LOG_LEVEL volumeMounts: - name: config-vol mountPath: /config volumes: - name: config-vol configMap: name: appconfig
Let’s have a quick look at the key points in this definition before we apply it:
The rest of the definition mounts the /config directory on the container. This directory contains all the data specified in the ConfigMap, including the items that we used as environment variables. Let’s apply our definitions and test the results:
$ kubectl apply -f configmap.yml configmap/appconfig created $ kubectl apply -f pod.yml pod/app created $ kubectl exec -it app -- bash bash-5.0# cat /config/application.ini foo=bar var=abc var2=xyz bash-5.0# echo $LOG_LEVEL info bash-5.0#
As you can see, we had the environment variable available as well as the configuration file. We didn’t include it in the demonstration, but if you list the contents of /config you’d find a file named LOG_LEVEL with “info” as its only content.
So far we used ConfigMaps for exposing external configuration data to containers. However, if this data is critical, it shouldn’t be injected through ConfigMaps for security reasons. Instead, you should use Secrets. Let’s have a small lab where we inject an imaginary API authentication file to our Pod through a Secret.
Assuming that we have our Google API authentication token: AIzaSyCNwsOvVIIzmJQ0fgp6s8zJJeWAdmrDF7M, we need to add it to a Secret so that we can use it securely in our container.
Secrets require that your data gets base64 encoded first:
$ echo -n AIzaSyCNwsOvVIIzmJQ0fgp6s8zJJeWAdmrDF7M | base64 QUl6YVN5Q053c092VklJem1KUTBmZ3A2czh6SkplV0FkbXJERjdN
Now, let’s create a Secret definition for this key:
apiVersion: v1 kind: Secret metadata: name: apiauth type: Opaque data: auth: QUl6YVN5Q053c092VklJem1KUTBmZ3A2czh6SkplV0FkbXJERjdN
Notice that we could equally create the Secret using the command line as such:
kubectl create secret generic apiauth --from-file=./auth
Where auth is a text file containing the base64-encoded string.
Our Pod definition should look as follows:
kind: Pod apiVersion: v1 metadata: name: app spec: containers: - name: app image: bash command: ["bash","-c","sleep 10000"] volumeMounts: - name: apiauth-vol mountPath: /config readOnly: true volumes: - name: apiauth-vol secret: secretName: apiauth
As you can see, the definition is very similar to the one that used a ConfigMap in our previous examples with some minor differences. For completeness, let’s login to our container and ensure that the API key is where it should be:
$ kubectl exec -it app -- bash bash-5.0# cat /config/auth && echo AIzaSyCNwsOvVIIzmJQ0fgp6s8zJJeWAdmrDF7M
Base64 is a method that is commonly used to encode binary data into text format. This allows data to be easily transferred through different network mediums. Base64-encoded data can be easily decoded using the base64 command. There are also many online tools that offer base64 encoding/decoding. Hence, nothing prevents a hacker from decoding your base64-encoded password if she gained access to it. So, does that mean that Secrets are no different than ConfigMaps security-wise? Well, not exactly. Kubernetes implements Secrets in a special way that makes them more secure:
Hence, it’s safe to say that your Secrets are secured when stored and handled by Kubernetes. However, it is your responsibility to handle their security when used by the application. One possible way is using your own encryption mechanism when storing and retrieving data from your Secrets. That is, encrypting the data before base64-encoding it then unencrypting it through the application.
Needless to say, you should never commit your Secret definition to version control.