Environment Variables

Configuration and Pipes.

Configuration and Pipes.

Introduction

Let’s say that you have microservices that you don’t want to start serving content unless certain environment variables have been defined.

Recently, we had to implement such a solution for FizzBuzz Pro

In our case, we needed to make sure that the fizz-crypto microservice did not start unless the following variables were defined in the system.

FIZZ_PORT_SVC_CRYPTO
FIZZ_JWT_KEY
FIZZ_RANDOM_BYTE_LENGTH
FIZZ_BCRYPT_HASH_ROUNDS
FIZZ_AES_PASSPHRASE

How do we make sure that the service does not start if any of these environment variables are missing? And how can we do this with the minimal amount of maintenance overhead?

Introducing the fizz-env Module

To begin, let’s create a module for our purposes. We’ll publish our module to github.com/zerotohero-dev/fizz-env private git repository:

# Switch to our workspace:
cd $WORKSPACE

# `fizz-env` is an empty repository:
git clone git@github.com:zerotohero-dev/fizz-env.git

# Switch to the project folder:
cd fizz-env

# Initialize our module:
go mod init github.com/zerotohero-dev/fizz-env

This will result in the following go.mod file that defines our module:

module github.com/zerotohero-dev/fizz-env

go 1.16

Then let’s create a ./pkg/env/fizz.go file that we’ll work on:

cd $WORKSPACE/fizz-env
mkdir -p pkg/env
touch pkg/env/fizz.go

Then let’s import the packages that we’ll need:

// $WORKSPACE/fizz-env/pkg/env/fizz.go

import (
    "fmt"
    "os"
    "reflect"
)

The FizzEnv Struct

To reduce potential errors and typos, it’s best to define the environment variables that we need using a struct that holds these variables as fields.

We’ll name our struct FizzEnv:

// $WORKSPACE/fizz-env/pkg/env/fizz.go

package env

type envCrypto struct {
    PortSvcCrypto    string
    JwtKey           string
    RandomByteLength string
    BcryptHashRounds string
    AesPassphrase    string
}

type FizzEnv struct {
    Crypto envCrypto
}

So, for example, if env is a FizzEnv, then env.Crypto.JwtKey will hold the value of $FIZZ_JWT_KEY environment variable.

The Factory Function

Let’s also create a factory function to create an instance of FizzEnv and populate the related field values from the system’s environment:

// $WORKSPACE/fizz-env/pkg/env/fizz.go

func New() *FizzEnv {
    res := &FizzEnv{
        Crypto: envCrypto{
            PortSvcCrypto:    os.Getenv("FIZZ_PORT_SVC_CRYPTO"),
            JwtKey:           os.Getenv("FIZZ_JWT_KEY"),
            RandomByteLength: os.Getenv("FIZZ_RANDOM_BYTE_LENGTH"),
            BcryptHashRounds: os.Getenv("FIZZ_BCRYPT_HASH_ROUNDS"),
            AesPassphrase:    os.Getenv("FIZZ_AES_PASSPHRASE"),
            Environment:      os.Getenv("FIZZ_ENV"),
        },
    }

    return res
}

Sanitizing the Environment Variables

Finally, let’s create a receiver function that traverses and makes sure that all of the environment variables that we need have been set:

// $WORKSPACE/fizz-env/pkg/env/fizz.go

func (e FizzEnv) SanitizeCrypto()  {
    v := reflect.ValueOf(e.Crypto)

    for i := 0; i < v.NumField(); i++ {
        val, name := v.Field(i).String(), v.Type().Field(i).Name

        if val == "" {
            panic(
                fmt.Sprintf(
                    "The environment variable that maps to '%s' " +
                    "is not defined.", name,
                ),
            )
        }
    }
}

Since there could be any number of fields in our struct, we had to use reflection to iterate across the struct fields to keep our code maintainable by following the open-closed principle.

And that’s pretty much it.

Whenever any of the required environment variables are not defined, calling env.SanitizeCrypto() on a FizzEnv instance env will cause the application to crash real loud real fast, which you would prefer rather than the app running silently despite the necessary environment information that it needs being missing.

Aside

Failing early, and failing loudly is also known as the fail-fast principle in software engineering. The fail-fast principle means that you should stop the current operation as soon as any unexpected error occurs.

Surprisingly, this approach results in a more stable solution in the long run. Why so? Because when you follow this principle, you tighten the feedback loop: Instead of suppressing errors and sweeping things under the rug, you quickly reveal the defects and fix the failures as early as possible.

In the end, you’ll benefit from this approach greatly.

Our Solution in Action

Let’s use this in a microservice to see our solution in action:

# Switch to our workspace:
cd $WORKSPACE

# `fizz-crypto` is an empty repository:
git clone git@github.com:zerotohero-dev/fizz-crypto.git

# We'll create our files in here:
cd fizz-crypto

# Initialize our module:
go mod init github.com/zerotohero-dev/fizz-crypto

This will result in the following go.mod file that defines our module:

module github.com/zerotohero-dev/fizz-env

go 1.16

Now, let’s go get our dependency:

go get github.com/zerotohero-dev/fizz-env

And we can use our new fizz-env/pkg/env as follows:

// $WORKSPACE/fizz-crypto/main.go 

package main

import (
    "fmt"
    "github.com/zerotohero-dev/fizz-env/pkg/env"
)

func main() {
    // Populate an `env.FizzEnv` collection buy parsing the system
    // environment variables:
    e := env.New()

    // Make sure that all of the environment variables that 
    //`fizz-crypto` needs have been defined; panic otherwise.
    e.SanitizeCrypto()

    // Print the value of an environment variable.
    fmt.Println("key", e.Crypto.JwtKey)
}

Since we haven’t defined any environment variables yet, running the above code on my system results in the following panic as expected:

panic: The environment variable that corresponds to 'PortSvcCrypto' 
is not defined.

goroutine 1 [running]:
github.com/zerotohero-dev/fizz-env/pkg/env.FizzEnv.
SanitizeCrypto(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0)
        /Users/volkan/go/pkg/mod/github.com/zerotohero-dev/
fizz-env@v0.1.0/pkg/env/fizz.go:40 +0x30f
main.main()
        /Users/volkan/Desktop/PROJECTS/fizz-crypto/main.go:22 +0x78

Conclusion

In this article, we’ve seen an approach to ensure that all of the environment variables that a service needs have been defined before the service starts.

We will use this fizz-env module in all of the FizzBuzz Pro microservices that we’ll be creating.

I’ll also share any improvement to this fizz-env module that’s worth mentioning as we develop it.

Read the Source

Below, you’ll find the zip archives of the projects and other related artifacts that we’ve covered in this article.

Enjoy… And may the source be with you 🦄.


Section Contents