How to simplify your CI/CD with Makefiles

In this post I'll show examples of how a Makefile can be used to help others use your project and also simplify your CI/CD Terraform workflows.

How to simplify your CI/CD with Makefiles
Photo by Gabriel Crismariu / Unsplash

Definition

Make is a build automation tool that automatically builds executable programs and libraries from source code by reading files called Makefiles which specify how to derive the target program.

Simply put, a Makefile is a file which controls the 'make' command.

The humble Makefile has been somewhat forgotten in my opinion. Make is available on virtually every platform, and is used to control the build process of a project.

Once a Makefile has been written for a project, make can easily and efficiently be used to build the project.

This could be a bunch of command line statements that compiles your software for a specific platform or as simple as creating a directory structure.

The makefile also acts as a useable piece of documentation to assist other in using a project.

In this post I'll show examples of how a Makefile can be used to help others use your project and also simplify your CI/CD workflows.

Teraform Workflow

Many who use Terraform to provision and control Public Cloud infrastructure will be familiar with the standard Terraform Workflow

Steps in the workflow:-

terraform init

The terraform init command is used to initialize a working directory containing Terraform configuration files. This is the first command that should be run after writing a new Terraform configuration or cloning an existing one from version control. It is safe to run this command multiple times.

terraform plan

The terraform plan command creates an execution plan, which lets you preview the changes that Terraform plans to make to your infrastructure.

terraform apply

The terraform apply command executes the actions proposed in a Terraform plan.

terraform destroy

The terraform destroy command is a convenient way to destroy all remote objects managed by a particular Terraform configuration.

Each of the above commands can have optional arguments that may be specific to your project.

For example

Maybe you are using a terraform_state.tfvars file to tell terraform where to put your state file. The init command may need to look like this.

terraform_state.tfvars terraform init -backend-config="terraform_state.tfvars"

Maybe you are pointing to specific variables files for the plan and apply phases of the workflow. The plan command may need to look like this.

terraform plan -var-file="variables.tfvars"

While you can provide other users of the project README files to detail this, often a better way is to wrap these commands up in a Makefile.

The Makefile

Using the above context we can bundle all this project specific stuff into a single Makefile in the root of your project.

  • Create a file named Makefile using your favourite editor

  • Populate the contents of the make file

init:
	terraform init

validate:
	terraform fmt -recursive
	terraform validate

plan:
	terraform validate
	terraform plan -var-file="variables.tfvars"

apply:
	terraform apply -var-file="variables.tfvars" --auto-approve

destroy:
	terraform destroy -var-file="variables.tfvars"
    
all: validate plan apply

NOTE: Makefiles must be indented using TABs and not spaces or make will fail.

The last all option can be used to combine multiple actions

Using the Makefile

Now we have our Makefile, lets use it! Since we are running Terraform commands we need some Terraform code. Here are the example files in my demo directory.

.
├── Makefile
├── main.tf
└── variables.tfvars
$ cat main.tf
resource "random_id" "random" {
  byte_length = 8
}

output "random" {
  value = random_id.random
}

variable "input" {
  type = string
}
$ cat variables.tfvars
input = "demo"

In directory where the makefile exists, I run the following command

make init

This is going to kick off our terraform init command, pulling down any dependant providers and initialize our statefile.

Example Output

make init
terraform init 

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/random from the dependency lock file
- Installing hashicorp/random v3.2.0...
- Installed hashicorp/random v3.2.0 (signed by HashiCorp)

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.

Now we can also perform a plan by adjusting the make command option as follows:-

Example Output

make plan
terraform validate
Success! The configuration is valid.

terraform plan -var-file="variables.tfvars"

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:

  # random_id.random will be created
  + resource "random_id" "random" {
      + b64_std     = (known after apply)
      + b64_url     = (known after apply)
      + byte_length = 8
      + dec         = (known after apply)
      + hex         = (known after apply)
      + id          = (known after apply)
    }

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

Changes to Outputs:
  + random = {
      + b64_std     = (known after apply)
      + b64_url     = (known after apply)
      + byte_length = 8
      + dec         = (known after apply)
      + hex         = (known after apply)
      + id          = (known after apply)
      + keepers     = null
      + prefix      = null
    }

Here we can see that make has performed several actions for us in one hit.

  • Run terraform fmt -recursive & terraform validate against out Terraform code, always good to have nicely formatted and valid code.
  • Run terraform plan -var-file="variables.tfvars" to see what changes are required.

Now we can run our apply.

Example Output

make apply  
terraform apply -var-file="variables.tfvars" --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:

  # random_id.random will be created
  + resource "random_id" "random" {
      + b64_std     = (known after apply)
      + b64_url     = (known after apply)
      + byte_length = 8
      + dec         = (known after apply)
      + hex         = (known after apply)
      + id          = (known after apply)
    }

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

Changes to Outputs:
  + random = {
      + b64_std     = (known after apply)
      + b64_url     = (known after apply)
      + byte_length = 8
      + dec         = (known after apply)
      + hex         = (known after apply)
      + id          = (known after apply)
      + keepers     = null
      + prefix      = null
    }
random_id.random: Creating...
random_id.random: Creation complete after 0s [id=7rS7GylfnAc]

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

Outputs:

random = {
  "b64_std" = "7rS7GylfnAc="
  "b64_url" = "7rS7GylfnAc"
  "byte_length" = 8
  "dec" = "17200578602167409671"
  "hex" = "eeb4bb1b295f9c07"
  "id" = "7rS7GylfnAc"
  "keepers" = tomap(null) /* of string */
  "prefix" = tostring(null)
}

As we had the auto-approve option set our resources are created as per our code.

The benefits!

The above is a very simple example of simplifying the build process for provisioning resources and is the tip of the iceburg!

Consider these additional benefits:-

  • All you need to perform in your GitHub Action or CodeBuild job is install the desired version of Terraform and then use the above single-line commands to control the Terraform workflow!
  • All the knowledge needed to build or provision your project is available and versioned alongside your Terraform policy code!
  • Need to debug something, checkout the repo and run through the CI/CD commands locally on your laptop or dev system!

Hope someone else finds this useful.

Cheers!