How to use the official Terraform Docker image

Audun Nes
4 min readSep 29, 2020

If you are reading this article, it is probably because you are already using HashiCorp Terraform to manage your infrastructure as code (IaC). If you are working in a larger automation team, you are also likely to discover the pitfalls if different team members are using different version of the Terraform CLI. This can include issues with shared state, or team members can hit bugs that are only applicable to an older Terraform CLI version.

One solution to this is to have all team members agree to use a specific version of Terraform CLI. This works fairly well, but it also means that every time you agree on using a more recent version of Terraform CLI, the whole team needs to upgrade at the same time. This can of course be automated, but an even easier method is to agree on using Terraform CLI inside a Docker image. Luckily for you, HashiCorp has made just that.

I believe one of the key strengths of Terraform is their excellent documentation. It is very well written, perfectly organised and full of great examples. The documentation for their official Docker images is in stark contrast though: https://hub.docker.com/r/hashicorp/terraform currently contains only the bare minimum, and assumes you have a completely flat directory structure.

Unless you already know Docker by heart, it is quite a steep learning curve to use Terraform CLI in that Docker image. So without further introduction, here is how I use it.

I have my infrastructure as code in a git repo (that very simplified) looks like this:

/IaC
├── README.md
├── env-vars
│ ├── prod.tfvars
│ └── test.tfvars
├── infrastructure
│ ├── test
│ │ ├── README.md
│ │ ├── config.tf
│ │ ├── data.tf
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── versions.tf
│ ├── prod
│ │ ├── README.md
│ │ ├── config.tf
│ │ ├── data.tf
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── versions.tf

I also use Azure for my infrastructure, and the Terraform state is also stored on Azure in a storage account on a resource group that is only used for storing state or secrets used for the automation. To authenticate towards Azure prior to running Terraform I have a script called ~/bin/azure-auth.sh that looks like this:

#!/bin/bash
export AZURE_CLIENT_ID=<hidden-value>
export AZURE_TENANT_ID=<hidden-value>
export AZURE_CLIENT_SECRET='<hidden-value>'
export AZURE_SUBSCRIPTION_ID=<hidden-value>
hideme=$(az login --service-principal --username $AZURE_CLIENT_ID --password $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID)
az account set -s $AZURE_SUBSCRIPTION_ID

So, if you remember from above I have sub directories for each of my environments, and a tfvars file for each environments.

First download the latest Docker images with HashiCorp Terraform:

docker pull docker.io/hashicorp/terraform

Find the version number of Terraform CLI inside the Docker image:

docker run -i -t docker.io/hashicorp/terraform version

At the time of writing this, the version number is: v0.13.3, so at this point you will agree with your team that this is the version number you will be using from now on, and until you decide again. This means that in all subsequent commands you will have to specify that version number.

docker run … hashicorp/terraform:0.13.3 …

You can pass environment variables into the Docker image (for Azure this is needed to authenticate) and you can also pass in options for Terraform. Below you will find my terraform init command where I both login to Azure and instruments Terraform where to store the state:

cd /IaC
export TF_ENV=test
docker run -i -t -v $PWD:$PWD -w $PWD/infrastructure/${TF_ENV} \
— env ARM_CLIENT_ID=$AZURE_CLIENT_ID \
— env ARM_CLIENT_SECRET=$AZURE_CLIENT_SECRET \
— env ARM_SUBSCRIPTION_ID=$AZURE_SUBSCRIPTION_ID \
— env ARM_TENANT_ID=$AZURE_TENANT_ID \
hashicorp/terraform:0.13.3 \
init -reconfigure \
-backend-config=”resource_group_name=${TF_ENV}-tf-state-rg” \
-backend-config=”storage_account_name=${TF_ENV}-tf-storage” \
-backend-config=”container_name=tfstate” \
-backend-config=”key=${TF_ENV}.terraform.tfstate”

Similar for terraform plan where I also refer to my tfvars file:

cd /IaC
export TF_ENV=test
docker run -i -t -v $PWD:$PWD -w $PWD/infrastructure/${TF_ENV} \
— env ARM_CLIENT_ID=$AZURE_CLIENT_ID \
— env ARM_CLIENT_SECRET=$AZURE_CLIENT_SECRET \
— env ARM_SUBSCRIPTION_ID=$AZURE_SUBSCRIPTION_ID \
— env ARM_TENANT_ID=$AZURE_TENANT_ID \
hashicorp/terraform:0.13.3 \
plan -var-file=../../env-vars/${TF_ENV}.tfvars -out=${TF_ENV}.tfplan

Then for terraform apply it will be:

cd /IaC
export TF_ENV=test
docker run -i -t -v $PWD:$PWD -w $PWD/infrastructure/${TF_ENV} \
— env ARM_CLIENT_ID=$AZURE_CLIENT_ID \
— env ARM_CLIENT_SECRET=$AZURE_CLIENT_SECRET \
— env ARM_SUBSCRIPTION_ID=$AZURE_SUBSCRIPTION_ID \
— env ARM_TENANT_ID=$AZURE_TENANT_ID \
hashicorp/terraform:0.13.3 \
apply ${TF_ENV}.tfplan

Finally the command for terraform destroy would be:

cd /IaC
export TF_ENV=test
docker run -i -t -v $PWD:$PWD -w $PWD/infrastructure/${TF_ENV} \
— env ARM_CLIENT_ID=$AZURE_CLIENT_ID \
— env ARM_CLIENT_SECRET=$AZURE_CLIENT_SECRET \
— env ARM_SUBSCRIPTION_ID=$AZURE_SUBSCRIPTION_ID \
— env ARM_TENANT_ID=$AZURE_TENANT_ID \
hashicorp/terraform:0.13.3 \
destroy -var-file=../../env-vars/${TF_ENV}.tfvars

I have presented this content in a pretty fast and straightforward way without diving into Docker features or Terraform command line options. I still hope you find this inspirational, and that it can help you towards your goal of automating infrastructure in a repeatable and predicable way.

Have fun, and stay frosty!

--

--

Audun Nes

Lead Cloud Engineer/Site Reliability Engineer from Copenhagen, Denmark. GitHub: https://github.com/avnes