Workstation Setup for Terraform

- 7 mins read

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.