Bazel and .env Files
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 ...