Produce Reliable Terraform Code With Pre-Commit Hooks

Produce Reliable Terraform Code With Pre-Commit Hooks

Run code linters and scanners in your CI/CD to enhance Terraform code

Guillaume Vincent
Guillaume Vincent

In a previous article, I presented to you the coding-style importance and pre-commit framework. Today, we're going to handle pre-commit for Terraform code. It will be in charge of validating code using linters and scanners. For any new git commit, we'll be able to ensure there is no regression and keep a constant quality.

We'll create a Terraform module for having something to test. The modules are good to not duplicated code and should be hardly tested to be reliable. We'll also have a look at which pre-commit plugins to use for Terraform.

Once the module is in place, we'll produce a Docker image for pre-commit Terraform purposes. The goal is to have a portable and ready-to-use toolbox for CI/CD. The module will have its own Circle CI project and reuse the image for testing.

Terraform Module

As an example, we're going to code a module to configure networking in AWS

Terraform version and providers

Variables

Outputs

Main

Pre-commit configuration

The git repo includes the pre-commit configuration. Inside I couple two pre-commit repositories:

The tflintrules are present in a separate file :

Pre-Commit Docker Image

Dockerfile

Now we’re going to create the Docker image. It should be ready to use with all the dependencies and as light as possible. We’re going to build it from an alpine image. We limit the layer numbers as much as possible. Docker will recover and build it faster and thus quicker CI/CD workflow.

Building and testing

We configure the test and build of the image in a Circle CI project. It runs hadolint to ensure the image is clean. Then it builds and pushes the image to the DockerHub repository :

CI for Terraform Modules

Configuration

The module repo has its own Circle CI configuration. The workflow checks out the repo and executes the image to run pre-commit checks :

Result

Any changes in git triggered the workflow for inspection :

Circle CI project of the Terraform module
Circle CI project of the Terraform module

The check results are visible in the workflow logs :

#!/bin/bash -eo pipefail 

docker run -v $PWD:/pre-commit --rm guivin/pre-commit-terraform
Unable to find image 'guivin/pre-commit-terraform:latest' locally
latest: Pulling from guivin/pre-commit-terraform

ab65f19f: Pulling fs layer 
2803aa7c: Pulling fs layer 
1c1036d2: Pulling fs layer 
Digest: sha256:fcbef363542a891ef0a238381aed17e974279cb657083d8abe5852ba2c1d144a
Status: Downloaded newer image for guivin/pre-commit-terraform:latest
[INFO] Initializing environment for https://github.com/gruntwork-io/pre-commit.
[INFO] Initializing environment for git://github.com/antonbabenko/pre-commit-terraform.
Terraform fmt............................................................Failed
- hook id: terraform-fmt
- exit code: 3

main.tf
--- old/main.tf
+++ new/main.tf
@@ -25,8 +25,8 @@
   vpc_id = join("", aws_vpc.this[*].id)
 
   route {
-	cidr_block = "0.0.0.0/0"
-	gateway_id = join("", aws_internet_gateway.this[*].id)
+    cidr_block = "0.0.0.0/0"
+    gateway_id = join("", aws_internet_gateway.this[*].id)
   }
 
   tags = var.tags
variables.tf
--- old/variables.tf
+++ new/variables.tf
@@ -6,9 +6,9 @@
 
 
 variable "region" {
-  type = string
+  type        = string
   description = "AWS region where to create resources"
-  default = "us-east-1"
+  default     = "us-east-1"
 }
 
 variable "tags" {
@@ -28,7 +28,7 @@
 variable "public_subnet_cidr_blocks" {
   type        = list(string)
   description = "List of CIDR blocks for public subnets"
-  default     = [
+  default = [
     "10.0.1.0/24",
     "10.0.2.0/24",
     "10.0.3.0/24"
@@ -38,7 +38,7 @@
 variable "private_subnet_cidr_blocks" {
   type        = list(string)
   description = "List of CIDR blocks for private subnets"
-  default     = [
+  default = [
     "10.0.4.0/24",
     "10.0.5.0/24",
     "10.0.6.0/24"

Terraform validate.......................................................Failed
- hook id: terraform-validate
- files were modified by this hook

--> Running 'terraform validate' in directory '.'

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

Terraform has been successfully initialized!
Success! The configuration is valid.


tflint...................................................................Failed
- hook id: tflint
- exit code: 2

Failed to check `terraform_naming_convention` rule. 1 error(s) occurred:

Error: Unsupported block type

  on .tflint.hcl line 42:
  (source code not available)

Blocks of type "local" are not expected here. Did you mean "locals"?

markdown-link-check......................................................Failed
- hook id: markdown-link-check
- exit code: 1

markdown-link-check is not available on this system.
Please install it by running 'npm install -g markdown-link-check'

Terraform validate with tfsec............................................Passed
Terraform docs...........................................................Passed

Exited with code exit status 1
CircleCI received exit code 1

Resources

pre-commit
hadolint/hadolint
Dockerfile linter, validate inline bash, written in Haskell - hadolint/hadolint
antonbabenko/pre-commit-terraform
pre-commit git hooks to take care of Terraform configurations - antonbabenko/pre-commit-terraform
gruntwork-io/pre-commit
A collection of pre-commit hooks used by Gruntwork tools - gruntwork-io/pre-commit
Infrastructure as CodeCI/CD

Guillaume Vincent

DevOps Engineer & AWS Certified Solution Architect. Cloud enthusiast and automation addict