When working with cloud infrastructures, it is crucial to use proper software and ensure that only specific fixed versions of tools are used in all environments (development, ci, production, QA, etc.). This guarantees that the infrastructure blocks built by one person can be maintained in the future by another person without risk of incompatibilities between the tools these people use.

Example 1: Terraform needs a fixed version

Terraform was famous for breaking back compatibility even between minor releases before reaching a stable 1.x.x release. It is a good practice to lock the specific required version of Terraform to avoid this problem:

terraform {
  required_version = "1.0.11"

Example 2: Kubernetes needs a specific range of versions

Kubernetes follows its Version Skew Policy which means that the version of kubectl must not be more than one minor version older or newer than the version of the kube-apiserver. Public cloud platforms provide different versions of managed Kubernetes, and thus it is essential to use the appropriate version of kubectl when working with them.

Use asdf to fix versions of tools

After analyzing the available solutions, we chose asdf.
It allows keeping a list of required tools and their versions in the plain-text file .tool-versions.

Here is what .tool-versions may look like:

helm 3.7.1
kubectl 1.21.7
terraform 1.0.11
vault 1.9.0

Start using asdf

To start with asdf, one needs to register software repositories for required tools. These repositories are called plugins and must be added one by one once in the very beginning:

$ asdf plugin add helm
$ asdf plugin add kubectl
$ asdf plugin add terraform
$ asdf plugin add vault

Unfortunately, it is not possible to perform this step declaratively using a configuration file.

The following bash script can still automate this process:

$ cut -d' ' -f1 .tool-versions | sort 
  | comm -23 - <(asdf plugin-list | sort) 
  | join -a1 - <(asdf plugin list all) 
  | xargs -t -L1 asdf plugin add

Now, it is possible to perform the installation with a single command:

$ asdf install

Under the hood, asdf will download the desired versions of tools from their repositories to the /usr/local/opt/asdf/bin directory and create a system of symlinks and bash scripts to recall executables with a desired version on the fly.

Let’s make sure that the task was done correctly.

Terraform has a required version 1.0.11 inside the project directory:

$ cd project-a
$ terraform version | head -1
Terraform v1.0.11

After leaving the project directory, another version of Terraform is active:

# Go back to $HOME directory
$ cd 
$ terraform version | head -1
Terraform v1.1.0-beta1

Integration with IDE

Modern IDEs assist software developers in writing clean code with syntax checkers, code linters, code autocompletion, and so on. A new promising way of accomplishing these tasks is using the so-called Language Server Protocol. In this case, a centralized development server has all required software installed and can execute commands upon requests from IDE.
Terraform supports this feature with their Terraform Language Server in the Visual Studio.

Still, many people prefer to be independent when writing source code and want their IDEs to work locally. To achieve this, an IDE needs to know where to find executables of the tools. Since asdf downloads binaries to its own directory, one needs to adjust these settings in the IDE.

There is a command to fetch the path to an executable:

$ asdf which terraform

Use this path to set up your IDE:

Now your IDE will use the same version of Terraform as in the tool-versions configuration file.

Native Alternatives

The problem of fixing a specific version is not new, and asdf did not really make a breakthrough here. For example, tfenv works perfectly for Terraform in the same manner: it reads a .terraform-version file and calls the desired version of the executable on the fly. Other tools may also have something similar. We prefer asdf because it supports all tools we need at the moment, and therefore it is one of the few dependencies to start doing cloud development.

Why not Docker?

In theory, Docker can solve the issue similarly to how docker-compose helps front-end developers reproduce their development environments. One of the downsides of this approach is a need to support plenty of aliases or bash scripts that will rewrite the original command.

For example, the following command:

$ terraform plan -refresh-only

should be transformed to its docker-compose alternative manually:

$ docker-compose exec terraform -refresh-only

Worth mentioning that asdf does this job automatically under the hood.

Upgrading tools

The process is straightforward when upgrading the tools since source code is the only source of truth.
Let’s plan the upgrade of Terraform to version 1.1.0 as an example.

1. Change the required version of Terraform everywhere in the code

This step is essential to make the upgrade mandatory.

terraform {
  required_version = "1.1.0"

2. Update Terraform version in the configuration file

Open up the .tool-versions file and adjust the version of Terraform:

terraform 1.1.0

3. Update executable file

It’s time to install a new version of Terraform to continue working with the source code:

$ asdf install
Downloading terraform version 1.1.0 from https://releases.hashicorp.com/terraform/1.1.0/terraform_1.1.0_darwin_amd64.zip

# ... removed lines for brevity

terraform_1.1.0_darwin_amd64.zip: OK
Cleaning terraform previous binaries
Creating terraform bin directory
Extracting terraform archive

Let’s check the new version:

$ terraform version | head -1
Terraform v1.1.0

All good!

4. Persist changes in the version control system

Don’t forget to git commit and git push updated file so that all team members can repeat step 3.

What comes next?

asdf guarantees that cloud developers use the required individual versions of tools when working on different teams with various infrastructure projects. Besides using the specific versions of tools, team members must have the same configuration settings. In the following article, we will show how to share settings within a project when working on the team and to isolate configuration settings of the tools that are in use in different projects.