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 usable 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 editorPopulate 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!