Environment variables and their usage in Kubernetes

Maxime Moreillon
4 min readNov 21, 2021

An application will almost always involve configuration specific to the environment it is being executed in. For instance, if the application connects to a database, the URL, username and password to access it must known by the former. Those three pieces of information would obviously not be the same for a different user deploying the application to a different environment.

This issue is generally solved by using environment variables. As the name suggests, environment variables are variables that are specific to the environment they are set in. This article presents how to use environment variables to configure an application, using a Node.js snippet as example.

Using environment variables in a Node.js app

Here is the beginning of a Node.js application that connects to a Neo4J database:

const neo4j = require('neo4j-driver')const driver = neo4j.driver(
http://192.168.1.2:7687,
neo4j.auth.basic(
'myUsername',
'myPassword'
)
)

Having credentials hardcoded in the source code of the application is terrible practice: Not only is it a huge security flaw, it also constraints the application to work only in its current environment, thus making it impossible to deploy somewhere else.

Ideally, credentials should be turned into variables, declared outside the application’s source code. For this purpose, one can use environment variables, which can be accessed in Node.js using the process.env object. As such, the previous snippet can be edited as follows

const neo4j = require('neo4j-driver')const driver = neo4j.driver(
process.env.NEO4J_URL,
neo4j.auth.basic(
process.env.NEO4J_USERNAME,
process.env.NEO4J_PASSWORD
)
)

Here, the variables can set by using the export command

export NEO4J_URL=bolt://192.168.1.2:7687
export NEO4J_USERNAME=myUsername
export NEO4J_PASSWORD=myPassword

Alternatively, one can use the dotenv package so as to enable the definition of environment variables in a dedicated .env file.

Deployment to Kubernetes

As such, the application can now read environment variables to set the database access parameters at runtime. Thus, if deployed in Kubernetes, the manifest file for its deployment would look like so:

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-node-app
spec:
replicas: 1
selector:
matchLabels:
app: my-node-app
template:
metadata:
labels:
app: my-node-app
spec:
containers:
- name: my-node-app
image: my-container-registry/my-node-app
imagePullPolicy: Always
ports:
- containerPort: 80
env:
- name: NEO4J_URL
value: bolt://192.168.1.2:7687
- name: NEO4J_USERNAME
value: myUsername
- name: NEO4J_PASSWORD
value: myPassword

Using this approach, the same application can be deployed into various Kubernetes clusters, having a specific configuration for each.

However, as it is, this method suffers from two flaws: Firstly, If the same variables are to be used by multiple applications in the same environment, then those would need to be written in each manifest file, making maintenance tedious. Secondly, deployment definitions are visible to users with access to the Kubernetes cluster via, for instance, kubectl. Thus, if the manifest contains sensitive information such as passwords, then this information becomes visible to other users of the cluster.

For instance, the variables become visible when running the kubectl describe deployment command, yielding an output such as:

Name:                   my-node-app
Namespace: default
CreationTimestamp: Fri, 15 Oct 2021 06:33:58 +0000
Labels: <none>
Annotations: deployment.kubernetes.io/revision: 10
Selector: app=omy-node-app
Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=my-node-app
Annotations: kubectl.kubernetes.io/restartedAt: 2021-10-26T01:24:18Z
Containers:
outfit-manager-front:
Image: my-container-registry/my-node-app
Port: 80/TCP
Host Port: 0/TCP
Environment:
NEO4J_URL: bolt://192.168.1.2:7687
NEO4J_USERNAME: myUsername
NEO4J_PASSWORD: myPassword
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: my-node-app-65f5fc66f4 (1/1 replicas created)
Events: <none>

To solve both of those problems, one can instead define environment variables in a Kubernetes secret.

Environment variables as Kubernetes secret

A Kubernetes secret can be easily created using a Kubernetes manifest. Here is an example for our application, creating a secret named environment-variables,

apiVersion: v1
kind: Secret
metadata:
name: environment-variables
type: Opaque
stringData:
NEO4J_URL: bolt://neo4j
NEO4J_USERNAME: myUsername
NEO4J_PASSWORD: myPassword

The manifest can be deployed just like any other using the kubectl apply command. Checking that the variables are indeed hidden can be achieved using kubectl describe secret environment-variables:

Name:         environment-variables
Namespace: default
Labels: <none>
Annotations:
Type: Opaque
Data
====
NEO4J_URL: 38 bytes
NEO4J_USERNAME: 9 bytes
NEO4J_PASSWORD: 9 bytes

With that done, the deployments can be updated so as to use the secret as a source of environment variables, usign the envFrom parameter:

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-node-app
spec:
replicas: 1
selector:
matchLabels:
app: my-node-app
template:
metadata:
labels:
app: my-node-app
spec:
containers:
- name: my-node-app
image: my-container-registry/my-node-app
imagePullPolicy: Always
ports:
- containerPort: 80
envFrom:
- secretRef:
name: environment-variables

Conclusion

Environment variables are the industry standard way to configure applications for their specific environment. For that matter, Kubernetes provides convenient options that can be leveraged to greatly facilitate deployments.

--

--