Gavin Sykes
Posted on November 7, 2023
Welcome back to this series, it was put on hold some time ago, but I'm now back and ready to go!
In the second part of this series we set up our .env
file which contained both environment-specific and secret variables, both of which are reasons to have Git ignore the file.
So, how do I know, then, that when I deploy onto my staging or production servers, that the file contains everything I need? During this project there have been a few times I've made some updates, added an environment variable, then realised I didn't make that same change on the server. This is where a verification script comes in.
Keep in mind that this file is to be committed, because, whilst it contains the names of all our environment variables, it does not contain their values.
I have also opted for something a little bit different and done this in Bash, this allows complete agnosticism as regards programming languages, so you could be writing your API in PHP, Go, C#, JavaScript, it doesn't matter. As long as your server has Bash, this will run (and considering that both Linux and macOS have Bash baked-in, and I'm fairly sure but not certain that you can run Bash scripts in Windows Powershell, you're more likely to have it than not).
So, how do we get started?
The first and most important line is the hashbang or shebang, also known as
#!/bin/bash
This is what makes the script executable just by running ./check_env.sh
and telling the system that it needs to use the Bash interpreter to run it.
And now we tell it which environment variables should appear in our file:
#!/bin/bash
required_env_vars=("_ENVIRONMENT" "_PDO_HOST" "_PDO_USERNAME" "_PDO_PASSWORD" "_PDO_NAME" "_ENCRYPTION_CIPHER_METHOD" "_HASHING_COST" "_SMTP_HOST" "_SMTP_USERNAME" "_SMTP_PASSWORD" "_RATE_LIMIT_GET" "_RATE_LIMIT_POST" "_RATE_LIMIT_PUT" "_RATE_LIMIT_DELETE")
And now let's tell it where to find our .env
file, this will be in the same directory as our script file so simply
#!/bin/bash
required_env_vars=("_ENVIRONMENT" "_PDO_HOST" "_PDO_USERNAME" "_PDO_PASSWORD" "_PDO_NAME" "_ENCRYPTION_CIPHER_METHOD" "_HASHING_COST" "_SMTP_HOST" "_SMTP_USERNAME" "_SMTP_PASSWORD" "_RATE_LIMIT_GET" "_RATE_LIMIT_POST" "_RATE_LIMIT_PUT" "_RATE_LIMIT_DELETE")
env_file=".env"
However, what if our .env
file is somewhere different, like in /shared
? Luckily we can check for that as well:
if [ $# -eq 1 ]; then
env_file="$1"
fi
So, um, what does that do?
This looks for the number of command line arguments($#
), and if it equals (-eq
) 1, set the env_file
variable to the first argument ("$1"
), rather than .env
. So now if we run ./check_env.sh
it will look for our .env
file locally, whereas if we run ./check_env.sh /shared/bookstore_api/.env
then it will look for it in /shared/bookstore_api/.env
instead.
fi
being if
backwards, just ends the if statement.
Now we need to make sure our .env
file actually exists.
if [ ! -f "$env_file" ]; then
echo "Error: .env file not found."
exit 1
fi
The if statement here is now looking for the file, signified by the -f
flag before "$env_file"
, with the preceding !
meaning negation i.e. if the file is not found, do this. It will then echo the error message then completely exit the program. But what does that 1 mean?
This is a concept that carries on strong in low-level programming from the very earliest days of computing. Every program ever written anywhere, ever, has to exit or end eventually, and exit codes are how we indicate whether or not it was successful.
A 0 means success - if it helps, consider it to mean "nothing to report", and anything other than 0 means an error. Some programs will use different numbers for different errors, but for the purpose of this short script, exit 1
on an error will do just fine.
#!/bin/bash
# What variables are we looking for?
required_env_vars=("_ENVIRONMENT" "_PDO_HOST" "_PDO_USERNAME" "_PDO_PASSWORD" "_PDO_NAME" "_ENCRYPTION_CIPHER_METHOD" "_HASHING_COST" "_SMTP_HOST" "_SMTP_USERNAME" "_SMTP_PASSWORD" "_RATE_LIMIT_GET" "_RATE_LIMIT_POST" "_RATE_LIMIT_PUT" "_RATE_LIMIT_DELETE")
# Where is our .env file?
env_file=".env"
# Have we been told it's somewhere else?
if [ $# -eq 1 ]; then
env_file="$1"
fi
# Is it where it should be?
if [ ! -f "$env_file" ]; then
echo "Error: .env file not found."
exit 1
fi
Okay so now let's start looking for our variables:
for var in "${required_env_vars[@]}"; do
grep -q "^$var=" "$env_file"
if [ $? -ne 0 ]; then
echo "Error: $var is missing in the $env_file file."
exit 1
fi
done
Okay, so...what? What's happening here?
Okay so let's break it down:
for var in "${required_env_vars[@]}"; do
# Do some stuff
done
So this should be a fairly self-explanatory for loop: for each of the required variables defined above, do some stuff. That [@]
is unique to Bash and means that it treats everything in the array as an item, rather than treating the whole array as an item.
grep -q "^$var=" "$env_file"
The first line inside the loop, this uses grep
to search for a line within our .env
file that matches ^$var=
which means that it contains our variable name, followed immediately by =
, and the ^
signifies that the line must start with that. Finally, the -q
means quiet, and tells grep
not to output anything, just to store its exit code, which will be 0 i.e. success if it finds the line.
if [ $? -ne 0 ]; then
echo "Error: $var is missing in the $env_file file."
exit 1
fi
That exit code is stored in $?
and -ne
means not equal, so if grep
's exit code is not 0, then echo the error message and, again, exit 1
.
Now, finally, if that loop runs fine and we reach the end of it, we can echo a success message and exit 0
echo "All environment variables present and correct in $env_file."
exit 0
Is there anything else we can add?
Well, yes, I am not by any means an expert on Bash scripting, even just writing this article I can see areas for improvement and I'm sure you can as well! There is something that we can add though and that I will demonstrate below, verification of the values.
Obviously, be careful here, you don't want to commit a file that says something like "the database password should match SuperSecretPassword123", because that will negate the entire point of keeping the password in a secret environment file!
What I will demonstrate though is limiting the options for _ENVIRONMENT
, which can serve as protection against things like typos.
ENVIRONMENT=$(grep "^_ENVIRONMENT=" "$env_file" | cut -d'=' -f2)
allowed_environments=("development" "staging" "demo" "production")
if ! [[ " ${allowed_environments[@]} " =~ " $ENVIRONMENT " ]]; then
echo "Error: _ENVIRONMENT must be one of ${allowed_environments[@]}"
exit 1
fi
So, what's happening here?
We are fetching the provided _ENVIRONMENT
value by running grep "^_ENVIRONMENT=" "$env_file"
and piping the resulting line into cut -d'=' -f2
meaning "cut this line using =
as the delimiter, and return the second part". Then setting the allowed environments as we did the required variables at the start.
Now, we need to make sure that ENVIRONMENT
is actually one of our allowed_environments
, we take care of this in the if statement, then once again, if needed, we echo our error message and exit 1
.
All of this will leave us with a script that looks like the below:
#!/bin/bash
required_env_vars=("_ENVIRONMENT" "_PDO_HOST" "_PDO_USERNAME" "_PDO_PASSWORD" "_PDO_NAME" "_ENCRYPTION_CIPHER_METHOD" "_HASHING_COST" "_SMTP_HOST" "_SMTP_USERNAME" "_SMTP_PASSWORD" "_RATE_LIMIT_GET" "_RATE_LIMIT_POST" "_RATE_LIMIT_PUT" "_RATE_LIMIT_DELETE")
env_file=".env"
if [ $# -eq 1 ]; then
env_file="$1"
fi
if [ ! -f "$env_file" ]; then
echo "Error: .env file not found."
exit 1
fi
for var in "${required_env_vars[@]}"; do
grep -q "^$var=" "$env_file"
if [ $? -ne 0 ]; then
echo "Error: $var is missing in the $env_file file."
exit 1
fi
done
ENVIRONMENT=$(grep "^_ENVIRONMENT=" "$env_file" | cut -d'=' -f2)
allowed_environments=("development" "staging" "demo" "production")
if ! [[ " ${allowed_environments[@]} " =~ " $ENVIRONMENT " ]]; then
echo "Error: _ENVIRONMENT must be one of ${allowed_environments[@]}"
exit 1
fi
echo "All environment variables present and correct in $env_file."
exit 0
Posted on November 7, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.