Skip to main content

How to minimize the risk of upgrading Terraform? Using Terraform in Docker Container

·1427 words·7 mins
terraform docker

Upgrading our software
#

Software upgrades are essential these days. We need to be sure we are compliant with security advisories or have access to the newest features. It’s easy to talk about upgrading, but this process poses some risks, for example - our software could become unusable.

In the world of computer networks, one of the biggest nightmares for a Network Engineer is when the device cannot boot after an upgrade…

image
All Network Engineers know that feeling! (pic by Dan Vaclavek)

Upgrading Terraform
#

Terraform is no exception to this rule. Imagine that your project is using Terraform 1.4.0. You find that it would be great to have the import block support that was implemented in version 1.5.0. The plan is simple - it’s time to upgrade Terraform!

Can Docker reduce a risk? 🤔
#

Terraform could play a key role in your CI/CD pipeline - we need to be sure, that our automation will not be disturbed after the upgrade.

Recently I discovered that Hashicorp publishes docker images for every Terraform release. You can spin up your docker container with Terraform so you won’t mess up local Terraform installation.

Note: All Terraform images you can find on Terraform Docker Hub space

I realized I could use it to verify if my project that leverages Terraform will still work after the TF Core upgrade without actually upgrading it on a host level.

In my case I had Terraform installed on a system level and I didn’t want to mess up with this installation. I had to be sure my terraform configuration would work with a new TF Core version.

Running a container with Terraform fits perfectly here. You can spin up the container with the desired version to see if your project will work after the upgrade. If so, then you can proceed with a upgrade on production.

I’ve prepared a small lab scenario to show you how it works.

Lab setup
#

Without further due, let’s see how Docker can help us! 🐳

In directory I have a single main.tf file.

mateusz@mfr terraform-docker % ls
main.tf

main.tf file is very simple. It consists of terraform and provider configuration and also single resource declaration.

terraform {
  required_providers {
    aci = {
      source = "ciscodevnet/aci"
    }
  }
}

provider "aci" {
  username = "admin"
  password = "*"
  url      = "https://sandboxapicdc.cisco.com"
}

resource "aci_tenant" "mfr_tenant" {
  name = "mfr_tenant"
}

Using Terraform Docker image
#

In order to run Terraform inside Docker container you need to run following command:

docker run --rm hashicorp/terraform:<image_tag> <TF_COMMAND>

Looks complicated? Let’s break this down.

  • docker run - runs a new container from provided image
  • --rm - removes container when it exits
  • hashicorp/terraform:<image_tag> - Image name for our container. For the purpose of this lab I used 1.7.4, so syntax was hashicorp/terraform:1.7.4.
  • <TF_COMMAND> - at the end of docker run command you can pass extra command to the container.
You can find all tags for Terraform Docker image here

I executed terraform version inside my container. ENTRYPOINT for terraform container is a terraform command, so if you pass version to the docker run command, terraform version will be executed inside.

docker run --rm hashicorp/terraform:1.7.4 version
Terraform v1.7.4

If you are really curious - you can see how Dockerfile for Terraform looks like.

Executing Init, Plan and Apply
#

Next, I had to execute the init, plan, and apply commands, but first, I had to give my container access to local terraform files. Docker uses volumes to mount host directories to the container space so they can be used later on. It’s like giving container access to the local files.

To achieve that, I passed extra arguments to the previous command so the final result is:

docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) hashicorp/terraform:1.7.4 init
  • -v $(pwd):$(pwd) - mount local storage to the container. I want to present my current directory with terraform files to the container current directory, so Terraform inside container will see those.
  • -w $(pwd) - set working directory for a container. It’s used to tell docker that command needs to be executed inside specified directory. I need to execute terraform inside mounted directory where main.tf file wa.

From this point, my container had an access to main.tf file so I could proceed with init.

mateusz@mfr terraform-docker % docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) hashicorp/terraform:1.7.4 init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of ciscodevnet/aci...
- Installing ciscodevnet/aci v2.13.2...
- Installed ciscodevnet/aci v2.13.2 (signed by a HashiCorp partner, key ID 433649E2C56309DE)

Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

My Terraform was successfully initialized using provided main.tf file, meaning that volume works any my container had access to the file.

mateusz@mfr terraform-docker % docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) hashicorp/terraform:1.7.4 plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aci_tenant.mfr_tenant will be created
  + resource "aci_tenant" "mfr_tenant" {
      + annotation                    = "orchestrator:terraform"
      + description                   = (known after apply)
      + id                            = (known after apply)
      + name                          = "mfr_tenant"
      + name_alias                    = (known after apply)
      + relation_fv_rs_tenant_mon_pol = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

This command is way too long!
#

I agree with you. I didn’t like it too, so I found solution for this. I applied alias in my .zshrc file (if you use bash shell, this will be .bashrc)

mateusz@mfr terraform-docker % cat ~/.zshrc | grep tf
alias tf='docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) hashicorp/terraform:1.7.4'
Note: Pay attention to quotes when setting up your alias. Use single quotes, otherwise pwd command substitution will be done at the time of alias declaration, not afterwards.

Now I could use my new alias to execute apply.

mateusz@mfr terraform-docker % tf apply --auto-approve

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aci_tenant.mfr_tenant will be created
  + resource "aci_tenant" "mfr_tenant" {
      + annotation                    = "orchestrator:terraform"
      + description                   = (known after apply)
      + id                            = (known after apply)
      + name                          = "mfr_tenant"
      + name_alias                    = (known after apply)
      + relation_fv_rs_tenant_mon_pol = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
aci_tenant.mfr_tenant: Creating...
aci_tenant.mfr_tenant: Creation complete after 4s [id=uni/tn-mfr_tenant]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

You can also define multiple aliases to test different Terraform version.

mateusz@mfr terraform-docker % alias | grep tf
tf='docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) hashicorp/terraform:1.7.4'
tf140='docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) hashicorp/terraform:1.4.0'
tf150='docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) hashicorp/terraform:1.5.0'
mateusz@mfr terraform-docker % tf150 version
Terraform v1.5.0
on linux_arm64

Your version of Terraform is out of date! The latest version
is 1.7.4. You can update by downloading from https://www.terraform.io/downloads.html
mateusz@mfr terraform-docker % tf140 version
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
Terraform v1.4.0
on linux_amd64

Your version of Terraform is out of date! The latest version
is 1.7.4. You can update by downloading from https://www.terraform.io/downloads.html

Isn’t that great?! 😎

Summary
#

Using Terraform in Docker is a great way to ensure that our project works with a new Terraform version. I strongly recommend spending time on testing before the upgrade even though this will take more time than the actual upgrade. This is worth doing that - reducing risk is essential. You don’t want to spend extra hours being stressed about that crucial app that use terraform is not working, don’t you?