1 minute read

Many of our new apps at Braintree use environment variables for configuration. This is in line with Twelve-Factor principles and makes it easier to run the same artifact in multiple environments. In our case, the artifact is a Docker image. Docker natively supports env files, so we can write our configuration to a file and then use docker run --env-file .env ....

Locally in development, we use a .env file for our configuration and generally use docker-compose, which also supports the feature:

# docker-compose.yml

myapp:
  image: myapp
  ports:
    - "8080:8080" 
  env_file: .env

However, our build tool, Bazel does not support this feature. Instead, you can pass individual environment variables on the command line:

bazel test --test_env EMAIL=a@example.com --test_env MODE=dev //...

I opened a GitHub issue about this feature (https://github.com/bazelbuild/bazel/issues/955), but in the meantime we can script our way out of it.

Usage

We decided to wrap our our common bazel invocations into a script that knew how to handle our .env file.

Here’s what its usage looks like:

./bazel build
./bazel build //myapp

./bazel test
./bazel test //myapp:test

./bazel run //myapp

The build and test commands allow targets, or if not passed, assume you want everything (//...). Bazel doesn’t have a way to pass in environment variable for a run command either, so in this case we can source the existing .env file before running the target.

Our script

And here is what the script looks like (slightly trimmed down for clarity):

#!/usr/bin/env ruby

ENV_FILE = ".env"

def main
  usage unless ARGV.size > 0

  case ARGV.first
  when "build"
    build(ARGV[1..-1])
  when "test"
    test(ARGV[1..-1])
  when "run"
    run(ARGV[1..-1])
  else
    usage
  end
end

def usage
  puts "Usage: ./bazel (build|test|run) <args>"
  exit 1
end

def build(targets)
  puts_and_exec "bazel build #{build_targets(targets)}"
end

def test(targets)
  test_envs = env_variables.map { |e| "--test_env #{e}"}.join(" ")
  puts_and_exec "bazel test #{test_envs} #{build_targets(targets)}"
end

def run(args)
  puts_and_exec "bash -c 'set -a; source #{ENV_FILE}; bazel run #{args.join(" ")}'"
end

def env_variables
  File.readlines(ENV_FILE).map do |line|
    key, value = line.strip.split("=", 2)
    "#{key}=#{ENV[key] || value}"
  end
end

def build_targets(targets)
  targets.empty? ? "//..." : targets.join(" ")
end

def puts_and_exec(command)
  puts command
  exec command
end

main if __FILE__ == $0

The "#{key}=#{ENV[key] || value}" piece allows the environment to override the values in the .env file. This lets use do things like:

FOO=bar ./bazel run ...

Updated: