Ensuring Your Terraform is Correctly Formatted Using Terraform fmt and GitHub Actions

As with all pull requests, we want to run various CI checks to ensure validation of your code and one of these is code quality, writing Terraform? Unsure if the Terraform in the pull-request is formatted correctly? This is where GitHub Actions come in handy. In this post, we’ll explore how to ensure your Terraform code is correctly formatted using terraform fmt and GitHub Actions

What is terraform fmt?

terraform fmt is a command as part of Terraform that is used to check and rewrite your Terraform files to the required canonical format and style. Several features and what it checks for is:

  • Indentation – Ensures your Terraform code has correct indentation, making the code easier to understand and read
  • Spacing – Checks for correct spacing between any resources and values of your Terraform code. Again correct spacing assists with the readability of your Terraform configuration(s)
  • Alignment – Aligning the Terraform in the correct canonical format

By using terraform fmt, you can ensure that your Terraform code adheres to best practices for formatting. Now let look at how we can integrate terraform fmt with GitHub Actions to automatically check the formatting of your Terraform code in every pull request.

The GitHub Action Workflow

name: Terraform Fmt
on: 
  pull_request:
    branches:
     - main
  workflow_dispatch:

env:
  tf_resource_group_name: "thomasthorntoncloud"
  tf_storage_account_name: "thomasthorntontfstate"
  tf_state_container: "github-tfplan-output-to-pr"
  tf_state_key: "terraform.tfstate"
      
jobs: 
  terraform-fmt-check:
    if: github.event_name == 'pull_request'
    defaults:
      run:
        working-directory: terraform-example-deploy
    name: Terraform
    environment: production
    runs-on: ubuntu-latest
    permissions:
      contents: write
    env:
      ARM_CLIENT_ID: "${{ secrets.AZURE_CLIENT_ID }}"
      ARM_SUBSCRIPTION_ID: "${{ secrets.AZURE_SUBSCRIPTION_ID }}"
      ARM_TENANT_ID: "${{ secrets.AZURE_TENANT_ID }}"
    steps:
    - name: Checkout Code
      uses: actions/checkout@v2.5.0

    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3

    - name: Terraform Init
      id: init
      run: terraform init -backend-config="resource_group_name=$tf_resource_group_name" -backend-config="storage_account_name=$tf_storage_account_name" -backend-config="container_name=$tf_state_container" -backend-config="key=$tf_state_key"
      env:
        ARM_CLIENT_ID: ${{ secrets.CLIENT_ID }}
        ARM_CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}
        ARM_SUBSCRIPTION_ID: ${{ secrets.SUBSCRIPTION_ID }}
        ARM_TENANT_ID: ${{ secrets.TENANT_ID }}
      working-directory: ./terraform
        
    - name: Terraform Fmt
      id: fmt
      run: terraform fmt
      working-directory: ./terraform

    - name: Auto Commit Changes
      uses: stefanzweifel/git-auto-commit-action@v5
      with:
        commit_message: "Terraform fmt"
        file_pattern: "*.tf"
        commit_user_name: "github-actions[bot]"

A brief overview of what the above does:

  1. Checkout Code: It checks out your code using the actions/checkout@v2.5.0 action.
  2. Setup Terraform: It sets up Terraform using the hashicorp/setup-terraform@v3 action. The terraform_wrapper input is set to true, which means the action will install a wrapper script to enhance the Terraform CLI’s functionality.
  3. Terraform Init: It initializes your Terraform configuration. This is the first command that should be run after writing a new Terraform configuration or cloning an existing one. It is safe to run this command multiple times. The -backend-config options are used to configure the backend, which is used for state storage. (not needed for Terraform fmt, but just added for some context)
  4. Terraform Fmt: It runs the terraform fmt command, which automatically updates your Terraform configuration files to a canonical format and style.
  5. Auto Commit Changes: If the terraform fmt command made any changes to the .tf files, those changes are automatically committed to the repository using the stefanzweifel/git-auto-commit-action@v5 action. The commit message is “Terraform fmt”, and the commit is made by “github-actions[bot]”.

Breakdown of Terraform fmt & Auto Commit changes steps

The terraform format step is very straightforward, it jus running terraform fmt as part of your GitHub Action workflow.

Currently I configured the below to run in a specific directory, you may even want to run terraform fmt -recusive which will tell terraform to also check all sub-directories of the current working-directory for terraform files also.

    - name: Terraform Fmt
      id: fmt
      run: terraform fmt
      working-directory: ./terraform

You may be wondering why the need for Auto Commit changes?

Well, when you run the above, it will format correctly any terraform files – as below:

It has formatted main.tf in this instance – we not want the updated file to be committed automatically back into the pull-request. With the below step, it will auto commit file changes that match the pattern *.tf – thanks to Stefan for stefanzweifel/git-auto-commit-action@v5

    - name: Auto Commit Changes
      uses: stefanzweifel/git-auto-commit-action@v5
      with:
        commit_message: "Terraform fmt"
        file_pattern: "*.tf"
        commit_user_name: "github-actions[bot]"

Lets look at an example usage

Now that we’ve showed the workflow and actions, lets look at a quick test. Here is an example terraform file that I will add as part of my pull request

resource "azurerm_resource_group" "example"           {
  name          = "thomasthorntoncloud-rg-terraform-fmt"
  location =         "UK South"
}

resource "azurerm_virtual_network" "example" {
  name                = "my-vnet"
  address_space       = ["10.0.0.0/16"    ]
  location            = azurerm_resource_group.example.location
  resource_group_name     = azurerm_resource_group.example.name
}

Noticed the incorrect Terraform formatting? (Lines highlighted)

I will commit the above and raise a pull-request to my GitHub repository, and show the output:

Checking commit history, reviewing the commit from github-actions[bot] – we can see that it has committed successfully the updated main.tf from the terraform fmt stage – awesome!

Below is the output with the correct formatting:

resource "azurerm_resource_group" "example" {
  name     = "thomasthorntoncloud-rg-terraform-fmt"
  location = "UK South"
}

resource "azurerm_virtual_network" "example" {
  name                = "my-vnet"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
}

To summarise, a quick test example above demonstrates the workflow’s effectiveness. A mis-formatted Terraform file is committed, triggering the GitHub Action. The output showcases the corrected formatting committed automatically by the GitHub Actions bot.

Automating Terraform formatting with GitHub Actions streamlines code quality checks, ensuring consistency and adherence to best practices.

GitHub Repository containing examples from above

Leave a Reply