Workstation Setup for Terraform
Introduction
In my last post, I covered how I set up VSCode for Terraform. The second most popular question is how I set up my workstation for Terraform development.
As with most things in the tech world, there are many ways to do things. The following is the way that works for me. I am always looking for ways to improve my workflow, so if you have suggestions, please let me know.
Additionally, I am a Mac user, so some of the tools I use are harder to use on Windows. However, the tools can be used in WSL or DevContainers if you use either of those tools.
On a Mac, I use Homebrew to install my tools. Also, Since we
have to store all our code in a version control system, I installed the latest
version of git
with Homebrew.
# Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install Git
brew install git
Installing Terraform
I have to use multiple versions of Terraform for different projects, so I use TFSwitch to switch the version of Terraform installed based on the directory I am in and the version constraint in my Terraform configuration.
brew install warrensbox/tap/tfswitch
I have configured tfswitch
to link the correct version of Terraform to
~/bin/terraform
as the ~/bin
directory is in my $PATH
. This is done by
creating a ~/.tfswitch.toml
file with the following contents:
bin="~/bin/terraform"
I use the following zsh
function to load tfswitch
in a directory with
Terraform configuration files.
load-tfswitch() {
local tfswitchrc_path="${HOME}/.tfswitch.toml"
# if [[ -f "$tfswitchrc_path" ]] && [[ -f "terraform.tf" ]]; then
if [[ -f "$tfswitchrc_path" ]]; then
if [[ $(ls -l *.tf | wc -l ) -gt 0 ]] 2> /dev/null; then
tfswitch
fi
fi
}
add-zsh-hook chpwd load-tfswitch
load-tfswitch
Now, if you don’t need to support multiple versions of Terraform, you can install it with Homebrew from HashiCorp’s tap.
brew install hashicorp/tap/terraform
Otherwise, you can download it directly from the Terraform Install page.
Git Tools
Storing your Terraform configuration in a version control system is a must. I don’t do development daily, so I forget to format my code and check for secrets or other vulnerabilities. One of the worst is that I forgot to update the README.md with any variable or output changes. The most annoying thing is that I forgot to create a new branch for my changes, as most of the git repos I work in have the default branch protected, so I can’t push directly to it.
To help me with all of these problems, I use
Git Hooks,
specifically the pre-commit
hook to check for all of these things before I
commit my changes. If you are unfamiliar with Git Hooks, they are scripts that
run automatically when specific actions occur in a Git repository. You can use
these scripts to enforce coding standards, check for vulnerabilities, or even
run tests before you commit your changes.
Traditionally, you would have to write these scripts yourself, but there is a tool called pre-commit that makes it easy to install and manage these hooks.
brew install pre-commit
Once you have pre-commit
installed, you can create a .pre-commit-config.yaml
file in the root of your repository with the following contents:
# yaml-language-server: $schema=https://json.schemastore.org/pre-commit-config.json
repos:
- repo: local
hooks:
- id: trufflehog
name: TruffleHog
description: Detect secrets in your data.
entry: trufflehog git file://. --since-commit HEAD --fail
language: golang
pass_filenames: false
stages: ["pre-commit", "pre-push"]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-merge-conflict
- id: end-of-file-fixer
- id: no-commit-to-branch
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.97.0
hooks:
- id: terraform_fmt
- id: terraform_validate
args:
- --hook-config=--retry-once-with-cleanup=true
- id: terraform_docs
args:
- --args=--lockfile=false
- id: terraform_tflint
args:
- --args=--config=__GIT_WORKING_DIR__/.tflint.hcl
- id: terraform_checkov
- id: infracost_breakdown
args:
- --args=--path=.
- --args=--terraform-var-file="terraform.tfvars"
# verbose: true
Every time you commit your changes, the pre-commit
tool runs and checks each
hook you have defined. If any hook fails, the commit will abort, and you must
fix the issues before committing your changes.
We haven’t covered all of the hooks in the .pre-commit-config.yaml
file, but
I’ll list the ones I use here:
trufflehog
: Scans your git repo for committed secrets 😱.check-merge-conflict
: Checks for files that contain merge conflict strings.end-of-file-fixer
: Ensures that files end with a newline.no-commit-to-branch
: Prevents commits directly to a branch (default branch in our case).terraform_fmt
: Formats your Terraform code.terraform_validate
: Validates your Terraform code.terraform_docs
: Dynamically updates your README.md with information on your module’s inputs, outputs, and requirements.terraform_tflint
: A Terraform linter that checks for best practices and errors in your Terraform code.terraform_checkov
: A tool that checks your Terraform code for security vulnerabilities.infracost_breakdown
: Gives you a cost estimate for the cloud resources your module would deploy.
Tools needed for the Pre-commit hooks that I use
You will need to install a few of these tools to use all of the pre-commit
hooks that I have listed above.
Trufflehog scans your git repo for secrets 😱. Doing this as a pre-commit hook lets you catch secrets before committing them. This way, they don’t end up in your git history, keeping your security team happy.
brew install trufflesecurity/trufflehog/trufflehog
Terraform-docs dynamically updates your README.md with information on your module’s inputs, outputs, and requirements.
brew install terraform-docs
In your README.md file, you can add the following comments to have
terraform-docs
update the file.
<!-- BEGIN_TF_DOCS -->
<!-- END_TF_DOCS -->
Infracost provides a cost estimate for the cloud resources on which your configuration will be deployed.
brew install infracost
Jq is a lightweight and flexible command-line
JSON processor. required for terraform_validate
with
--retry-once-with-cleanup
flag, and for infracost_breakdown
hook.
brew install jq
TFLint is a Terraform linter that checks for best practices and errors in your Terraform code.
brew install tflint
Checkov is a static code analysis tool for infrastructure as code (IaC).
brew install checkov
Convenience Tools
With Terraform installed via tfswitch
and the pre-commit
hooks setup, I have
a solid foundation for my Terraform development workflow. But I use a few more
tools to make my life easier.
First, I use 1Password to store my secrets, API keys, and passwords. I also use the 1Password CLI to access my secrets via environment variables and shell scripts.
brew install 1password 1password-cli
With 1Password and the 1Password CLI installed, I can access my secrets via the
op
command.
export TFE_TOKEN=$(op read --cache "op://Vault/Item/Key")
This allows me to easily set environment variables for my secrets in my shell. However, I need to remember to unset these variables when I’m done with them. To help with this, I use Direnv to set and unset variables based on my directory.
brew install direnv
I use the following .envrc
file in the root of my Terraform configuration to
set my secrets.
# Exports
export TFE_TOKEN=$(op read --cache "op://Vault/Item/Key")
# Terraform Variable exports
export TF_VAR_okta_token=$(op read --cache "op://Vault/Item/Section/Key")
export TF_VAR_okta_client_id=$(op read --cache "op://Vault/Item/Section/Key")
export TF_VAR_okta_client_secret=$(op read --cache "op://Vault/Item/Section/Key")
The above example exports the TFE_TOKEN
and a few Terraform variables. When I
change into or out of the directory with the .envrc
file, direnv
will set
and unset these variables for me.
With so many tools in use, remembering all the commands to run can be a pain.
Yes, I could create aliases for them, but I like to keep my shell clean. So, I
use Task to create a Taskfile.yml
with all the
commands I use.
# yaml-language-server: $schema=https://taskfile.dev/schema.json
# https://taskfile.dev
version: "3"
dotenv: [".envrc"]
vars:
CURRENT_DATE:
sh: date +"%Y-%m-%dT%H:%M:%S%Z"
tasks:
default:
cmds:
- task: pre
hog:
cmds:
- trufflehog git file://. --since-commit HEAD --only-verified --fail
pre:
cmds:
- pre-commit autoupdate
- pre-commit run -a
push:
cmds:
- git add .
- git commit -m "{{.CURRENT_DATE}}"
- git push
silent: true
tag:
cmds:
- git push
- git tag -s {{.CLI_ARGS}} -m "{{.CLI_ARGS}}"
- git push --tags
Now I can run task hog
to scan my git repo for secrets, task pre
to run all
of my pre-commit
hooks, task push
to commit my changes and push them to the
remote, and task tag -- v1.0.0
to tag my release.
Conclusion
This is how I set up my workstation for Terraform development. I hope you found it helpful. If you have any suggestions or tools that you use, please let me know. I am always looking for ways to improve my workflow.