Direnv: For Secure Coding and Kind Documentation
What environment variables are
Introduction to /usr/bin/env
/usr/bin/env
(or just env
) echoes out all of the currently set environment variables.
Shebangs at the top of files in Unix-based OS’s determine how to execute instead of by file extension.
Might see #!/usr/bin/env python
or #!/usr/bin/env ruby
as a nicer way of not hardcoding the path to ruby
or python
binary.
Why not #!ruby
or #!python
? Environment variables such as GEM_HOME
.
Let’s take a look into what is set in your terminal environment: (Incoming!)
|
|
Accessing from the language
Ruby:
|
|
Python:
|
|
PowerShell:
|
|
Following good practices
3 basic ways, depending on how framework-y you want to get:
- Just-in-time environment variables
- Shell script setting environment variables
dotenv
/direnv
frameworks
Just-in-time
Setup: N/a
Invoke:
|
|
Shell script
Setup:
|
|
Invoke:
|
|
Frameworks
Depending on the framework, it auto-loads (a la source
) the file when entering the directory. It is hidden by default, hence filename starting with .
, so it stays out of your way most times. The file evaluated depends on the framework (.env
for dotenv
, .envrc
for direnv
), and evaluates all other files by that name up the directory tree. Might have seen this approach already with other tools (rspec, git, etc.)
Setup:
|
|
Invoke:
|
|
Documentation
Document it.
No. Seriously.
When
Always. As soon as it is introduced, even in a branch.
Make the code fail spectacularly (throw unhandled exception with explanation in message) if the environment varaible is not set.
Where
Possible locations:
.env.example
.envrc.example
- within
README.md
- within other file that is versioned with the project
How (Development Practices)
Documenting environment variables can be done in a README.md
(best), but the quickest notes for later reference can be as comments in a .env.example
file itself.
JIT environment variable setting
Document example usage in README.md
, or other docs
Benefits
- Consistent – Same approach as with
/etc/init.d/*
scripts - Dependencies – No additional tooling
- Power – Access to everything the shell has to offer
- Transitive – Switching projects? Variables do not persist to shell session
Tradeoffs
- Forgetting – Must remember to include variables on every invocation
- Human Error – Grabbing what variables are needed requires parsing the docs intended for humans (which is less fault-tolerant)
- Complicated – Must be a one-liner, or else it falls under the “sourced files” option by definition
Source files manually
Center on the .env
convention and include a file (committed to the repository) named .env.example
, containing sample values for all variables.
This allows people to optionally use a framework like dotenv
. Do not allow yourself to commit .env
to the repo, so add it to global gitignore for your workstation.
Benefits
- Simplicity – Includes example, machine-parseable values with the project source code
- Dependencies – No additional tooling
- Power – Access to everything the shell has to offer
Tradeoffs
- Forgetting – Must remember to
source ./.env
in every terminal running the application- Designed well: Blows up spectacularly at earliest possible phase
- Designed poorly: Partially executes, then bombs out
- Dirty Environment – Must open new or restart terminal when switching projects
Frameworks
I use direnv
because I like the set-and-forget approach. For this section, I’m going to focus on direnv
implementation.
- Create a
.envrc
file - Globally gitignore
.envrc
- Create a
.envrc.example
file - Store sanitized, dummy examples in
.envrc.example
Yes, I store credentials on local machine in plaintext. Shame me. If you can think of a way around this, I’m very open to improvement.
Direnv
Found at https://direnv.net/
Benefits
- Secure – Required to manually whitelist versions of
.envrc
every time it changes (direnv allow
) - Automatic – Sets variables when
cd
into directory - Self-Contained – No hooks to additional libraries inside of program (like with
dotenv
) - Inheiritance – PWD containing
.envrc
overwrites ones in parent dirs, on up the chain
Tradeoffs
- Execution – Execute from outside of project directory (
./project-dir/some_script.sh
will not automatically set environment variables in./project-dir/.envrc
) - Lag – Additional program running on every
cd
in the shell - Shell – Limits to one of several popular shells
- Variables – Only handles shell variables (e.g.,
export SOME_VAR='foo'
, notsome_var() { echo 'foo' }
)