HOWTO set up your programming environment for great fun and glorious profit

I made some notes about how to start an open-source infrastructure-as-code project. Here is the first bit of that in blog form. This post deals with step-0 decisions on how to set up a new programming project, managing the programming language tools, their associated version sprawl and environment variables across different projects or directory trees.

tl;dr -- Introducing the problem for which direnv and asdf-vm is the fix.

What's going on?


Every chicken has an egg problem and every egg has a chicken problem. -- Somebody

The C in IAC stands for Code. It is software and it needs to run or execute. With this interpretation of things something like terraform is an interpreter and it executes *.tf source code files. These files are structured internally in hcl format, which is a type of json. terraform is therefore a programming language that executes json code/data against your cloud fabric.

Every software project targets a specific runtime explicitly and sometimes implicitly. This runtime contains the implementation of the programming language your code is based on, and also the accumulated changes and incompatibilities between released versions. As with any software, updates invalidate assumptions about the program behaviour that nobody knew they held.


Running the code against the wrong version of the tool may result in unknown-unknowable and unexpected problems and error states, so maintaining the execution runtime is important for engineer/operator happiness. Future-you will be happy not to deal with both unexpected upgrade maintenance and regular urgent maintenance at the same time.

This problem exists in programming projects in general. Your specific runtime might be the version of build-essentials that you use, or something else like babashka, terragrunt, ruby, python, ansible, clojure, java etc &c. The point is that as an operator you need to run the software in the git repository against the version(s) of the software it was developed with.

A compounding factor is that you might be working on multiple different codebases at the same time and it's possible and sometimes likely that every repository has its own idea of what version of each tool is required.

We want to stay in the pit of success.

One way to solve this meta-problem is to rely on a tool that will install other tools. This is what a package manager does. Your operating system (ubuntu or arch or something) has a package manager, but usually package managers want to provide you with the version of every package it manages. What you as a software engineer require is a package manager that, 1) doesn't piss off your main package manager and, 2) still allows you to have multiple versions of the multiple tools within easy reach in our work environment. The default system-provided package manager is not good enough, we need something else.

To expand on this a little; Package managers like brew and apt do not understand the concept of having multiple versions of one tool around at the same time. (python and python2 and python3 and python-is-python-2 is dumb...). Yet as a developer you might still have to target both AWS lambda and GCP Cloud functions in one workday. Each of these require a different version of python.

It is to solve this problem that tools like rbenv for ruby and pyenv for python exist. Those tools allow you to have multiple versions of the same tool installed (without requiring sudo...) and make different ones of those available depending on which directory your bash session or process currently is in.

But what about bash or nodejs? What about <insert favourite programming language here>? It is unlikely that every programming community has this custom made tool to solve this problem for its programming environment runtime.

Luckily we can solve the problem by having another standard tool... with plugins for every versioned programming environment you might require.

asdf, that one tool to rule all other tools and their versions


Let's set ourselves up for success.


If you have `asdf` installed, you can use it to install specific versions of programming languages into your programming environment and manage this with a special file called .tool-versions. When your shell, bash or zsh, encounters this file it activates the version of each tool specified in the file.

Example:

$ cat .tool-versions  
terraform 1.2.3
terragrunt 0.38.2
babashka 0.8.157

$ asdf install
babashka 0.8.157 is already installed
terraform 1.2.3 is already installed
terragrunt 0.38.2 is already installed
...

$ terragrunt --version \
&& terraform --version \
&& bb version
terragrunt version v0.38.2
Terraform v1.2.3
on linux_amd64

Your version of Terraform is out of date! The latest version
is 1.2.5. You can update by downloading from https://www.terraform.io/downloads.html
babashka v0.8.157

Oh nice! Look at that friendly warning about another new version of something you need to depend on... /s

So I open nano and change .tool-versions

cat .tool-versions
terraform 1.2.5
terragrunt 0.38.2
babashka 0.8.157

$ asdf install
babashka 0.8.157 is already installed
terraform --version
Terraform v1.2.5
on linux_amd64

Downloading terraform version 1.2.5 from https://releases.hashicorp.com/terraform/1.2.5/terraform_1.2.5_linux_amd64.zip
Verifying signatures and checksums
gpg: keybox '/tmp/asdf_terraform_GLCZwo/pubring.kbx' created
gpg: /tmp/asdf_terraform_GLCZwo/trustdb.gpg: trustdb created
gpg: key 34365D9472D7468F: public key "HashiCorp Security (hashicorp.com/security) <security@hashicorp.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg: Signature made Wed 13 Jul 2022 12:23:52 SAST
gpg:                using RSA key 374EC75B485913604A831CC7C820C6D5CD27AB87
gpg: Good signature from "HashiCorp Security (hashicorp.com/security) <security@hashicorp.com>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: C874 011F 0AB4 0511 0D02  1055 3436 5D94 72D7 468F
    Subkey fingerprint: 374E C75B 4859 1360 4A83  1CC7 C820 C6D5 CD27 AB87
terraform_1.2.5_linux_amd64.zip: OK
Cleaning terraform previous binaries
Creating terraform bin directory
Extracting terraform archive
terragrunt 0.38.2 is already installed

$ terraform --version
Terraform v1.2.5
on linux_amd64

Problem solved.

(OK - I admit, the gpg warning seems like a problem that requires solving too, but that problem exists on a different plane of problems than the one I'm writing so much about.)

direnv and .envrc


Once more.

You actually have this problem in general. There are tools in the world that engineers and operators use that depend on specific environment variables being set, and being set to some specific values on a per-project basis. Sometimes you want to have the same variable with different values depending on the directory even within the same project. terragrunt is like this.

One example is having the value of AWS_PROFILE=company-prod-account-profile-name in one specific directory tree (eg for .../terragrunt/prod/cdn/terragrunt.hcl) in your terragrunt project. Then in another directory tree in the same project (eg .../terragrunt/dev/cdn/terragrunt.hcl) you can set AWS_PROFILE=company-dev-account-profile-name.

This allows you to deploy to dev environment or prod environment in the same bash session/software-ide-project, WITHOUT needing to maintain secrets in the open. IE You can use your aws configure and set up $HOME/.aws/config and $HOME/.aws/credentials to contain credentials for as many accounts or clients or projects as you require and use specific credentials only in certain directory trees and their subtrees.

$ nano .envrc
...
direnv: error /path/to/iaac/prod/.envrc is blocked. Run `direnv allow` to approve its content                                                                               
$ cat .envrc
export AWS_PROFILE=company-prod-account

$ direnv allow
direnv: loading ~/path/to/iaac/prod/.envrc
direnv: export +AWS_PROFILE

$ env | grep -i aws
AWS_PROFILE=company-prod-account

$ cd `mktemp -d` # other directory somewhere
direnv: unloading

$ echo 'export AWS_PROFILE=company-dev-account' > .envrc
direnv: error /tmp/tmp.ZFPF6NsWAD/.envrc is blocked. Run `direnv allow` to approve its content

$ direnv allow
direnv: loading /tmp/tmp.ZFPF6NsWAD/.envrc
direnv: export +AWS_PROFILE

$ env | grep -i aws
AWS_PROFILE=company-dev-account

Problem solved.

Use the same for any other environment variable that may configure any piece of kit that follows the 12-factor app design.

Other variables I like to set like this: AWS_ACCOUNT_ID, VAULT_TOKEN, VAULT_HTTP_ADDR, SMTP_FROM_ADDRESS &c.

## emacs

In your $HOME/.emacs.d/init.el, using quelpa and use-package together:

(use-package quelpa
  :config
  (setq quelpa-upgrade-interval 7)
  (add-hook #'after-init-hook #'quelpa-upgrade-all-maybe))

(require 'quelpa-use-package)

(use-package direnv
  :config (direnv-mode))

(use-package asdf-vm
  :quelpa (asdf-vm :fetcher github :repo "delonnewman/asdf-vm.el"))
(require 'asdf-vm)
(asdf-vm-init)


vim: direnv & asdf.
vscode:direnv. (I could not find a vscode plugin for asdf-vm)

Conclusion


Now that we know how to pin tool versions and environment variables to specific directories we can actually start to set up a infrastructure as code project. More about this next time.