Build a Modern API with Slim 4 - Verify your .env file

gavinsykes

Gavin Sykes

Posted on November 7, 2023

Build a Modern API with Slim 4 - Verify your .env file

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
gavinsykes
Gavin Sykes

Posted on November 7, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related