tfs is a command-line tool that helps you manage multiple versions of Terraform efficiently.
It was inspired by this project.
tfs is simple, lightweight, and follows the XDG Base Directory Specification.
It works out of the box on all GNU/Linux distributions and macOS.
git clone https://github.com/yannlambret/tfs.git && cd tfs
go mod tidy
go build -o build/tfsAlternatively, you can download a prebuilt binary for your platform from the Releases page.
Then place the resulting binary somewhere in your PATH.
macOS users may have to run this kind of command to mark the binary as safe:
sudo xattr -d com.apple.quarantine ~/.local/bin/tfsHere are a few examples of how to use the tool (assuming the current directory contains some Terraform manifest files):
tfsNote: tfs now uses HashiCorp’s hc-install library to automatically download and install Terraform.
It is thus no longer possible to download Terraform from alternative sources.
If no version constraint is detected, tfs will activate the most recently downloaded Terraform version.
If no Terraform version constraint is specified in the configuration, you can manually select the version to use:
tfs 1.10.1When you run tfs without specifying a version, it inspects Terraform configuration files
in the current directory to infer which version to activate.
tfs understands standard comparison operators (=, !=, >, >=, <, <=) and also
supports the Terraform / HashiCorp pessimistic operator ~> (sometimes called the "compatible with" operator).
Supported forms and their expansion:
~> 1 => >=1.0.0, <2.0.0
~> 1.2 => >=1.2.0, <1.3.0
~> 1.2.3 => >=1.2.3, <1.3.0
Multiple constraints are ANDed (, separator) and any || segments (if present) act as OR, following the semantics of the underlying hashicorp/go-version library.
Examples:
# Accept any 1.2.x (but not 1.3.0):
required_version = "~> 1.2"
# Accept patch upgrades starting at 1.2.5 (but still < 1.3.0):
required_version = "~> 1.2, >= 1.2.5"
# Accept any 1.x:
required_version = "~> 1"
# Mixed with other operators:
required_version = ">= 1.5.0, ~> 1.6"
Invalid uses (e.g. ~> 1.alpha, ~> 1..2, ~> ~> 1.2) are rejected and will cause tfs to report an error instead of choosing a wrong version silently.
The ~> constraints are internally expanded before being passed to the version resolver; this ensures consistent behavior without pulling additional parsing libraries.
Tip: If no constraint is found,
tfssimply activates the most recently downloaded Terraform version.
tfs listtfs prunetfs prune-until 1.8.0By default, Terraform binaries are stored in ${XDG_CACHE_HOME}/tfs
If XDG_CACHE_HOME is not set, it defaults to ${HOME}/.cache/tfs.
A symbolic link to the active Terraform binary is created at ${HOME}/.local/bin/terraform,
so make sure this directory is added to your PATH.
tfs supports a configuration file to customize behavior.
By default, the configuration file is located at:
$XDG_CONFIG_HOME/tfs/config.yaml
If XDG_CONFIG_HOME is not set, it falls back to:
$HOME/.config/tfs/config.yaml
# -- Cache Management
# Custom path for the Terraform cache directory.
# Default: "${XDG_CACHE_HOME}/tfs"
# Fallback: "${HOME}/.cache/tfs"
#cache_directory: <CUSTOM_PATH>
# Enable automatic cache cleanup.
cache_auto_clean: true # default value
# Maximum number of releases to keep in the cache (fallback mode).
cache_history: 8 # default value
# Advanced cache management:
# Keep a limited number of releases per minor version,
# and a limited number of patch versions within each minor.
#
# For example, with the config below, you might keep:
# * 1.9.3
# * 1.9.5
# * 1.10.2
# * 1.10.3
# * 1.11.0
#
# When both values are defined, cache_history is ignored.
#cache_minor_version_nb: 3
#cache_patch_version_nb: 2tfs tries to preserve the current version in use — even if it would otherwise be cleaned up.
Consider this scenario (using the config above):
$ tfs prune # The cache is now empty
$ tfs 1.10.2
$ tfs 1.10.3Now the cache contains:
$ tfs list
1.10.2
1.10.3 (active)Let’s install one more version:
$ tfs 1.10.4Since you want to keep at most two patch versions, 1.10.2 is removed:
$ tfs list
1.10.3
1.10.4 (active)Now, suppose you need to work with 1.10.1:
$ tfs 1.10.1Even though 1.10.1 falls outside the patch retention window, tfs will not delete it immediately, because it’s now the active version.
$ tfs list
1.10.1 (active)
1.10.3
1.10.4But as soon as you switch again:
$ tfs 1.10.4
$ tfs list
1.10.3
1.10.4 (active)Now 1.10.1 is cleaned up, as expected.
tfs ships as a Docker image that can be used to automatically download and activate the right
Terraform version during a build, based on the required_version constraint in your configuration.
Mount your Terraform project as the working directory and pass Terraform subcommands directly:
docker run --rm \
-v $(pwd):/workspace \
-w /workspace \
tfs:1.4.3 sh -c "terraform init && terraform plan"tfs reads the required_version constraint from the current directory, downloads the matching
Terraform binary, and makes it available before executing the rest of the command.
By default, downloaded binaries are stored inside the container at /root/.cache/tfs and are
discarded when the container exits. If your CI platform supports volume caching, you can mount a
host directory or a named volume to avoid re-downloading Terraform on every build:
docker run --rm \
-v $(pwd):/workspace \
-w /workspace \
-v /ci/cache/tfs:/root/.cache/tfs \
tfs:1.4.3 sh -c "terraform init && terraform plan"Alternatively, the cache directory can be overridden via the TFS_CACHE_DIRECTORY environment
variable — useful when the default path does not match your caching setup:
docker run --rm \
-v $(pwd):/workspace \
-w /workspace \
-e TFS_CACHE_DIRECTORY=/cache \
-v /ci/cache/tfs:/cache \
tfs:1.4.3 sh -c "terraform init && terraform plan"Note on cache invalidation:
tfsnames binaries after their version (terraform_1.12.2), so a cached binary is reused as-is across builds. Cache entries are never stale.
Depending on your Terraform configuration, you will likely need to inject credentials into the container. Common examples:
# GCP — service account key (preferred over user credentials in CI)
-e GOOGLE_APPLICATION_CREDENTIALS=/run/secrets/sa-key.json \
-v /path/to/sa-key.json:/run/secrets/sa-key.json:ro
# AWS — standard credential environment variables
-e AWS_ACCESS_KEY_ID=... \
-e AWS_SECRET_ACCESS_KEY=... \
-e AWS_DEFAULT_REGION=...
# GitHub — for modules sourced from private repositories
-e GITHUB_TOKEN=...
# Consul — for Consul-backed remote state
-e CONSUL_HTTP_TOKEN=... \
-e CONSUL_HTTP_ADDR=...How credentials are provided depends on your CI/CD platform. Most platforms (GitHub Actions, GitLab CI, CircleCI…) offer native secret injection mechanisms that map directly to environment variables, so no files need to be mounted.
MIT © Yann Lambret