Automated Terraform Tests for Azure using GitHub Actions
Marcel.L
Posted on February 14, 2022
Overview
This tutorial uses examples from the following GitHub project Terraform module repository - Dynamic Subnets.
In the previous tutorial on this blog series Terraform Registry, we looked at how to automate terraform module releases on the public registry using GitHub. In todays tutorial we will build on the same topic but take a look at how we can also perform full end to end automation that includes:
- Automated dependency checks for Terraform modules using GitHub dependabot.
- Triggering an automated Terraform test when dependabot opens a Pull Request (PR) on the version change.
- Test if the terraform code changes in the PR will work.
- If all tests are successful automatically merge the PR.
- Once the PR is merged automatically create a new release of the public module on the public Terraform registry.
Public Marketplace GitHub Actions
I actually wrote a public GitHub action which I will use in this tutorial to demonstrate the automated tests. The GitHub action we will be using is called: Terraform Tests for AZURE.
Dependabot
If you look at the following GitHub project: Terraform module repository - Dynamic Subnets, you will see there is a special folder path called .github
and inside that folder is a YAML
file called: dependabot.yml.
version: 2
updates:
- package-ecosystem: 'terraform' # See documentation for possible values
directory: '/' # Location of terraform version file
schedule:
interval: 'daily'
This dependabot file enables dependabot on our GitHub project repository and will daily
check our repository root folder, directory: '/'
for any terraform files that have provider versions configured and checks if the provider versions are the latest version.
We discussed dependabot in much more detail on the previous tutorial automate terraform module releases on the public registry using GitHub, so take a look at the previous tutorial for more details on enabling dependabot.
Automated Testing
As we know dependabot will automatically open a PR if it detects that a version change is available for our terraform module and also show us the file changes.
It is important to note that Dependabot will only open a PR and nothing else. Normally after a PR has been opened a user will look at the PR, perform a code review, and have to also manually go and test whether the changes in that PR will actually work before merging the PR and then creating a new release using the new version of the code.
What we will do instead is automate the entire process from the moment the PR is opened by dependabot by creating a workflow to trigger and run based on the Pull Request event.
NOTE: In this tutorial we look at PRs opened by dependabot, but the testing action can be used in other contexts as well where a user/developer makes any sort of changes/additions/configs to a module, for example if new resources are added to a module. The same testing GitHub Action: Terraform Tests for AZURE can also be used in such cases to test if those changes will work.
Creating the Workflow
In our GitHub project: Terraform module repository - Dynamic Subnets, inside of the .github
folder/path, you will see another folder/path called workflows
, there is a YAML
workflow called: dependency-tests.yml.
### This workflow will run only when Dependabot opens a PR on master ###
### Full integration test is done by doing a plan, build and destroy of config under ./tests/auto_test1 ###
### If tests are successful the PR is automatically merged to master ###
### If the merge was completed the next patch version is released and the patch is bumped and pushed to terraform registry ###
name: 'Automated-Dependency-Tests-and-Release'
on:
workflow_dispatch:
pull_request:
branches:
- master
jobs:
# Dependabot will open a PR on terraform version changes, this 'dependabot' job is only used to test TF version changes by running a plan, apply and destroy in sequence.
dependabot-plan-apply-destroy:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
actions: read
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Checkout
uses: actions/checkout@v3.6.0
- name: Run Dependency Tests - Plan AND Apply AND Destroy
uses: Pwd9000-ML/terraform-azurerm-tests@v1.0.6
with:
test_type: plan-apply-destroy ## (Required) Valid options are "plan", "plan-apply", "plan-apply-destroy". Default="plan"
path: 'tests/auto_test1' ## (Optional) Specify path to test module to run.
tf_version: latest ## (Optional) Specifies version of Terraform to use. e.g: 1.1.0 Default="latest"
tf_vars_file: testing.tfvars ## (Required) Specifies Terraform TFVARS file name inside module path (Testing vars)
tf_key: tf-mod-tests-dyn-subn ## (Required) AZ backend - Specifies name that will be given to terraform state file and plan artifact (testing state)
az_resource_group: TF-Core-Rg ## (Required) AZ backend - AZURE Resource Group hosting terraform backend storage account
az_storage_acc: tfcorebackendsa ## (Required) AZ backend - AZURE terraform backend storage account
az_container_name: ghdeploytfstate ## (Required) AZ backend - AZURE storage container hosting state files
arm_client_id: ${{ secrets.ARM_CLIENT_ID }} ## (Required - Dependabot Secrets) ARM Client ID
arm_client_secret: ${{ secrets.ARM_CLIENT_SECRET }} ## (Required - Dependabot Secrets) ARM Client Secret
arm_subscription_id: ${{ secrets.ARM_SUBSCRIPTION_ID }} ## (Required - Dependabot Secrets) ARM Subscription ID
arm_tenant_id: ${{ secrets.ARM_TENANT_ID }} ## (Required - Dependabot Secrets) ARM Tenant ID
github_token: ${{ secrets.GITHUB_TOKEN }} ## (Required) Needed to comment output on PR's. ${{ secrets.GITHUB_TOKEN }} already has permissions.
##### If dependency tests are successful merge the pull request #####
merge_pr:
needs: dependabot-plan-apply-destroy
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Checkout
uses: actions/checkout@v3.6.0
with:
token: ${{secrets.GITHUB_TOKEN}}
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.1.1
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
- name: Auto-merge PR after tests
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
##### Create and automate new release based on next patch version of releases #####
release_new_version:
needs: merge_pr
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Determine version
id: version
uses: zwaldowski/semver-release-action@v2
with:
bump: patch
dry_run: true
github_token: ${{secrets.GITHUB_TOKEN}}
- name: Create new release and push to registry
id: release
uses: ncipollo/release-action@v1
with:
generateReleaseNotes: true
name: 'v${{ steps.version.outputs.version }}'
tag: ${{ steps.version.outputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
This workflow will only trigger if a Pull Request
is opened on the master
branch:
name: 'Automated-Dependency-Tests-and-Release'
on:
workflow_dispatch:
pull_request:
branches:
- master
It consists out of 3 jobs: dependabot-plan-apply-destroy:
, merge_pr
, and release_new_version
. Lets take a closer look at each job.
dependabot-plan-apply-destroy:
This first job uses my public github action: Terraform Tests for AZURE
# Dependabot will open a PR on terraform version changes, this 'dependabot' job is only used to test TF version changes by running a plan, apply and destroy in sequence.
dependabot-plan-apply-destroy:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
actions: read
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Checkout
uses: actions/checkout@v3.6.0
- name: Run Dependency Tests - Plan AND Apply AND Destroy
uses: Pwd9000-ML/terraform-azurerm-tests@v1.0.6
with:
test_type: plan-apply-destroy ## (Required) Valid options are "plan", "plan-apply", "plan-apply-destroy". Default="plan"
path: 'tests/auto_test1' ## (Optional) Specify path to test module to run.
tf_version: latest ## (Optional) Specifies version of Terraform to use. e.g: 1.1.0 Default="latest"
tf_vars_file: testing.tfvars ## (Required) Specifies Terraform TFVARS file name inside module path (Testing vars)
tf_key: tf-mod-tests-dyn-subn ## (Required) AZ backend - Specifies name that will be given to terraform state file and plan artifact (testing state)
az_resource_group: TF-Core-Rg ## (Required) AZ backend - AZURE Resource Group hosting terraform backend storage account
az_storage_acc: tfcorebackendsa ## (Required) AZ backend - AZURE terraform backend storage account
az_container_name: ghdeploytfstate ## (Required) AZ backend - AZURE storage container hosting state files
arm_client_id: ${{ secrets.ARM_CLIENT_ID }} ## (Required - Dependabot Secrets) ARM Client ID
arm_client_secret: ${{ secrets.ARM_CLIENT_SECRET }} ## (Required - Dependabot Secrets) ARM Client Secret
arm_subscription_id: ${{ secrets.ARM_SUBSCRIPTION_ID }} ## (Required - Dependabot Secrets) ARM Subscription ID
arm_tenant_id: ${{ secrets.ARM_TENANT_ID }} ## (Required - Dependabot Secrets) ARM Tenant ID
github_token: ${{ secrets.GITHUB_TOKEN }} ## (Required) Needed to comment output on PR's. ${{ secrets.GITHUB_TOKEN }} already has permissions.
As you can see we have an if:
expression to say that the job should only run if the PR was opened by dependabot, also note that the action I am using in this job will need the ability to add comments/issues on the Pull Request to display any issues and the plans from the terraform runs.
To do this a special token called GITHUB_TOKEN
is needed, by default this token in the context of dependabot will be read-only and so we can give the token additional permissions as you can see from the following YAML
code:
permissions:
pull-requests: write
issues: write
actions: read
if: ${{ github.actor == 'dependabot[bot]' }}
NOTE: To see what extra permissions can be granted to the GITHUB_TOKEN
see: Permissions for the github_token
The automated tests are then run using the action: uses: Pwd9000-ML/terraform-azurerm-tests@v1.0.6
The following inputs can be used:
Input | Required | Description | Default |
---|---|---|---|
test_type |
FALSE | Specify test type. Valid options are plan , plan-apply , plan-apply-destroy . |
"plan" |
path |
FALSE | Specify path to Terraform module relevant to repo root. (Test module) | "." |
tf_version |
FALSE | Specifies the Terraform version to use. | "latest" |
tf_vars_file |
TRUE | Specifies Terraform TFVARS file name inside module path. (Test vars) | N/A |
tf_key |
TRUE | AZ backend - Specifies name that will be given to terraform state file and plan artifact | N/A |
az_resource_group |
TRUE | AZ backend - AZURE Resource Group name hosting terraform backend storage account | N/A |
az_storage_acc |
TRUE | AZ backend - AZURE terraform backend storage account name | N/A |
az_container_name |
TRUE | AZ backend - AZURE storage container hosting state files | N/A |
arm_client_id |
TRUE | The Azure Service Principal Client ID | N/A |
arm_client_secret |
TRUE | The Azure Service Principal Secret | N/A |
arm_subscription_id |
TRUE | The Azure Subscription ID | N/A |
arm_tenant_id |
TRUE | The Azure Service Principal Tenant ID | N/A |
github_token |
TRUE | Specify GITHUB TOKEN, only used in PRs to comment outputs such as plan , fmt , init and validate . ${{ secrets.GITHUB_TOKEN }} already has permissions, but if using own token, ensure repo scope. |
N/A |
This action has a special input called test_type:
which can be used to run different types of tests:
-
test_type: "plan"
- This test type will only perform a terraform
plan
ONLY against a terraform configuration.
- This test type will only perform a terraform
-
test_type: "plan-apply"
- This test type will perform a terraform
plan
AND a terraformapply
in sequence against a terraform configuration.
- This test type will perform a terraform
-
test_type: "plan-apply-destroy"
- This test type will perform a terraform
plan
, a terraformapply
AND a terraformdestroy
in sequence against a terraform configuration.
- This test type will perform a terraform
WARNING: Apply tests will create resources in your environment. Please be aware of cost and also please be aware of the environment used. When applying new resources ensure you are using a test subscription or test resource group inside of your test configuration file being targeted by the path:
input or by using testing vars via a test TFVARS
file.
It is also really important to mention that the tests action requires a few secrets to be set on the GitHub repository, such as ARM_CLIENT_ID
, ARM_CLIENT_SECRET
, ARM_SUBSCRIPTION_ID
, ARM_TENANT_ID
, by navigating to Settings->Security->Secrets.
Also note that dependabots secrets are managed separately to actions secrets. So if the tests action is used in normal Actions PRs then the secrets needs to be added to the Actions secrets, but because we are working with PRs made by Dependabot specifically, we have to add these secrets to the Dependabot secrets:
As you can see I have written a terraform test in the path:
path: "tests/auto_test1"
terraform {
backend "azurerm" {}
}
provider "azurerm" {
features {}
}
##################################################
# MODULE TO TEST #
##################################################
module "dynamic-subnets-test" {
source = "../.."
network_address_ip = var.network_ip
network_address_mask = var.network_mask
virtual_network_rg_name = var.resource_group_name
virtual_network_name = var.vnet_name
subnet_config = var.subnet_config
}
As you can see my terraform test is a simple terraform configuration using a source: source = "../.."
which will target the root module hosted at the root of my project repo.
The test actually involves creating a terraform plan, followed by an apply, followed by a destroy in sequence, as I selected input: test_type: plan-apply-destroy
Any issues or plans during the tests are then added to the PR as well as artifacts on the workflow.
merge_pr:
The next job in our workflow will only be called if the previous job that did the tests were successful using needs: dependabot-plan-apply-destroy
:
##### If dependency tests are successful merge the pull request #####
merge_pr:
needs: dependabot-plan-apply-destroy
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Checkout
uses: actions/checkout@v3.6.0
with:
token: ${{secrets.GITHUB_TOKEN}}
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.1.1
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
- name: Auto-merge PR after tests
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
This job will automatically merge the Pull Request opened by dependabot and the PR will be closed as all tests were successful. Again we need to give the GITHUB_TOKEN
some additional permissions to be able to merge and close the PR.
release_new_version:
The last job in our workflow will only be called if the previous job was successful in merging the PR needs: merge_pr
:
##### Create and automate new release based on next patch version of releases #####
release_new_version:
needs: merge_pr
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Determine version
id: version
uses: zwaldowski/semver-release-action@v2
with:
bump: patch
dry_run: true
github_token: ${{secrets.GITHUB_TOKEN}}
- name: Create new release and push to registry
id: release
uses: ncipollo/release-action@v1
with:
generateReleaseNotes: true
name: 'v${{ steps.version.outputs.version }}'
tag: ${{ steps.version.outputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
This job will look at the project releases and determine what the next release version (semantic versioning) should be and then creates a new release automatically.
We can decide what we want to increment the version by specifying: bump: patch
(This can also be bump: minor
or bump: major
) depending on how you want to perform your releases, but because this use case is just a version change of the terraform provider dependency a patch
increment should be fine.
As you can see a new release will automatically be created and reflected on the terraform registry:
I hope you have enjoyed this post and have learned something new. You can find the code samples used in this blog post on my GitHub project Terraform module repository - Dynamic Subnets. ❤️
If you are interested in checking out my public terraform modules on the registry here they are:
I will be adding a few more cool modules on the public registry in due course.
Author
Like, share, follow me on: 🐙 GitHub | 🐧 X/Twitter | 👾 LinkedIn
Posted on February 14, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 5, 2022