Terraform pipeline with popular open source tools

Harshit JainHarshit Jain
5 min read

Overview:

Today we will look beyond terraform validate, plan and apply. While writing IaC, it is important to treat it like a code rather than just a bunch of tf files for spinning up the infrastructure.

Today in this article, I will focus on how to ensure clean IaC coding with required security measures by enforcing checks in the terraform pipeline.

Scenarios:

Now, there will be 3 actions we need to perform:

  1. Terraform Plan (Triggers at: PR level)

  2. Terraform Apply (Triggers at: Code merge in default branch)

  3. Terraform Destroy (Trigger: Manual trigger)

In this article, we will focus on the Terraform plan part, which means checking the IaC at PR level (before applying the changes), because all the creativity will take place at this stage only. Rest both the stages (apply & destroy) will be plan and simple.

CICD Tool used:

For this lab, I will be using Github actions as the CICD tool. If you are using any other tool like Azure DevOps or Jenkins, you can use the same steps there as well.

Steps:

  1. So the first thing in pipeline is specifying the trigger for the pipeline. Since this pipeline is for checking the code at PR level, the trigger will be set for PRs targeting the default branch.

     name: Terraform Plan
    
     on:
       pull_request:
         types:
           - opened
           - synchronize
           - reopened
         branches:
           - 'main'
    
  2. Now starts the jobs, the first job will be for checking the code quality, possible errors, deprecated syntax, naming convention etc. For this, we will be using the open source tool TFlint.

     jobs:
       terraform-lint:
         name: TFLint
         runs-on: ubuntu-latest
         steps:
           - name: Checkout code
             uses: actions/checkout@v4
    
           - uses: terraform-linters/setup-tflint@v4
             name: Setup TFLint
             with:
               tflint_version: v0.53.0
    
           - name: Run TFLint
             run: tflint
             working-directory: infra
    

    Lets understand each step:

    • First, we are checking out the code.

    • Then, we are using the Github action to setup/install the Tflint tool. You can also do this by using simple bash command.

    • Finally, we are running the tflint command in the directory that contains the IaC.

  3. Second job is for checking the code for security vulnerabilities, for this we will be using popular open source tool Trivy.

       tf-security-scan:
         name: trivy
         runs-on: ubuntu-latest
         steps:
           - name: Checkout code
             uses: actions/checkout@v4
    
           - name: Install trivy
             run: |
               sudo apt-get install wget apt-transport-https gnupg
               wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
               echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
               sudo apt-get update
               sudo apt-get install trivy
    
           - name: Run trivy
             run: trivy config .
             working-directory: infra
    

    Lets understand each step:

    • Checkout code.

    • Install Trivy.

    • Run Trivy in the source directory.

  4. Once the security scan is successfully succeeded we will create a terraform plan, and post the plan in github PR comment. Commenting the terraform plan in PR will help the reviewer to overview the PR request quickly.

     terraform-plan:
       name: Terraform plan
       runs-on: ubuntu-latest
       needs: tf-security-scan
       permissions:
         contents: read
         pull-requests: write # Required to post comments
    
       env:
         ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
         ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
         ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
         ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
         TF_VAR_registry_password: ${{ secrets.TF_VAR_registry_password }}
    
       steps:
         - name: Checkout code
           uses: actions/checkout@v4
    
         - name: Setup Terraform
           uses: hashicorp/setup-terraform@v3
           with:
             terraform_version: 1.9.4
    
         - name: Terraform Init
           run: terraform init
           working-directory: infra
    
         - name: Terraform validate
           run: terraform validate
           working-directory: infra
    
         - name: Terraform Plan
           run: terraform plan -var-file=dev.tfvars -out=.planfile
           working-directory: infra
    
         - name: Post terraform plan comment
           uses: borchero/terraform-plan-comment@v2
           with:
             token: ${{ github.token }}
             working-directory: infra
             planfile: .planfile
    

    Lets understand the steps:

    • In Github actions, we get a github.token which has a lifecycle of that pipeline run. That means, we don't have to provide any PAT token to the pipeline for authentication. To read more about github.token visit here.

    • So first, we will provide a permission block, this block is for providing permissions to our github token which will be used for commenting on the PR.

    • Next, we will define all the env variables for terraform plan, basically authenticating to our cloud provider.

    • Next, checkout the code.

    • Terraform install using github action (can be done by using bash commands).

    • Terraform initialise for downloading the modules.

    • Terraform validate to check the syntax.

    • Terraform plan by mentioning the var file and outputting the output in .planfile

    • Now, we will use the github action borchero/terraform-plan-comment@v2 for commenting terraform plan output, it will take .planfile as an input and will post a well formatted comment on the PR.

  5. At last, we will compare the change in infra between default branch and PR and will comment the change in cost on PR using open source tool Infracost. This tool is for cost estimation of our infra change.

     Infracost:
       runs-on: ubuntu-latest
       needs: terraform-plan
       permissions:
         contents: read
         pull-requests: write
    
       steps:
         - name: Setup Infracost
           uses: infracost/actions/setup@v3
           with:
             api-key: ${{ secrets.INFRACOST_API_KEY }}
    
         # Checkout the base branch of the pull request (e.g. main/master).
         - name: Checkout base branch
           uses: actions/checkout@v4
           with:
             ref: '${{ github.event.pull_request.base.ref }}'
    
         # Generate Infracost JSON file as the baseline.
         - name: Generate Infracost cost estimate baseline
           run: |
             infracost breakdown --path=infra \
               --format=json \
               --out-file=/tmp/infracost-base.json \
               --terraform-var-file dev.tfvars
    
         # Checkout the current PR branch so we can create a diff.
         - name: Checkout PR branch
           uses: actions/checkout@v4
    
         # Generate an Infracost diff and save it to a JSON file.
         - name: Generate Infracost diff
           run: |
             infracost diff --path=infra \
               --format=json \
               --compare-to=/tmp/infracost-base.json \
               --out-file=/tmp/infracost.json \
               --terraform-var-file dev.tfvars
    
         - name: Post Infracost comment
           run: |
             infracost comment github --path=/tmp/infracost.json \
               --repo=$GITHUB_REPOSITORY \
               --github-token=${{ github.token }} \
               --pull-request=${{ github.event.pull_request.number }} \
               --behavior=update
    

    Lets understand each step in detail:

    • Setup the infracost using the Infracost API key, you can get this by running the command infracost auth login && infracost configure get api_key.

    • Checkout base branch, which is target branch for the pull request.

    • Generate a cost estimate report for the default branch.

    • Now checkout to the PR branch.

    • Now generate a cost difference by comparing current branch with default branch.

    • Post the report in the comment of the PR.

0
Subscribe to my newsletter

Read articles from Harshit Jain directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harshit Jain
Harshit Jain

DevOps Engineer